Spring AOP的理解
目录
AOP是什么
Spring AOP的实现原理
Spring AOP的使用-基于@AspectJ的配置
3.1 声明切面
3.2 声明切入点
3.3 声明通知
3.3.1 前置通知
3.3.2 后置通知
3.3.3 环绕通知
3.3.4 引入通知
基于XML Schema的配置
总结,直接背
1. AOP是什么
AOP是一种编程范式,它的目的是通过分离横切关注点来提升代码的模块化程度。所谓关注点就是一段特定的功能,有些关注点出现在多个模块中,就成为横切关注点。
AOP解决了两个问题:第一是代码混乱,核心的业务逻辑代码还必须兼顾其他功能,这就导致不同功能的代码交织在一起,可读性很差;第二是代码分散,同一个功能的代码分散在多个模块中,不易维护。
在AOP中有几个重要的概念:
概念 | 说明 |
切面(aspect) | 按关注点进行模块分解时,横切关注点就表示为一个切面 |
连接点(join point) | 程序执行的某一刻,在这个点上可以添加额外的动作 |
通知(advice) | 切面在特定连接点上执行的动作 |
切入点(pointcut) | 切入点是用来描述连接点的,它决定了当前代码与连接点是否匹配 |
通过切入点来匹配程序中的特定连接点,在这些连接点上执行通知,这种通知可以是在连接点前后执行,也可以是将连接点包围起来。
此外,声明式事务就是依靠AOP来实现的,Spring还为我们提供了简单的方式来使用AOP,可以选择基于注解或XML的方式来配置AOP相关的功能。
2. Spring AOP的实现原理
Spring AOP背后的核心技术是动态代理技术。代理模式是23中经典设计模式之一,我们可以为某个对象提供一个代理,控制对该对象的访问,代理可以在两个有调用关系的对象之间起到中介的作用——代理封装了目标对象,调用者调用了代理的方法,代理再去调用实际的目标对象。如图:
动态代理就是在运行时动态地为对象创建代理的技术。在Spring中,由AOP框架创建、用来实现切面的对象被称为AOP代理,一般采用JDK动态代理或者是CGLIB代理,两者在使用时的区别如下:
必须要实现接口 | 支持拦截public方法 | 支持拦截protected方法 | 拦截默认作用域方法 | |
JDK动态代理 | 是 | 是 | 否 | 否 |
CGLIB代理 | 否 | 是 | 是 | 是 |
Spring容器在为Bean注入依赖时,会自动将被依赖Bean的AOP代理注入进来,这就让我们感觉是在使用原始的Bean,其实不然。被切面拦截的对象称为目标对象或通知对象,因为Spring用了动态代理,所以目标对象就是要被代理的对象。
以JDK动态代理为例,假设我们希望在代码示例的方法执行前后增加两句日志,可以采用下面这套代码,
2-1 要被动态代理的Hello接口及其实现片段
public interface Hello {
void say();
}
public class SpringHello implements Hello {
@Override
public void say() {
System.out.println("Hello Spring!");
}
}
随后设计一个InvocationHandler,于是对代理对象的调用都会转为调用invoke方法,传入的参数中就包含了所调用的方法和实际的参数。
2-2 在Hello.say()前后打印日志的InvocationHandler
public class LogHandler implements InvocationHandler {
private Hello source;
public LogHander(Hello source) {
this.source = source;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Ready to say something.");
try{
return method.invoke(source, args);
} finally {
System.out.println("Already say something.");
}
}
}
最后,在通过Proxy.newProxyInstance()为Hello实现类的Bean实例创建使用LogHandler的代理,如2-3 创建JDK动态代理并调用方法
public class Application {
public static void main(String[] args) {
Hello original = new SpringHello();
Hello target = (Hello) Proxy.newProxyInstance(Hello.class.getClassLoader(),
original.getClass().getInterfaces(), new LogHandler(original));
target.say();
}
}
这段代码的运行结果如下:
Ready to say something.
Hello Spring!
Already say something.
Spring AOP的实现方式与上述例子大同小异,如果深究可以阅读proxyFactoryBean的源码,若是采用JDK动态代理,AopProxyFactory会创建JdkDynamicAopProxy;若是采用CGLIB代理,则是创建ObjenesisCglibAopProxy,前者的逻辑就和上述的例子差不多。
另外在Spring AOP中,为了能用到被AOP增强过的方法,我们应该始终与代理对象交互。如果存在一个类的内部方法调用,这个调用的对象不是代理,而是其本身,则无法享受AOP增强的效果。
比如下面这个类中的foo()方法调用了bar(),哪怕Spring AOP对bar()做了拦截,由于调用的不是代理对象,因而看不到任何效果。
public class Hello {
public void foo() {
bar();
}
public void bar() {...}
}
实际项目中这也可能会引起事务失效。
3. Spring AOP的使用-基于@AspectJ的配置
首先要引入org.springframework:spring-aspects依赖,然后通过@EnableAspectJAutoProxy注解或XML配置开启对@AspectJ的支持。
另外Spring Boot的自动配置中的AopAutoConfiguration类已经帮助我们默认开启了对@AspectJ的支持。
Spring 5.x默认JDK动态代理;Spring Boot 2.x默认CGLIB代理。二者的主要区别是JDK动态代理只能代理接口,而后者没有这个限制。
JKD动态代理与CGLIB代理的区别:
必须要实现接口 | 支持拦截public方法 | 支持拦截protected方法 | 拦截默认作用域方法 | |
JDK动态代理 | 是 | 是 | 否 | 否 |
CGLIB代理 | 否(比如支持将代理对象赋值给实现类) | 是 | 是 | 是 |
虽然CGLIB支持拦截非public作用域的方法调用,但在不同对象之间交互时,建议还是以public方法调用为主。
3.1 声明切面
只需要在类上面添加@Aspect注解即可。
注意:
- 添加@Aspect注解只是告诉Spring“这个类是切面”,但并没有把它声明为Bean,因此需要另外配置为Bean。
- Spring Framework会对带有@Aspect注解的类做特殊对待,因为其本身就是一个切面,所以不会被别的切面自动拦截。
3.2 声明切入点
注解方式的切入点声明由两部分组成——切入点表达式和切入点方法签名。前者用来描述要匹配的连接点,后者可以用来引用切入点,方便切入点的复用,一些简单的切入点声明如下所示:
package learning.spring.helloworld;
public class HelloPointcut {
@Pointcut("target(learning.spring.helloworld.Hello)")
public void helloType() {} // 目标对象是learning.spring.helloworld.Hello类型
@Pointcut("execution(public * say())")
public void sayOperation() {} // 执行public的say()方法
@Pointcut("helloType() && sayOperation()") // 复用其他切入点
public void sayHello() {} // 执行Hello类型中public的say()方法
}
@Pointcut注解中使用的就是AspectJ 5的表达式,其中一些常用的PCD(pointcut designator,切入点标识符)如下所示:
PCD | 说明 |
execution | 最常用的一个PCD,用来匹配特定方法的执行 |
within | 匹配特定范围内的类型,可以用通配符来匹配某个Java包内的所有类 |
this | 略 |
target | 略 |
args | 略 |
bean | 略 |
另外还有针对注解的PCD:
PCD | 说明 |
@target | 执行的目标对象带有特定类型注解 |
@args | 传入的方法参数带有特定类型注解 |
@annotation | 拦截的方法上带有特定类型注解 |
@within | 比较特殊,下面详细说 |
因此我们可以通过配合自定义注解,灵活的实现定义切入点,比如先定义一个注解learning.spring.helloworld.Log,然后使用如下切入点:
public class LogAspect {
@Pointcut("@within(learning.spring.helloworld.Log) ||
@annotation(learning.spring.helloworld.Log)")
public void logPoint() {}
}
表示拦截在类和方法上添加了@Log注解的所有方法,也可以看出切入点表达式支持&&、||、!。
关于@within:
Spring AOP虽然使用了AspectJ 5的切入点表达式,也共用了不少AspectJ的PCD,但其实两者还是有区别的。比如,Spring AOP 中仅支持有限的PCD,AspectJ中还有很多PCD是Spring AOP不支持的。
由于Spring AOP的实现基于动态代理,因而只能匹配普通方法的执行,像静态初始化、静态方法、构造方法、属性赋值等操作都是拦截不到的。所以说相比AspectJ而言,Spring AOP的功能弱很多,但在大部分场景下也基本够用。
在Spring AOP中,@target与 @within两者在使用上感受不到什么区别。前者要求运行时的目标对象带有注解,这个注解的@Retention是 RetentionPolicy.RUNTIME,即运行时的;后者要求被拦截的类上带有Retention是RetentionPolicy.CLASS 的注解。但Spring AOP只能拦截到非静态public方法的执行,两个PCD的效果一样,所以还是老老实实用@target 吧。
关于Spring AOP 中@target与 within的差异,StackOverflow上有一篇名为“Difference between @target and @witia(Spring AOP)”的文章说得比较清楚,感兴趣的同学可以参考。
(至于为什么上面示例我写的是@within,因为项目里就是用的这个。。。不过结合上面Spring和Spring Boot关于默认代理模式的不同,猜想也可能和这个有关系。)
3.3 声明通知
3.3.1 前置通知
@Before注解,注解中可以引用实现定义好的切入点,也可以直接传入一个切入点表达式(几个通知都一样),在被拦截到的方法开始执行前,会先执行通知中的代码:
@Aspect
public class BeforeAspect {
@Before("learning.spring.helloworld.HelloPointcut.sayHello()")
public void before() {
System.out.println("Before Advice");
}
}
前置通知的方法没有返回值,因为它在被拦截的方法前执行,就算有返回值也没地方使用,但是它可以对被拦截方法的参数进行加工,通过args这个PCD能明确参数,并将其绑定到前置通知方法的参数上。例如,要在sayHello(AtomicInteger)这个方法前对AtomicInteger类型的参数进行数值调整,就可以这样做:
@Before("learning.spring.helloworld.HelloPointcut.sayHello() && args(count)")
public void before(AtomicInteger count) {
// 操作count
}
要是同时存在多个通知作用于同一处,可以让切面类实现Ordered接口,或者在上面添加@Order注解。指定的值越低,优先级则越高。(几个通知都一样)
3.3.2 后置通知
@AfterReturing注解用于拦截正常返回的调用,可以有返回值,使用returning属性获得,就暂时不举例了。
@AfterThrowing注解拦截抛出异常的调用,可以有返回值,使用throwing属性获得。
@After注解不关注执行是否成功,必须要能够处理正常与异常两种情况,但获取不到返回值,因此一般只被用来做一些资源清理的工作。
3.3.3 环绕通知
环绕通知的功能比较强大,不仅可以在方法执行前后加入自己的逻辑,甚至可以完全替换方法本身的逻辑,或者替换调用参数。我们可以添加@Around注解来声明环绕通知,这个方法的签名需要特别注意,它的第一个参数必须是ProceedingJoinPoint类型的,方法的返回类型是被拦截方法的返回类型,或者直接用Object类型。
例如,我们希望统计say()方法的执行时间,可以如下声明环绕通知:
@Aspect
public class TimerAspect {
@Around("execution(public * say(...))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try{
return pjp.proceed();
} finally {
long end = System.currentTimeMillis();
System.out.println("Total time: " + (end - start) + "ms");
}
}
}
实际项目中做个性化日志处理一般也是用环绕通知,可以通过ProceedingJoinPoint对象得到被拦截的类名、方法名等信息。
3.3.4 引入通知
不太常用,暂时先略过
4. 基于XML Schema的配置
需要配置XML、实现不同的接口,现在一般不用这种方式,就略过吧。
5. 总结,直接背
AOP是一种编程范式,它的目的是通过分离横切关注点来提升代码的模块化程度。所谓关注点就是一段特定的功能,有些关注点出现在多个模块中,就成为横切关注点。
AOP解决了两个问题:第一是代码混乱,核心的业务逻辑代码还必须兼顾其他功能,这就导致不同功能的代码交织在一起,可读性很差;第二是代码分散,同一个功能的代码分散在多个模块中,不易维护。
在AOP中有几个重要的概念:切面、连接点、 通知、切入点。
它们之间的联系是:通过切入点来匹配程序中的特定连接点,在这些连接点上执行通知,这种通知可以是在连接点前后执行,也可以是将连接点包围起来。
此外,声明式事务就是依靠AOP来实现的,Spring还为我们提供了简单的方式来使用AOP,可以选择基于注解或XML的方式来配置AOP相关的功能。
Spring AOP背后的核心技术是动态代理技术,动态代理就是在运行时动态地为对象创建代理的技术。Spring容器在为Bean注入依赖时,会自动将被依赖Bean的AOP代理注入进来,被切面拦截的对象称为目标对象,因为Spring用了动态代理,所以目标对象就是要被代理的对象。
代理对象底层是通过AopProxyFactory生成的,如果是采用JDK动态代理,AopProxyFactory会创建JdkDynamicAopProxy;如果是CGLIB代理,则是创建ObjenesisCglibAopProxy。
JDK动态代理织入AOP切面的原理是InvocationHandler接口,里面的invoke方法的入参中就包含了所调用的方法和实际的参数。
在项目中AOP用到最多的就是日志,可以通过先声明切面,然后在切面类里面声明切入点和通知,切入点也可以先定义自定义注解,然后拦截所有用这个日志注解标记的类和方法。
零散点:
- 由于AOP的原理是动态代理,所以如果存在一个类的内部方法调用,这个调用的对象不是代理,而是其本身,则无法享受AOP增强的效果。
- Spring 5.x默认JDK动态代理;Spring Boot 2.x默认CGLIB代理。二者的主要区别是JDK动态代理只能代理接口,而后者没有这个限制。
- 环绕通知的功能比较强大,不仅可以在方法执行前后加入自己的逻辑,甚至可以完全替换方法本身的逻辑,或者替换调用参数。它的第一个参数必须是ProceedingJoinPoint类型的,方法的返回类型是被拦截方法的返回类型,或者直接用Object类型。可以通过ProceedingJoinPoint对象得到被拦截的类名、方法名等信息。
- 事务失效的场景:数据库不支持事务;事务没有被Spring管理;是通过内部调用;JDK动态代理时访问权限不是public、CGLIB代理时访问权限不是public或protect;异常然后外层被try包住。
还没有评论,来说两句吧...