学习AOP时,我们首先来了解一下何为AOP
一. 概念
AOP(面向切面编程,Aspect Oriented Programming)是一种编程技术,旨在通过预编译方式或运行期动态代理实现程序功能的统一管理和增强。AOP的主要目标是在不改变原有业务逻辑代码的基础上,添加或修改非核心业务逻辑,如日志记录、性能监控、安全检查等。这种方式有助于减少代码重复,提高代码的模块化程度,降低系统组件间的耦合度,进而提升软件的可维护性和可扩展性。
AOP的核心概念包括:
切面(Aspect):切面是关注点的模块化,它横切多个对象。例如,事务管理就是一个典型的切面。
连接点(Join Point):程序执行过程中的某个点,如方法调用或异常抛出。AOP框架会在这个点上应用切面。
通知(Advice):切面在特定的连接点上执行的动作。根据调用时机不同,通知分为前置通知、后置通知、环绕通知等。
切入点(Pointcut):定义了通知应该应用于哪些连接点的规则。它是切面的一个组成部分。
用图来表示
注意的是:所有方法都是一个连接点,而我们需要增强的就是一个切入点
二. 底层
AOP其实是基于代理模式来实现的.而在Java当中,分为静态代理和动态代理两种模式.
简单描述一下静态代理,着重表示一下动态代理.
2.1 静态代理
静态代理: 你可以理解为就是硬编码, 例如现在一个计算器类,分别有加减乘除功能.而此时你想增强这些方法,例如在这些方法上面添加一些日志,那静态代理的实现就是创建一个新的类,聚合一下这个计算器类,也实现计算器类实现的接口,也就是加减乘除. 去实现具体的加减乘除时还是调用原本计算器类的方法,只是在调用这个方法前后可以书写你要增加的逻辑.
2.2 动态代理
在动态代理当中我们分为两种代理方式.
2.2.1 JDK动态代理
JDK的代理是基于接口实现的.如果一个类没有实现接口,是不可以使用JDK的动态代理的.
2.2.1.1 演示
我通过一个火车站售票的一个场景来演示一下动态代理是怎么实现的.
创建一个售票的接口,里面有两个方法,一个售票,一个打印
创建一个火车站类,实现这个接口
在JDK中,我们可以调用Proxy.newProxyInstance()方法来获取一个代理对象.
- 第一个参数为加载当前需要代理的类的类加载器,
- 第二个参数为需要代理的类的实现的接口(这也就证明是基于接口来实现的)
- 第三个参数需要一个实现InvocationHandler接口的类,可以用匿名内部类来实现,这一块也就是用来增加逻辑的一块.
我们创建一个代理工厂的类来实现这段逻辑
测试类
测试结果
2.2.2 CGLIB动态代理
CGLIB是可以基于接口也可以基于类实现的.
2.2.2.1 演示
还是买票这个例子,只不过我们换种实现.
而使用CGLIB代理时,我们需要去实现MethodInterceptor接口,创建代理对象时,需要使用CGLIB提供的Enhance类来实现.
这里使用了三个方法.
setInterfaces与setSuperclass通过方法名我们知道,是用来设置代理类的接口与父类.侧面也证实了CGLIB是可以基于接口和类实现的.
而setCallback的作用是设置回调对象,回调对象在CGLIB生成的代理对象中用于处理方法调用。具体来说:
- 当通过代理对象调用方法时,CGLIB会拦截该方法调用,并将控制权交给回调对象。
- 回调对象可以对方法调用进行增强处理,如添加日志、事务管理、权限检查等。
- 在这个例子中,回调对象被设置为当前对象(this),意味着当前对象需要实现CGLIB的回调接口(如MethodInterceptor),以便在方法调用时执行特定的逻辑.
所以这就是为什么需要实现MethodInterceptor方法.
测试类
测试结果
我们尝试将Transtaion实现的接口不实现,看能否代理成功
测试类
测试结果
我们清晰看到代理没有任何问题,也是通过代理对象调用的.
二. AOP实战演练
我们简单书写一个计算机的工具类
现在我们来书写一个AOP切面类.
介绍一下这里使用到的几个重要注解的作用
- @Aspect(切面类): 声明此类为一个AOP切面类,需要扫描方法上Before等注解
- @PointCut(切入点): 切入点表达式,选取对应的连接点来作为切入点
- execution: 切入点表达式,以 返回值 + [全限类名] + 方法名(参数类型),* 指代任意类型
- @Before(前置通知): 方法调用前执行
- @After(后置通知): 方法调用后执行
- @AfterThrowing(异常后通知): 方法执行时发生异常执行
- @AfterReturning(方法返回后通知): 方法能正确返回值执行.
他们的调度过程可以用一张图来表示
我们执行测试类来查看测试结果
这里有两个细节
- 第一个表明了AOP拦截的执行顺序为前置,返回(无异常时),后置
- 第二个表明,我们查看到实际上执行对应方法的是代理对象,而更注意的是SpringBoot的底层使用的是CGLIB代理.但实际上我们还看到一个执行对象就是我们具体的实现CaculatorImpl,这两个是不一样的,在使用add方法的入口时,使用的是AOP代理对象,而实际去执行方法时,还是调用我们实现的CaculatorImpl去实现的.
这就是我们基于Spring AOP来简单实现了一下如何应用.
那么复杂一点的情形,现在有两个切面类同时应用到了同一个方法,那么又会是怎么样的情形呢?
我们再书写一个权限的切面类,来查看会有什么现象产生
同样启动我们的测试实例,来查看效果
我们观察到执行顺序如下面这一张图来表示
所以我们了解到有AOP嵌套时的,执行顺序会变成这样的流程,会把另一个AOP的过程当成整体作为执行流程.
还有一个情形,就是当我们使用环绕通知时,我们是可以利用环绕通知模拟这四个情形,我们修改我们原来日志的代码
利用环绕通知来模拟四种情形,启动一下测试
至此AOP基本功能我们都测试完毕,还有一种情形我们一直没有探究,当我们执行时出现异常时,AOP的执行流程是什么样子的呢?
测试结果为 前置 -> 执行 -> 异常 -> 后置
那我们再开启一下我们之前的权限AOP,我们捕获了异常,此时会发生什么现象呢?(由于异常日志太多,我这里捕获异常就不打印啦)
在权限这里,我们发现他并没有走异常处理的增强,执行流程为下方图示这样
所以,我们通常建议环绕通知时,捕获异常后,继续往上抛出异常,让其他的AOP对象感知到异常的存在,执行对应的异常处理.