今天给大家带来的是Spring AOP讲解,那么什么是Spring AOP呢?
AOP,全称是Aspect Oriented Programming,就是面向切面编程的意思。我觉得AOP是一种思想,一种优秀的思想,它把我们业务代码上一些公共的特性部分给抽取出来,做成一个切面,减少我们重复开发代码的工作量,同时也使得代码结构更加美观。
下面举一个通俗的栗子
在疫情严控的今天,假设有一家商场要开门营业了。但是要求检测所有进来的客人的体温。那么我们可以有几种方案:
1、 通知商场里面的每一家店铺,要求他们在接待顾客的时候先检测他们的体温,这样所有店铺的职责功能就是检测顾客体温 + 开展自己的业务。
2、 商场在所有的入口,统一设立检查口,所有顾客经过体温检测且结果正常的人才允许进入商场,这样商场内的各类店铺就不需要检测顾客的体温,只需要正常开展自己的业务就好了。
这里就很明显了,第二种方案更好。因为它把原本所有店铺都需要检测体温的工作给抽取出来,交给了商场管理来做。这样,在顾客角度来看,体验是比较好的,每次进入商场,只需要在门口检测一次体温即可,而不是每去一家店铺,就要检测一次体温。对于商场管理来说,只是多设立了一道检测工作,并且这个检测由自己来做是相对比较可靠的,因为不排除有些商家他尽管收到了要帮顾客检测体温的通知,但是他忘记要去帮顾客检测体温。并且,在将来变更检测内容的时候,只需要让检测口变更一下检测的内容即可,而不需要通知到每一家店铺。甚至将来疫情解除了,我们不需要检测顾客的体温,那我们可以直接撤掉检测点,这一切对于商场里面的店铺来说,都是不可感知的。
好了,栗子啃完了,我们说回正题。那么AOP在我们实际的代码开发中有什么应用呢?
看下面的一张图,你就可以很直观地知道AOP的实用场景了。
性能监测、权限校验、日志记录和事务控制这些都穿插在我们实际的核心业务之中,这些功能其实与我们的业务关联性不大,但是又是必须的。因此,我们可以把它们抽取出来,形成一个切面。
这里挑一个日志记录结合例子来讲
我们可以添加这样的切面,来收集所有访问我们controller层日志信息。
普通的访问:http://127.0.0.1:8080/getEmailRecords 这里,我们只会输出邮件发送记录的数据,并无其他的日志信息。
前置通知(@Before):http://127.0.0.1:8080/beforeGetEmailRecords 先打印日志,再打印数据。
后置通知(@After):http://127.0.0.1:8080/afterGetEmailRecords 打印了数据之后,再打印日志
返回通知(@AfterReturning):http://127.0.0.1:8080/afterReturningGetEmailRecords
返回通知同样也会在目标方法执行后打印日志,那么它与后置通知的区别在哪里?后置通知与返回通知的区别就在于,如果目标方法执行出现了异常,那么后置通知是可以正常打印日志的,因为它的代码其实是在finally里面。但是如果返回通知的目标方法出现了异常,那么返回通知是无法正常打印日志信息,因为它的代码紧跟在目标方法之后,出现了异常它就被略过了。
异常通知(@AfterThrowing):http://127.0.0.1:8080/afterThrowingGetEmailRecords 异常通知,目标方法抛出异常的时候触发。需要注意的是,假如我们自己捕获了目标方法的异常并且处理了,那么就不会触发异常通知了。
环绕通知(@Around):http://127.0.0.1:8080/aroundGetEmailRecords 环绕通知就是把各种通知结合起来,在目标方法的调用前和调用后都可以进行一个额外的动作。
下面给出各种通知在方法执行前后的位置图
如果,在一个方法上加上多个通知,效果会怎样?
一般来说,会有那么两种情况。
第一种,我们只定义了一个切面类,那么各种通知的执行顺序如下:
无异常:@Around(proceed()之前的部分) → @Before → 方法执行 → @Around(proceed()之后的部分) → @After → @AfterReturning
有异常:@Around(proceed(之前的部分)) → @Before → 扔异常ing → @After → @AfterThrowing (大概是因为方法没有跑完抛了异常,没有正确返回所有@Around的proceed()之后的部分和@AfterReturning两个注解的加强没有能够织入)
第二种,定义了多个切面类。
单个Aspect肯定是和只有一个Aspect的时候的情况是一样的,但不同的Aspect里面的advice的顺序呢??答案是不一定,像是线程一样,没有谁先谁后,除非你给他们分配优先级,同样地,在这里你也可以为@Aspect分配优先级,这样就可以决定谁先谁后了。
优先级有两种方式:
实现org.springframework.core.Ordered接口,实现它的getOrder()方法
给aspect添加@Order注解,该注解全称为:org.springframework.core.annotation.Order
无论用什么方式来定义顺序,数值越小,优先级越高。
多个前置通知叠加:http://127.0.0.1:8080/manyBeforeGetEmail
这里我们定义了两个切面类EmailAspect1和EmailAspect2,也分别定义了他们的顺序。那么方法按照预期的顺序运行,但是有一点值得关注的,如果在同一个切面类里面,定个多个同类型的通知顺序,那么这个通知的实际执行顺序非但不生效,而且还会以一种诡异的顺序运行,暂时无法解释这个现象,姑且先把它看作是无规则的。
前面我们通过了一些例子来看到AOP的神奇功能,那么我们怎么在项目中应用它?(以Spring boot v2.2.4 项目为例子)
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
1
2
3
4
5
定义一个切面类和编写代码
@Aspect
@Component
public class EmailAspect {
Logger logger = LoggerFactory.getLogger(EmailAspect.class);
/**
* 前置通知
*/
@Before("execution(public * com.example.demo.controller.EmailRecordsController.beforeGetEmailRecords(..))")
public void doBeforeEmail(){
logger.info("前置通知:有个人想要来查看邮件发送记录");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
代码中,我们为一个普通的类增加了@Aspect 注解,还在它的方法上添加了@Before注解,这样,其实就AOP就已经完成了。
那么现在的问题,就是怎么理解@Before注解括号里面的内容了。
在理解这个内容之前,我们不可避免地需要引入一些专业的术语了。
Aspect(切面): 由切点和通知构成,定义通知应该应用到哪些函数
PointCut(切入点): 指定对哪些目标函数进行切入
Advice(通知): 在某个切入点上执行的动作,还记得我们前面定义的那五种通知吗?
Joinpoint(连接点): 指定了目标函数
Weaving(织入):把切面应用到目标函数的过程,后面会讲解织入的概念。
这里最容易让人混淆的就是切入点和连接点的定义了。在SpringAOP中,所有的对象方法都可以是连接点(joinpoint),所以,我们通过切入点(pointcut)来描述,匹配哪些的方法可以被织入通知(advice)。所以,切入点和连接点其实是两个不同维度的东西。
在了解了这些概念定义以后,我们回到上面的那个问题,如何理解@Before注解括号里面的内容。首先是execution,这个是表达式主体,用来匹配连接点的一个标识符,类似的还有很多,具体的可以在使用的时候进行百度。紧跟着的
public * com.example.demo.controller.EmailRecordsController.beforeGetEmailRecords(…))
就是用来匹配目标函数,它支持通配符进行匹配。就是这样,一个简单切面就定义好了。
揭开织入的神秘面纱
Spring AOP 是基于动态织入的动态代理技术。动态代理又分为 JAVA JDK 动态代理和CGLIB动态代理。
JDK动态代理是基于反射技术来实现。假设我们需要执行A类的方法a,那么我们传统的方式是
A obj = new A();
obj.a();
1
2
3
但是现在基于反射技术,我们可以这样写。
小结一下,运用JDK动态代理,被代理类(目标对象,如A类),必须已有实现接口如(ExInterface),因为JDK提供的Proxy类将通过目标对象的类加载器ClassLoader和Interface,以及句柄(Callback)创建与A类拥有相同接口的代理对象proxy,该代理对象将拥有接口ExInterface中的所有方法,同时代理类必须实现一个类似回调函数的InvocationHandler接口并重写该接口中的invoke方法,当调用proxy的每个方法(如案例中的proxy#execute())时,invoke方法将被调用,利用该特性,可以在invoke方法中对目标对象(被代理对象如A)方法执行的前后动态添加其他外围业务操作,此时无需触及目标对象的任何代码,也就实现了外围业务的操作与目标对象(被代理对象如A)完全解耦合的目的。当然缺点也很明显需要拥有接口。
运行结果:
CGLIB 动态代理(Code Generation Library,一个强大的,高性能,高质量的Code生成类库)动态代理实现上述功能并不要求目标对象拥有接口类,实际上CGLIB动态代理是通过继承的方式实现的。
从代码看被代理的类无需接口即可实现动态代理,而CGLibProxy代理类需要实现一个方法拦截器接口MethodInterceptor并重写intercept方法,类似JDK动态代理的InvocationHandler接口,也是理解为回调函数,同理每次调用代理对象的方法时,intercept方法都会被调用,利用该方法便可以在运行时对方法执行前后进行动态增强。关于代理对象创建则通过Enhancer类来设置的,Enhancer是一个用于产生代理对象的类,作用类似JDK的Proxy类,因为CGLib底层是通过继承实现的动态代理,因此需要传递目标对象(如A)的Class,同时需要设置一个回调函数对调用方法进行拦截并进行相应处理,最后通过create()创建目标对象(如A)的代理对象,运行结果与前面的JDK动态代理效果相同。
运行结果:
回顾
通过前面的简短描述,相信大家已经对Spring AOP已经有了一个初步的认识。我们的目标对象交给Spring 进行管理(所以,我们能够织入通知的对象,它必定是Spring bean),通过JAVA JDK代理或者CGLIB(一般默认是JAVA JDK代理)生成了代理对象,然后我们预先定义的各种通知(增强)会被特殊的类编译器织入到目标对象之中,这样,在目标对象执行了相应的方法时,我们的增强也会按照预设的规则去执行。关于织入,这里还要提及一点就是,AOP有两个主要流行的框架,一个是aspectJ,另外一个就是我们的Spring AOP。aspectJ的织入有三个时间点,分别是编译期织入,类装载期织入和动态织入。
而Spring AOP采用的就是动态织入, 在运行期为目标类添加增强生成子类的方式。
————————————————
版权声明:本文为CSDN博主「weixin_40561490」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_40561490/article/details/105557043
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




