学习AOP,我有如下4点疑问:
- 什么是AOP
- AOP的原理是什么
- 如何实现AOP
- AOP和注解有什么联系
一、什么是AOP
AOP:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
主要功能:日志记录、性能统计、安全控制、事物处理、异常处理等。
消除代码重复、交叉和纠缠。
切面横切各个功能。
一些基本概念:
Aspect(切面):通常是一个类,里面可以定义切入点和通知。
Pointcut(切入点):就是带有通知的连接点
Advice(通知):AOP在特定的切入点上执行的增强处理(动作),有before(前置),after(后置),afterReturning(最终),afterThrowing(异常),around(环绕)
JointPoint(连接点):程序执行过程中明确的点,一般是方法的调用。
Target Object(目标对象): 包含连接点的对象。也被称作被通知或被代理对象。
weave(织入):将切面应用到目标对象并导致代理对象创建的过程
introduction(引入):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
通知分类说明
Before前置通知:在目标方法被调用之前做增强处理,@Before只需要指定切入点表达式即可
AfterReturning返回后通知:在目标方法正常完成后做增强,@AfterReturning除了指定切入点表达式后,还可以指定一个返回值形参名returning,代表目标方法的返回值
AfterThrowing异常通知:在目标方法抛出异常退出时执行的操作,主要用来处理程序中未处理的异常,@AfterThrowing除了指定切入点表达式后,还可以指定一个throwing的返回值形参名,可以通过该形参名来访问目标方法中所抛出的异常对象
After后通知:在目标方法完成之后做增强,无论目标方法是否成功完成。@After可以指定一个切入点表达式
Around环绕通知:在目标方法完成前后做增强处理,环绕通知是最重要的通知类型,像事务,日志等都是环绕通知,入参必须是ProceedingJoinPoint
参考:https://www.jianshu.com/p/5b9a0d77f95f
二、AOP原理是什么
实现原理有如下两种方式:
- 预编译:AspectJ --- 未研究,暂时不做阐述
- 运行期动态代理(JDK动态代理、CGLIB动态代理):SpringAOP、JbossAOP
JDK动态代理:与静态代理一样,目标类需要实现一个代理接口,通过代理类调用目标类的方法
CgLib动态代理:对指定的业务类生成一个子类,并覆盖其中的业务方法来实现代理。
动态代理:代理类通过实现和目标类一样的接口(方便目标类注入该类中)或继承目标类的方式来代理目标类调用方法执行操作,在代理类调用连接点方法前、后可以执行一些操作,这个操作就是前置、后置操作等。
参考这篇博客,关于动态代理,写的浅显易懂:https://www.cnblogs.com/bqh10086/p/spring_aop_dynamic_proxy.html
这篇也不错:https://www.cnblogs.com/ruanraun/p/javaaop.html
注意事项:
- JDK动态代理和CgLib动态代理的主要区别:
JDK动态代理只能针对实现了接口的类的接口方法进行代理
CgLib动态代理基于继承来实现代理,所以无法对final类、private方法和static方法实现代理
- Spring AOP中的代理使用的默认策略是(什么时候用jdk和cglib):
如果目标对象实现了接口,则默认采用JDK动态代理
如果目标对象没有实现接口,则采用CgLib进行动态代理
如果目标对象实现了接口,且强制CgLib代理,则采用CgLib进行动态代理
- jdk和cglib那个效率高?
jdk1.6和1.7时,JDK动态代理的速度要比CGLib动态代理的速度要慢
jdk1.8时,JDK动态代理的速度已经比CGLib动态代理的速度快很多了
参考博客:https://blog.csdn.net/xlgen157387/article/details/82497594
三、如何实现AOP
以spring boot为例,利用注解实现AOP,相比xml方式实现,较为简单。
注意关注执行结果,有助理解各类通知的执行顺序。
aop maven依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
注解类:
package com.example.demo1.jwt;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** TYPE: Class, interface (including annotation type), or enum declaration */
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface LogInject {
}
切面类(包含切入点和通知):
package com.example.demo1.jwt;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogInjectParser {
/**定义切入点,@within 表示使用LogInject注解的类中的方法为切入点 */
@Pointcut("@within(com.example.demo1.jwt.LogInject)")
public void logPointcut(){}
/**前置通知,和切入点绑定*/
@Before("logPointcut()")
public void before(){
System.out.println("before");
}
@AfterReturning("logPointcut()")
public void afterreturning(){
System.out.println("afterreturning");
}
@After("logPointcut()")
public void after(){
System.out.println("after");
}
@AfterThrowing("logPointcut()")
public void afterthrowing(){
System.out.println("afterthrowing");
}
/**环绕通知,入参必须是ProceedingJoinPoint */
@Around("logPointcut()")
public Object around(ProceedingJoinPoint pjp)
{
Object object = null;
try {
System.out.println("around1");
object = pjp.proceed();
}
catch (Throwable throwable) {
throwable.printStackTrace();
}
finally {
System.out.println("around2");
}
return object;
}
}
连接点实现类:
(1)
package com.example.demo1.jwt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@LogInject
@RestController
@RequestMapping({ "/xxx" })
public class JwtRest {
@Autowired
public JwtService jwtService;
/**连接点1*/
@RequestMapping(value = "/yyy", method = RequestMethod.POST)
public void test1() {
System.out.println("JwtRestTest1");
jwtService.test1();
throw new RuntimeException();
}
}
(2)
package com.example.demo1.jwt;
import org.springframework.stereotype.Component;
@LogInject
@Component
public class JwtService {
/**连接点2*/
public void test1(){
System.out.println("JwtServiceTest1");
}
}
请求接口 /xxx/yyy,日志打印如下:
...
2020-06-16 10:48:24.023 INFO 12568 --- [nio-9111-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 21 ms
around1 // <--连接点1,函数执行前,[around通知] 打印1
before // <--连接点1,函数执行前,[before通知] 打印
JwtRestTest1 // <--连接点1,函数执行中打印,后续调用连接点2函数
around1 // <--连接点2,函数执行前,[around通知] 打印1
before // <--连接点2,函数执行前,[before通知] 打印
JwtServiceTest1 // <--连接点2,函数执行中打印,后续返回连接点1函数
afterreturning // <--连接点2,函数正常返回后,[afterreturning通知] 打印
after // <--连接点2,函数返回后,[after通知] 打印
around2 // <--连接点2,函数执行后,[around通知] 打印2
afterthrowing // <--连接点1,函数继续执行,抛出运行异常,[afterthrowing通知] 打印
after // <--连接点1,函数返回后,[after通知] 打印
java.lang.RuntimeException
at com.example.demo1.jwt.JwtRest.test1(JwtRest.java:20)
...
四、AOP技术和注解有什么联系
AOP和注解是两种不同的概念,不要混为一谈,AOP可以用自定义注解的方式实现,但AOP != 注解。上面解释了什么是AOP,下面解释下什么是注解,以下只记录部分我关注的内容,详细内容可参考:https://www.runoob.com/w3cnote/java-annotation.html
(1)概念:
Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。
Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容 。 当然它也支持自定义 Java 标注。
(2)内置的注解:
Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中。
作用在代码的注解是
- @Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
- @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
- @SuppressWarnings - 指示编译器去忽略注解中声明的警告。
作用在其他注解的注解(或者说 元注解)是:
- @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
- @Documented - 标记这些注解是否包含在用户文档中。
- @Target - 标记这个注解应该是哪种 Java 成员。
- @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
从 Java 7 开始,额外添加了 3 个注解:
- @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
- @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
- @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
(3)部分注解说明:
@interface
使用 @interface 定义注解时,意味着它实现了 java.lang.annotation.Annotation 接口,即该注解就是一个Annotation。
定义 Annotation 时,@interface 是必须的。
注意:它和我们通常的 implemented 实现接口的方法不同。Annotation 接口的实现细节都由编译器完成。通过 @interface 定义注解后,该注解不能继承其他的注解或接口。
@Documented
类和方法的 Annotation 在缺省情况下是不出现在 javadoc 中的。如果使用 @Documented 修饰该 Annotation,则表示它可以出现在 javadoc 中。
定义 Annotation 时,@Documented 可有可无;若没有定义,则 Annotation 不会出现在 javadoc 中。
@Target(ElementType.TYPE)
前面我们说过,ElementType 是 Annotation 的类型属性。而 @Target 的作用,就是来指定 Annotation 的类型属性。
@Target(ElementType.TYPE) 的意思就是指定该 Annotation 的类型是 ElementType.TYPE。这就意味着,MyAnnotation1 是来修饰"类、接口(包括注释类型)或枚举声明"的注解。
定义 Annotation 时,@Target 可有可无。若有 @Target,则该 Annotation 只能用于它所指定的地方;若没有 @Target,则该 Annotation 可以用于任何地方。
@Retention(RetentionPolicy.RUNTIME)
前面我们说过,RetentionPolicy 是 Annotation 的策略属性,而 @Retention 的作用,就是指定 Annotation 的策略属性。
@Retention(RetentionPolicy.RUNTIME) 的意思就是指定该 Annotation 的策略是 RetentionPolicy.RUNTIME。这就意味着,编译器会将该 Annotation 信息保留在 .class 文件中,并且能被虚拟机读取。
定义 Annotation 时,@Retention 可有可无。若没有 @Retention,则默认是 RetentionPolicy.CLASS。
(4)什么是反射
反射是一面镜子,镜子外站的是对象,镜子里是类的字节码对象(class文件在jvm中生成的对象),通过class对象可得到类的所有构造方法、成员变量和方法。
反射机制:Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。
参考链接:https://blog.csdn.net/weixin_43027596/article/details/105560469,这篇讲反射蛮好的
(4)切入点指示符
execution:用于匹配方法执行的连接点
@within:用于匹配所以持有指定注解类型内的方法
详细可参考:https://blog.csdn.net/qq_23167527/article/details/78623639