1. 概述
1.1. AOP定义
AOP(Aspect Oriented Programming),为面向切面编程或面向方面编程,是一种编程范式。旨在将交叉切入关注与作为业务主体的核心关注进行分离,以提高程序代码的模块化程度。
面向方面编程的核心概念,是从核心关注中分离出交叉切入关注。面向方面编程,在支配性分解的基础上,提供叫做方面(aspect)的一种辅助的模块化机制,这种新的模块化机制可以捕捉交叉切入关注。
(参考维基百科)
简单来说,面向切面编程就是面向特定方法编程。通过将横切关注点(cross-cutting concerns)从主要业务逻辑中分离出来,提供一种更好的代码模块化和可维护性。
横切关注点指的是在应用程序中横跨多个模块或层的功能,例如日志记录、事务管理、安全性、缓存、异常处理等。
1.2. 特点
- 模块化和解耦合: 将横切关注点与核心逻辑解耦,提高代码的模块化程度。
- 降低代码复杂度: 简化核心逻辑,提高代码的可读性。
- 集中化关注点: 将多个模块间共同的关注点集中管理,提高代码的重用性。
- 代码无入侵:通过切入点表达式扫描对应方法进行动态代理增强,而不需要修改源代码。
1.3. 应用
- 日志记录:记录方法的调用、参数和返回值,记录数据库的操作,用于调试和性能分析。
- 事务管理:管理事务的开始、提交、回滚等操作,确保数据的一致性和完整性。
- 权限校验:实施安全控制,例如身份验证和授权。
- 异常处理:捕获并处理异常,避免它们影响核心业务逻辑。
- 性能监控:监控方法的执行时间,以便识别性能瓶颈并进行优化。
- 数据缓存:实现数据的缓存,提高系统的响应速度。
- 追踪审计:记录用户操作,用于追踪和审计系统的使用情况。
1.4. SpringBootAOP
Spring AOP是Spring框架提供的一种AOP实现方式。AOP是一种编程范式,而Spring AOP是Spring框架对AOP的具体实现。
SpringBoot AOP是基于Spring AOP的另一种AOP实现方式,专门针对Spring Boot应用程序提供的一种简化配置和使用的方式。
1.5. AOP相关术语
切面(Aspect):是指横切多个对象的关注点的一个模块化,事务管理就是J2EE应用中横切关注点的很好示例。在Spring AOP中,切面通过常规类(基本模式方法)或者通过使用了注解@Aspect的常规类来实现。
连接点(Joint point):是指在程序执行期间的一个点,比如某个方法的执行或者是某个异常的处理。在Spring AOP中,一个连接点往往代表的是一个方法执行。
通知(Advice):是指切面在某个特殊连接点上执行的动作。通知有不同类型,包括 "around" ,"before" 和 "after" 通知。许多AOP框架包括Spring,将通知建模成一个拦截器,并且围绕连接点维持一个拦截器链。
切入点(Pointcut):是指匹配连接点的一个断言。通知是和一个切入点表达式关联的,并且在任何被切入点匹配的连接点上运行(举例,使用特定的名字执行某个方法)。AOP的核心就是切入点表达式匹配连接点的思想。Spring默认使用AspectJ切入点表达式语言
引入(Introduction):代表了对一个类型额外的方法或者属性的声明。Spring AOP允许引入新接口到任何被通知对象(以及一个对应实现)。比如,可以使用一个引入去使一个bean实现IsModified接口,从而简化缓存机制。(在AspectJ社区中,一个引入也称为一个inter-type declaration类型间声明)
目标对象(Target object):是指被一个或多个切面通知的那个对象。也指被通知对象("advised object"),由于Spring AOP是通过运行时代理事项的,这个目标对象往往是一个代理对象。
AOP 代理(AOP proxy):是指通过AOP框架创建的对象,用来实现切面合约的(执行通知方法等等)。在Spring框架中,一个AOP代理是一个JDK动态代理或者是一个CGLIB代理。
织入(Weaving):将切面和其他应用类型或者对象连接起来,创建一个被通知对象。这些可以在编译时(如使用AspectJ编译器)、加载时或者运行时完成。Spring AOP,比如其他纯Java AOP框架一般是在运行时完成织入。
名称 |
说明 |
连接点 |
可以被AOP控制的方法 |
通知 |
指那些重复的逻辑,也就是共性功能(最终体现为一个方法) |
切入点 |
匹配连接点的条件,通知仅会在切入点方法执行时被应用,在aop的开发当中,我们通常会通过一个切入点表达式来描述切入点 |
切面 |
当通知和切入点结合在一起,就形成了一个切面。 |
目标对象 |
通知所应用的对象(被AOP加强的对象) |
切面类 |
被@Aspect注解标识的类,切面所在位置 |
AOP 代理 |
通过AOP框架创建的对象,被AOP加强的方法本质是AOP框架常见一个代理类继承原始类,重写被AOP加强/控制的方法 |
1.6. AOP Advice 相关术语
- 前置通知(Before advice):在一个连接点之前执行的通知。但这种通知不能阻止连接点的执行流程(除非它抛出一个异常)
- 后置返回通知(After returning advice):在一个连接点正常完成后执行的通知(如,如果一个方法没有抛出异常的返回)
- 后置异常通知(After throwing advice):在一个方法抛出一个异常退出时执行的通知。
- 后置通知(After(finally) advice):在一个连接点退出时(不管是正常还是异常返回)执行的通知。
- 环绕通知(Around advice):环绕一个连接点的通知,比如方法的调用。这是一个最强大的通知类型。环绕通知可以在方法调用之前和之后完成自定义的行为。也负责通过返回自己的返回值或者抛出异常这些方式,选择是否继续执行连接点或者简化被通知方法的执行。
2. SpringBoot AOP入门
2.1. 基本步骤
- 导入依赖
<!--Aop依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
- 编写切面类
import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Component // 把当前类的对象交给spring容器去管理 @Aspect // 代表当前是一个切面类 @Slf4j //日志 public class TimeAspect { //配置规则 @Around("execution(* com.system.controller.*.*(..))") public Object recordTime(ProceedingJoinPoint jp) throws Throwable{ long start = System.currentTimeMillis(); Object obj = jp.proceed(); long end = System.currentTimeMillis(); log.info("执行方法用了:{}ms",end - start); return obj; } }
2.2. 流程解析:
底层通过动态代理形式,创建一个子类去继承接口实现类,重写接口类的方法。
2.3. 通知类型
在入门程序当中,我们已经使用了一种功能最为强大的通知类型:Around环绕通知。
其他通知类型对应的注解如下:
@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行 |
@Before:前置通知,此注解标注的通知方法在目标方法前被执行 |
@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行 |
@AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行 |
@AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行 |
@PointCut:将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可。 |
@Pointcut("execution(* com.itheima.service.*.*(..))") // 抽取切入点表达式 public void pt(){} @Before("pt()") public void before(JoinPoint jp){ System.out.println("执行了before"); } @After("pt()") public void after(JoinPoint jp){ System.out.println("执行了after"); } @AfterReturning("pt()") public void afterReturning(JoinPoint jp){ System.out.println("执行了AfterReturning"); } @AfterThrowing("pt()") public void afterThrowing(JoinPoint jp){ System.out.println("执行了AfterThrowing"); }
注意事项:
- @Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
- @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。
2.4. 通知顺序
当在项目开发当中,我们定义了多个切面类,而多个切面类中多个切入点都匹配到了同一个目标方法。此时当目标方法在运行的时候,这多个切面类当中的这些通知方法都会运行。这多个通知方法到底哪个先运行,哪个后运行?
结论:
- 不同切面类中,默认按照切面类的类名字母排序;
- 可使用@Order注解,控制通知的执行顺序;@Order()内数字从小到大执行,从大到小退出;类似于过滤器链。
@Slf4j @Component @Aspect @Order(2) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行) public class MyAspect2 { //前置通知 @Before("execution(* com.system.service.*.*(..))") public void before(){ log.info("MyAspect2 -> before ..."); } //后置通知 @After("execution(* com.system.service.*.*(..))") public void after(){ log.info("MyAspect2 -> after ..."); } }
2.5. 切入点表达式
2.5.1. execution
execution(......):用于匹配特定方法名的方法
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?) ● 其中带 ? 的表示可以省略的部分 ● 访问修饰符:可省略(比如: public、protected) ● 返回值:常使用*代替,表示任意类型 ● 包名.类名: 可省略(不建议省略) ● 方法名:需正确 ● 方法参数: 常使用..表示,表任意类型 ● throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常) 可以使用通配符描述切入点 * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数, 也可以通配包、类、方法名的一部分 .. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
//全称 //假如指向DeptController内方法add(Dept dept) @Around("execution(public com.system.pojo.Result com.system.controller.DeptController.add(com.system.pojo.Dept) )") //假如指向DeptService内方法findById(Integer id) @Before("execution(public com.system.pojo.Dept com.system.service.DeptService.findById(java.lang.Integer))") //常见 @Around("execution(* com.system.controller.*.*(..))")
2.5.2. annotation
@annotation(……) :用于匹配标识有特定注解的方法。
1.自定义注解 ================================================ import java.lang.annotation.ElementType; import java.lang.annotation.Target; @Target(ElementType.METHOD) public @interface MyAnno { } 2.标记方法 ================================================ @MyAnno @Override public Dept findById(Integer id) { return deptMapper.findById(id); } 3.切面表达式连接 ================================================ @Before("@annotation(com.system.anno.MyAnno)") public void before(JoinPoint jp){ System.out.println("执行了before"); }
注意事项:
- 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。
- 切入表达式通常关注接口,而非其接口实现类,增强拓展性。
- 在满足业务需要的前提下,尽量缩小切入点的匹配范围。可将execution和annatation搭配使用。
- 包名匹配尽量不使用 ..
- 使用 * 匹配单个包
- 不建议省略类名.包名
2.6. 连接点
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。其中:
对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
对于其他四种通知,获取连接点信息只能使用 JoinPoint ,它是 ProceedingJoinPoint 的父类型
import com.system.anno.MyAnno; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.*; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.lang.annotation.Annotation; import java.util.Arrays; @Component @Aspect @Slf4j @Order(1) public class DemoAspect { @Pointcut("execution(* com.system.service.*.*(..))") // 抽取切入点表达式 public void pt(){} //通过 ProceedingJoinPoint 获取方法运行相关信息 @Around("pt()") public Object arround(ProceedingJoinPoint jp) throws Throwable { Object obj = jp.getTarget(); // 获取目标对象【被代理的对象】 String className = obj.getClass().getSimpleName(); // 获取类名 String name = obj.getClass().getName(); // 获取全类名 Signature signature = jp.getSignature(); // 获取方法的签名 String methodName = signature.getName(); // 获取方法名 Object[] args = jp.getArgs(); // 获取方法执行的参数 Object res = jp.proceed(); // 执行完方法,得到方法的返回值 System.out.println("obj = " + obj); System.out.println("className = " + className); System.out.println("name = " + name); System.out.println("signature = " + signature); System.out.println("methodName = " + methodName); System.out.println("args:" + Arrays.toString(args)); System.out.println("res:" + res); return res; } /* obj = com.itheima.service.impl.DeptServiceImpl@581fb6b9 className = DeptServiceImpl signature = List com.itheima.service.impl.DeptServiceImpl.findAll() methodName = findAll args:[] res:[Dept(id=1, name=学66, createTime=2024-02-03T15:36:52, updateTime=2024-02-03T16:35:43), Dept(id=2, name=教研部, createTime=2024-02-03T15:36:52, updateTime=2024-02-03T15:36:52), Dept(id=3, name=咨询部4, createTime=2024-02-03T15:36:52, updateTime=2024-02-03T22:49:06), Dept(id=4, name=就业部, createTime=2024-02-03T15:36:52, updateTime=2024-02-03T15:36:52), Dept(id=5, name=人事部, createTime=2024-02-03T15:36:52, updateTime=2024-02-03T15:36:52)] */ }
3. 底层原理
Spring AOP 的实现原理是基于动态代理和字节码操作的。在编译时, Spring 会使用 AspectJ 编译器将切面代码编译成字节码文件。在运行时, Spring 会使用 Java 动态代理或 CGLIB 代理生成代理类,这些代理类会在目标对象方法执行前后插入切面代码,从而实现AOP的功能。
Spring AOP 可以使用两种代理方式:JDK动态代理和 CGLIB 代理。如果目标对象实现了至少一个接口,则使用JDK动态代理;否则,使用 CGLIB 代理。SpringBoot默认使用 CGLIB 代理。
3.1. JDK动态代理
public class JdkDynamicAopProxy implements AopProxy, InvocationHandler { private final AdvisedSupport advised; public JdkDynamicAopProxy(AdvisedSupport advised) { this.advised = advised; } @Override public Object getProxy() { return Proxy.newProxyInstance( getClass().getClassLoader(), advised.getTargetSource().getInterfaces(), this ); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { MethodInterceptor methodInterceptor = advised.getMethodInterceptor(); MethodInvocation methodInvocation = new ReflectiveMethodInvocation( advised.getTargetSource().getTarget(), method, args, methodInterceptor, advised.getTargetSource().getTargetClass() ); return methodInvocation.proceed(); } }
在该代码中,JdkDynamicAopProxy 类实现了 AopProxy 和 InvocationHandler 接口。getProxy 方法返回一个代理对象,该代理对象实现了目标对象实现的所有接口。invoke 方法用于执行代理方法,该方法会在目标对象方法执行前后插入切面代码。
3.2. CGLIB动态代理
CGLIB 代理是一个基于字节码操作的代理方式,它可以为没有实现接口的类创建代理对象。CGLIB 代理会在运行时生成一个目标对象的子类,并覆盖其中的方法,以实现AOP的功能。
import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; public class CglibProxy implements MethodInterceptor { private Object target; public Object getProxy(Object target) { this.target = target; Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(this.target.getClass()); // 设置代理目标 enhancer.setCallback(this); // 设置回调 return enhancer.create(); // 创建代理对象 } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("方法调用前的处理"); Object result = proxy.invokeSuper(obj, args); // 调用目标对象的方法 System.out.println("方法调用后的处理"); return result; } }
在该代码中,CglibAopProxy 类实现了 AopProxy 接口。getProxy 方法返回一个代理对象,该代理对象是目标对象的子类,并覆盖了其中的方法。DynamicAdvisedInterceptor 类实现了 MethodInterceptor 接口,用于在目标对象方法执行前后插入切面代码。