1. 什么是AOP
AOP (Aspect Oriented Programming)面向切面编程,是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。AOP是⼀种思想,是对某⼀类事情的集中处理,它的实现方法有很多, Spring AOP、AspectJ、CGLIB等。
在传统的面向对象编程中,程序的功能逻辑被分散在各个对象中,而横切关注点则分散在多个对象之间,导致代码重复、可维护性差,并且难以修改和扩展。AOP 的目标就是解决这些问题。
Spring AOP 是Spring框架提供的一种面向切面编程的技术。它通过引入横切关注点(例如日志记录、事务管理、安全控制等),将其从主业务逻辑代码中分离出来,以模块化的方式实现对这些关注点的管理和重用。
切面(Aspect)是用来描述横切关注点,是对横切关注点的封装,切面定义了何时、在何处、如何应用横切关注点。在 AOP 中,切面可以横跨多个对象,独立于主业务逻辑。
2. Spring AOP 的核心概念
引⼊AOP依赖
在pom.xml⽂件中添加配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
我们以上述 记录Controller中每个方法的执行时间 这个程序来介绍下面的概念。
2.1. 切点(Pointcut)
用于定义在哪些连接点上应用通知(切面对哪些方法生效)。切点通过表达式进行定义,用于匹配目标对象的一组方法,如匹配所有 public 方法或匹配某个包下的所有方法等,在这些方法执行时切面会被触发。
上面的表达式 execution(* com.bite.demo.controller..(…)) 就是切点表达式。
2.2. 连接点(Joinpoint)
满足切点表达式规则的方法就是连接点,或者说程序中能够被切面插入的点就是连接点,典型的连接点包括方法调用、异常抛出等等。
比如上述例子中所有 com.bite.demo.controller 路径下的方法, 都是连接点。
2.3. 通知(Advice)
切面在特定切点上执行的代码,包括在连接点之前、之后或周围执行的行为。
比如上述程序中记录业务方法的消耗时间, 就是通知。
2.4. 切面(Aspect)
切面(Aspect) = 切点(Pointcut) + 通知(Advice)
切面是横切关注点的模块化单元,它将通知和切点组合在一起,描述了在何处、何时和如何应用横切关注点。
切面所在的类, 我们⼀般称为切面类(被@Aspect注解标识的类)。
2.5. 织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程,可以在编译时、加载时或运行时进行。在 Spring AOP 中,织入发生在运行时,通过代理对象的方式实现。
2.6. 通知类型
- @Around: 环绕通知, 此注解标注的通知方法在目标方法前, 后都被执行
- @Before:前置通知, 此注解标注的通知方法在目标方法前被执行
- @After: 后置通知, 此注解标注的通知方法在目标方法后被执行, 无论是否有异常都会执行
- @AfterReturning: 返回后通知, 此注解标注的通知方法在目标方法后被执行, 有异常不会执行
- @AfterThrowing: 异常后通知, 此注解标注的通知方法发生异常后执行
@Slf4j
@Component
@Aspect
public class AspectDemo {
@Before("execution(* com.xiaoyu.aop.controller.*.*(..))")
public void doBefore() {
log.info("执行AspectDemo.before方法...");
}
@After("execution(* com.xiaoyu.aop.controller.*.*(..))")
public void doAfter() {
log.info("执行AspectDemo.after方法...");
}
@AfterReturning("execution(* com.xiaoyu.aop.controller.*.*(..))")
public void doAfterReturn() {
log.info("执行AspectDemo.afterReturn方法...");
}
@AfterThrowing("execution(* com.xiaoyu.aop.controller.*.*(..))")
public void doAfterThrow() {
log.info("执行AspectDemo.afterThrow方法...");
}
@Around("execution(* com.xiaoyu.aop.controller.*.*(..))")
public Object doAround(ProceedingJoinPoint joinPoint) {
log.info("执行AspectDemo.around 目标方法前...");
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable e) {
log.info(joinPoint.toShortString() + "发生异常:e",e);
// throw new RuntimeException(e);
}
log.info("执行AspectDemo.around 目标方法后...");
return result;
}
}
测试类代码:
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/t1")
public String t1() {
log.info("执行t1方法");
return "t1";
}
@RequestMapping("/t2")
public String t2() {
log.info("执行t2方法");
int a = 10/0;
return "t2";
}
}
当before、after方法和around方法同时存在时,执行顺序:
当正常响应时,执行顺序:
当发生异常时,执行顺序:
注意:@AfterReturning 标识的通知⽅法不会执⾏, @AfterThrowing 标识的通知⽅法执⾏了。@Around 环绕通知中原始⽅法调⽤时有异常,通知中的环绕后的代码逻辑也不会在执⾏了。
2.7. @Pointcut
上面代码存在⼀个问题, 就是存在⼤量重复的切点表达式 execution(* com.xiaoyu.aop.controller..(…)) , Spring提供了 @PointCut 注解, 把公共的切点表达式提取出来, 需要用到时引用该切⼊点表达式即可。
上述代码就可以修改为:
当其他类使用这个公共切点时,需要写全限定类名:
执行结果:
2.8. 切面优先级@Order
当我们在⼀个项⽬中, 定义了多个切⾯类时, 并且这些切⾯类的多个切⼊点都匹配到了同⼀个⽬标⽅法。当⽬标⽅法运⾏的时候, 这些切⾯类中的通知⽅法都会执⾏, 那么这几个通知⽅法的执⾏顺序是什么样的呢?
定义多个切⾯类:
@Slf4j
@Component
@Aspect
public class AspectDemo2 {
@Before("com.xiaoyu.aop.aspect.AspectDemo.pt()")
public void doBefore() {
log.info("执行 AspectDemo2.doBefore 方法");
}
@After("com.xiaoyu.aop.aspect.AspectDemo.pt()")
public void doAfter() {
log.info("执行 AspectDemo2.doAfter 方法");
}
}
@Slf4j
@Component
@Aspect
public class AspectDemo3 {
@Before("com.xiaoyu.aop.aspect.AspectDemo.pt()")
public void doBefore() {
log.info("执行 AspectDemo3.doBefore 方法");
}
@After("com.xiaoyu.aop.aspect.AspectDemo.pt()")
public void doAfter() {
log.info("执行 AspectDemo3.doAfter 方法");
}
}
@Slf4j
@Component
@Aspect
public class AspectDemo4 {
@Before("com.xiaoyu.aop.aspect.AspectDemo.pt()")
public void doBefore() {
log.info("执行 AspectDemo4.doBefore 方法");
}
@After("com.xiaoyu.aop.aspect.AspectDemo.pt()")
public void doAfter() {
log.info("执行 AspectDemo4.doAfter 方法");
}
}
运行程序,访问接口:http://127.0.0.1:8080/test/t1
通过上述程序的运⾏结果, 可以看出:存在多个切面类时, 默认按照切面类的类名字母排序。但这种⽅式不⽅便管理, 我们的类名更多还是具备⼀定含义的。
Spring 给我们提供了⼀个新的注解, 来控制这些切⾯通知的执⾏顺序: @Order
使⽤⽅式如下:
重新运行程序,访问接口,观察运行结果:
@Order 控制切⾯的优先级,先执⾏优先级较⾼的切⾯, 再执⾏优先级较低的切⾯, 最终执⾏⽬标⽅法。Order的值越大,优先级越低。
3. Spring AOP的实现方式
切点表达式常见有两种表达⽅式:
- execution(…):根据⽅法的签名来匹配
- @annotation(…) :根据注解匹配
3.1. 基于注解实现AOP(@Aspect注解和execution表达式)
execution() 是最常⽤的切点表达式, ⽤来匹配⽅法, 语法为:
execution(<访问修饰符> <返回类型> <包名.类名.⽅法(⽅法参数)> <异常>)
其中: 访问修饰符和异常可以省略。
切点表达式⽀持通配符表达:
-
*:匹配任意字符,只匹配⼀个元素(返回类型, 包, 类名, ⽅法或者⽅法参数)
a. 包名使⽤ * 表⽰任意包(⼀层包使⽤⼀个*) b. 类名使⽤ * 表⽰任意类 c. 返回值使⽤ * 表⽰任意返回值类型 d. ⽅法名使⽤ * 表⽰任意⽅法 e. 参数使⽤ * 表⽰⼀个任意类型的参数
-
两个. : 匹配多个连续的任意符号, 可以通配任意层级的包, 或任意类型, 任意个数的参数
a. 使⽤ .. 配置包名,标识此包以及此包下的所有⼦包 b. 可以使⽤ .. 配置参数,任意个任意类型的参数
切点表达式示例:
- TestController 下的 public修饰, 返回类型为String ⽅法名为t1, ⽆参⽅法:
execution(public String com.example.demo.controller.TestController.t1())
- 省略访问修饰符
execution(String com.example.demo.controller.TestController.t1())
- 匹配所有返回类型
execution(* com.example.demo.controller.TestController.t1())
- 匹配TestController 下的所有⽆参⽅法
execution(* com.example.demo.controller.TestController.*())
- 匹配TestController 下的所有⽅法
execution(* com.example.demo.controller.TestController.*(…))
- 匹配controller包下所有的类的所有⽅法
execution(* com.example.demo.controller..(…))
- 匹配所有包下⾯的TestController
execution(* com…TestController.*(…))
- 匹配com.example.demo包下, ⼦孙包下的所有类的所有⽅法
execution(* com.example.demo…*(…))
@Slf4j
@Component
@Aspect
public class AspectDemo {
@Before("execution(* com.xiaoyu.aop.controller.*.*(..))")
public void doBefore() {
log.info("执行AspectDemo.before方法...");
}
}
3.2. 基于自定义类和实现AOP(自定义注解和@annotation表达式)
execution表达式更适⽤有规则的, 如果我们要匹配多个⽆规则的⽅法呢, ⽐如:我们想实现一个计录方法执行时间的功能,只作用在TestController中的t1()和UserController中的u1()这两个⽅法上。这个时候我们使⽤execution这种切点表达式来描述就不是很⽅便了。
我们可以借助自定义注解的方式以及另⼀种切点表达式 **@annotation **来描述这⼀类的切点。
实现步骤:
- 定义一个注解
- 实现该注解需要完成的功能(使⽤ @annotation 表达式来描述切点)
- 使用该注解(在连接点的方法上添加自定义注解)
自定义注解@TimeRecord:
创建一个注解类
/**
* 记录方法的执行时间
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeRecord {
}
实现该注解的功能:
使用 @annotation 切点表达式定义切点,切面类代码如下:
@Aspect
@Component
@Slf4j
public class AspectDemo5 {
@Around("@annotation(com.xiaoyu.aop.aspect.TimeRecord)")
public Object timeRecord(ProceedingJoinPoint joinPoint) {
long start = System.currentTimeMillis();// 目标方法执行前 记录开始时间
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable e) {
log.info(joinPoint.toString()+"发生异常:e",e);
}
log.info(joinPoint.toString()+"cost time:"+(System.currentTimeMillis() - start));
return result;
}
}
在连接点的方法上添加自定义注解:
在TestController中的t1()和UserController中的u1()这两个⽅法上添加⾃定义注解 @TimeRecord , 其他⽅法不添加。
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/t1")
@TimeRecord
public String t1() {
log.info("执行t1方法");
return "t1";
}
@RequestMapping("/t2")
public String t2() {
log.info("执行t2方法");
return "t2";
}
}
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/u1")
public String u1() {
log.info("执行u1方法");
return "u1";
}
@RequestMapping("/u2")
@TimeRecord
public String u2() {
log.info("执行u2方法");
return "u2";
}
}
运行程序,测试接口:http://127.0.0.1:8080/test/t1
测试接口:http://127.0.0.1:8080/test/t2
切面通知未执行
测试接口:http://127.0.0.1:8080/user/u1
切面通知未执行
测试接口:http://127.0.0.1:8080/user/u2
3.3. 基于Spring API 实现AOP(通过xml配置的方式)
3.4. 基于代理实现AOP
本质上,上面三种底层都是基于代理实现AOP的。
4. 使用Spring AOP的优点
使用Spring AOP的主要原因是它可以帮助我们更好地管理各种横切关注点,例如日志记录、事务管理、安全性检查等。
- 模块化:Spring AOP将横切关注点从主业务逻辑代码中分离出来,以模块化的方式实现对这些关注点的管理和重用。这样,我们可以更容易地维护代码,并且可以将同一个关注点的逻辑应用到多个方法或类中。
- 非侵入式:使用Spring AOP时,我们不需要修改原始业务逻辑代码,只需要在切点和增强中定义我们所需要的逻辑即可。这样,我们可以保持原始代码的简洁性和可读性。
- 可重用性:我们可以将同一个切面应用于多个目标对象进行横切处理。这样,我们可以提高代码的重用性,并且可以更加方便地维护和更新切面逻辑。
- 松耦合:AOP可以减少各个业务模块之间的耦合度,这是因为我们可以将某些通用的逻辑作为切面来实现,而不是直接在各个业务模块中实现。这样可以使得各个业务模块之间更加独立,从而提高代码的可维护性。
总之,使用Spring AOP可以帮助我们更好地管理和重用横切关注点逻辑,使得代码更易于维护和理解,并且可以提高代码的可重用性和灵活性。
5. Spring AOP实现原理
Spring 是如何实现AOP的?
AOP 实现技术主要分为两大类:静态代理和动态代理。Spring 是基于动态代理来实现AOP的,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring AOP 主要基于两种方式实现的: JDK 及 CGLIB 的方式:
- JDK 动态代理:对于实现了接口的目标类,Spring AOP 默认使用 JDK 的 java.lang.reflect.Proxy 类来创建代理对象。代理对象会在运行时实现代理接口,并覆盖其中的方法,在方法调用前后执行切面逻辑(即通知,advice)。
- CGLIB 动态代理:对于未实现接口的类,Spring AOP 会选择使用 CGLIB 库来生成代理对象。CGLIB 通过字节码技术创建目标类的子类,在子类中重写目标方法并在方法调用前后插入切面逻辑。
执行流程:当客户端通过代理对象调用目标方法时,代理对象会拦截这个调用,根据切面配置找到对应的通知,并按照通知类型的不同执行相应的增强逻辑。例如,如果是环绕通知,它会完全控制原始方法的调用过程,可以在调用前后插入自定义逻辑,甚至决定是否执行原方法。
通过上述方式,Spring AOP 巧妙地实现了对目标对象方法的拦截和增强,从而实现了面向切面编程的功能。
源码解析:
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
@Nullable Object[] specificInterceptors, TargetSource targetSource) {
if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory)
this.beanFactory, beanName, beanClass);
}
//创建代理⼯⼚
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
/**
* 检查proxyTargetClass属性值,spring默认为false
* proxyTargetClass 检查接⼝是否对类代理, ⽽不是对接⼝代理
* 如果代理对象为类, 设置为true, 使⽤cglib代理
*/
if (!proxyFactory.isProxyTargetClass()) {
//是否有设置cglib代理
if (shouldProxyTargetClass(beanClass, beanName)) {
//设置proxyTargetClass为true,使⽤cglib代理
proxyFactory.setProxyTargetClass(true);
} else {
/**
* 如果beanClass实现了接⼝,且接⼝⾄少有⼀个⾃定义⽅法,则使⽤JDK代理
* 否则CGLIB代理(设置ProxyTargetClass为true )
* 即使我们配置了proxyTargetClass=false, 经过这⾥的⼀些判断还是可能会将其设为true
*/
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);
proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}
// Use original ClassLoader if bean class not locally loaded in overriding
class loader
ClassLoader classLoader = getProxyClassLoader();
if (classLoader instanceof SmartClassLoader && classLoader !=
beanClass.getClassLoader()) {
classLoader = ((SmartClassLoader) classLoader).getOriginalClassLoader();
}
//从代理⼯⼚中获取代理
return proxyFactory.getProxy(classLoader);
}
代理工厂有⼀个重要的属性: proxyTargetClass, 默认值为false。也可以通过程序设置:
proxyTargetClass | ⽬标对象 | 代理⽅式 |
---|---|---|
false | 实现了接口 | JDK代理 |
false | 未实现接口(只实现了类) | CGLIB代理 |
true | 实现了接口 | CGLIB代理 |
true | 未实现接口(只实现了类) | CGLIB代理 |
5.1. Spring AOP 使用了哪种代理?
- Spring Framework
默认使用JDK动态代理,proxyTargetClass默认为false。如果代理的是接口,且接口至少有⼀个自定义方法,使用JDK动态代理;否则,使用CGLIB代理。如果代理的是没有实现接口的类,使用CGLIB动态代理。
- Spring Boot
Spring Boot 2.X 之前的版本,与Spring Framework 保持一致,默认使用JDK动态代理。
Spring Boot 2.X 之后的版本,默认配置使用CGLIB动态代理,proxyTargetClass默认为true。代理的类无论是否实现了接口,都使用CGLIB动态代理,如果需要使用JDK动态代理,可以通过配置项 spring.aop.proxy-target-class=false 来进⾏修改,设置默认为JDK代理。
共同点:底层实现都是JDK和CGLIB。