Spring AOP的理解

小咪咪 2024-03-18 00:30 172阅读 0赞

目录

  1. AOP是什么

  2. Spring AOP的实现原理

  3. Spring AOP的使用-基于@AspectJ的配置

3.1 声明切面

3.2 声明切入点

3.3 声明通知

3.3.1 前置通知

3.3.2 后置通知

3.3.3 环绕通知

3.3.4 引入通知

  1. 基于XML Schema的配置

  2. 总结,直接背


1. AOP是什么

AOP是一种编程范式,它的目的是通过分离横切关注点来提升代码的模块化程度。所谓关注点就是一段特定的功能,有些关注点出现在多个模块中,就成为横切关注点。

AOP解决了两个问题:第一是代码混乱,核心的业务逻辑代码还必须兼顾其他功能,这就导致不同功能的代码交织在一起,可读性很差;第二是代码分散,同一个功能的代码分散在多个模块中,不易维护。

在AOP中有几个重要的概念:
























概念 说明
切面(aspect) 按关注点进行模块分解时,横切关注点就表示为一个切面
连接点(join point) 程序执行的某一刻,在这个点上可以添加额外的动作
通知(advice) 切面在特定连接点上执行的动作
切入点(pointcut) 切入点是用来描述连接点的,它决定了当前代码与连接点是否匹配

通过切入点来匹配程序中的特定连接点,在这些连接点上执行通知,这种通知可以是在连接点前后执行,也可以是将连接点包围起来。

此外,声明式事务就是依靠AOP来实现的,Spring还为我们提供了简单的方式来使用AOP,可以选择基于注解或XML的方式来配置AOP相关的功能。

2. Spring AOP的实现原理

Spring AOP背后的核心技术是动态代理技术。代理模式是23中经典设计模式之一,我们可以为某个对象提供一个代理,控制对该对象的访问,代理可以在两个有调用关系的对象之间起到中介的作用——代理封装了目标对象,调用者调用了代理的方法,代理再去调用实际的目标对象。如图:

08450e52ec864516934ce7d4d603d6a2.png

动态代理就是在运行时动态地为对象创建代理的技术。在Spring中,由AOP框架创建、用来实现切面的对象被称为AOP代理,一般采用JDK动态代理或者是CGLIB代理,两者在使用时的区别如下:

























必须要实现接口 支持拦截public方法 支持拦截protected方法 拦截默认作用域方法
JDK动态代理
CGLIB代理

Spring容器在为Bean注入依赖时,会自动将被依赖Bean的AOP代理注入进来,这就让我们感觉是在使用原始的Bean,其实不然。被切面拦截的对象称为目标对象或通知对象,因为Spring用了动态代理,所以目标对象就是要被代理的对象。

以JDK动态代理为例,假设我们希望在代码示例的方法执行前后增加两句日志,可以采用下面这套代码,

2-1 要被动态代理的Hello接口及其实现片段

  1. public interface Hello {
  2. void say();
  3. }
  4. public class SpringHello implements Hello {
  5. @Override
  6. public void say() {
  7. System.out.println("Hello Spring!");
  8. }
  9. }

随后设计一个InvocationHandler,于是对代理对象的调用都会转为调用invoke方法,传入的参数中就包含了所调用的方法和实际的参数。

2-2 在Hello.say()前后打印日志的InvocationHandler

  1. public class LogHandler implements InvocationHandler {
  2. private Hello source;
  3. public LogHander(Hello source) {
  4. this.source = source;
  5. }
  6. @Override
  7. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  8. System.out.println("Ready to say something.");
  9. try{
  10. return method.invoke(source, args);
  11. } finally {
  12. System.out.println("Already say something.");
  13. }
  14. }
  15. }

最后,在通过Proxy.newProxyInstance()为Hello实现类的Bean实例创建使用LogHandler的代理,如2-3 创建JDK动态代理并调用方法

  1. public class Application {
  2. public static void main(String[] args) {
  3. Hello original = new SpringHello();
  4. Hello target = (Hello) Proxy.newProxyInstance(Hello.class.getClassLoader(),
  5. original.getClass().getInterfaces(), new LogHandler(original));
  6. target.say();
  7. }
  8. }

这段代码的运行结果如下:

  1. Ready to say something.
  2. Hello Spring!
  3. Already say something.

Spring AOP的实现方式与上述例子大同小异,如果深究可以阅读proxyFactoryBean的源码,若是采用JDK动态代理,AopProxyFactory会创建JdkDynamicAopProxy;若是采用CGLIB代理,则是创建ObjenesisCglibAopProxy,前者的逻辑就和上述的例子差不多。


另外在Spring AOP中,为了能用到被AOP增强过的方法,我们应该始终与代理对象交互。如果存在一个类的内部方法调用,这个调用的对象不是代理,而是其本身,则无法享受AOP增强的效果。

比如下面这个类中的foo()方法调用了bar(),哪怕Spring AOP对bar()做了拦截,由于调用的不是代理对象,因而看不到任何效果。

  1. public class Hello {
  2. public void foo() {
  3. bar();
  4. }
  5. public void bar() {...}
  6. }

实际项目中这也可能会引起事务失效。

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注解即可。

注意:

  1. 添加@Aspect注解只是告诉Spring“这个类是切面”,但并没有把它声明为Bean,因此需要另外配置为Bean。
  2. Spring Framework会对带有@Aspect注解的类做特殊对待,因为其本身就是一个切面,所以不会被别的切面自动拦截。

3.2 声明切入点

注解方式的切入点声明由两部分组成——切入点表达式和切入点方法签名。前者用来描述要匹配的连接点,后者可以用来引用切入点,方便切入点的复用,一些简单的切入点声明如下所示:

  1. package learning.spring.helloworld;
  2. public class HelloPointcut {
  3. @Pointcut("target(learning.spring.helloworld.Hello)")
  4. public void helloType() {} // 目标对象是learning.spring.helloworld.Hello类型
  5. @Pointcut("execution(public * say())")
  6. public void sayOperation() {} // 执行public的say()方法
  7. @Pointcut("helloType() && sayOperation()") // 复用其他切入点
  8. public void sayHello() {} // 执行Hello类型中public的say()方法
  9. }

@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,然后使用如下切入点:

  1. public class LogAspect {
  2. @Pointcut("@within(learning.spring.helloworld.Log) ||
  3. @annotation(learning.spring.helloworld.Log)")
  4. public void logPoint() {}
  5. }

表示拦截在类和方法上添加了@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注解,注解中可以引用实现定义好的切入点,也可以直接传入一个切入点表达式(几个通知都一样),在被拦截到的方法开始执行前,会先执行通知中的代码:

  1. @Aspect
  2. public class BeforeAspect {
  3. @Before("learning.spring.helloworld.HelloPointcut.sayHello()")
  4. public void before() {
  5. System.out.println("Before Advice");
  6. }
  7. }

前置通知的方法没有返回值,因为它在被拦截的方法前执行,就算有返回值也没地方使用,但是它可以对被拦截方法的参数进行加工,通过args这个PCD能明确参数,并将其绑定到前置通知方法的参数上。例如,要在sayHello(AtomicInteger)这个方法前对AtomicInteger类型的参数进行数值调整,就可以这样做:

  1. @Before("learning.spring.helloworld.HelloPointcut.sayHello() && args(count)")
  2. public void before(AtomicInteger count) {
  3. // 操作count
  4. }

要是同时存在多个通知作用于同一处,可以让切面类实现Ordered接口,或者在上面添加@Order注解。指定的值越低,优先级则越高。(几个通知都一样)

3.3.2 后置通知

@AfterReturing注解用于拦截正常返回的调用,可以有返回值,使用returning属性获得,就暂时不举例了。

@AfterThrowing注解拦截抛出异常的调用,可以有返回值,使用throwing属性获得。

@After注解不关注执行是否成功,必须要能够处理正常与异常两种情况,但获取不到返回值,因此一般只被用来做一些资源清理的工作。

3.3.3 环绕通知

环绕通知的功能比较强大,不仅可以在方法执行前后加入自己的逻辑,甚至可以完全替换方法本身的逻辑,或者替换调用参数。我们可以添加@Around注解来声明环绕通知,这个方法的签名需要特别注意,它的第一个参数必须是ProceedingJoinPoint类型的,方法的返回类型是被拦截方法的返回类型,或者直接用Object类型。

例如,我们希望统计say()方法的执行时间,可以如下声明环绕通知:

  1. @Aspect
  2. public class TimerAspect {
  3. @Around("execution(public * say(...))")
  4. public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
  5. long start = System.currentTimeMillis();
  6. try{
  7. return pjp.proceed();
  8. } finally {
  9. long end = System.currentTimeMillis();
  10. System.out.println("Total time: " + (end - start) + "ms");
  11. }
  12. }
  13. }

实际项目中做个性化日志处理一般也是用环绕通知,可以通过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包住。

发表评论

表情:
评论列表 (有 0 条评论,172人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Spring AOP理解(通俗易懂)

    这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。  1.我所知道的aop   初看aop,上来就是一大堆术语,而且还有个拉风的

    相关 4.Spring AOP理解

    1. AOP原理 AOP将应用系统分为两部分,核心业务逻辑(Core business concerns)及横向的通用逻辑,也就是所谓的方面Crosscuttin

    相关 spring aop 理解

    最近一直在面试,aop这个还是永远的热点,看不了不少大佬的关于aop的文章,感觉还是下面不错 常问的点 :  两种动态代理 区别 , 怎么强制使用CGLIB代理  切点和切面