Spring AOP
一、AOP术语
核心概念
名称 | 说明 |
---|---|
切面(Aspect) | 切面是通知和切点的结合。 切面是一组定义了横切关注点的类。在 Spring AOP 中,切面通常通过使用 @Aspect 注解的类来实现。 |
连接点(Joinpoint) | 连接点表示应用执行过程中能够插入切面的一个特定点,如方法的调用或异常的抛出。 在 Spring AOP 中,连接点总是方法的执行。 |
切入点(Pointcut) | 定义了可以插入增强处理的连接点。 Spring AOP 允许通过正则表达式或特定的 API 来定义切入点。 |
通知(Advice) | AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理。 |
织入(Weaving) | 织入是将切面应用到目标对象并创建新的代理对象的过程。Spring AOP 在运行时动态地织入切面。 |
代理(Proxy) | 代理是织入切面后生成的对象,它包装了目标对象,并在执行目标对象的方法前后应用通知。 |
目标对象(Target Object) | 目标对象是被代理的对象,即包含连接点的业务逻辑组件。 |
引入(Introduction) | 引入允许为现有的类添加新的方法、属性和接口,而不需要修改类本身。 |
Spring 通知分类
通知类型 | 说明 |
---|---|
before(前置通知) | 在目标方法执行之前执行的通知。不管目标方法是否执行,前置通知总是会被执行。 |
after(后置通知) | 在目标方法执行之后执行的通知,无论方法执行是否成功,都会执行后置通知。 |
after-returning(返回后通知) | 当目标方法成功执行并返回值后执行的通知。如果目标方法抛出异常,则不会执行返回通知。 |
after-throwing(抛出异常后通知) | 当目标方法抛出异常时执行的通知。如果目标方法正常执行并返回,则不会执行异常通知。 |
around(环绕通知) | 包围目标方法执行的通知,可以在目标方法执行前后添加逻辑,并且可以决定是否继续执行目标方法或替换返回值。 |
AOP 织入时期
织入时期 | 说明 | 适用场景 |
---|---|---|
编译时织入 | 在源代码编译成字节码时进行织入。 | 在编译阶段,AspectJ 的编译器会处理注解,将切面织入到 Java 字节码中。这种方式需要使用 AspectJ 的特定编译器。 |
加载时织入 | 在字节码加载到 JVM 时进行织入。 | 在类加载到 JVM 时,通过自定义的类加载器动态地修改字节码,将切面织入。这同样需要 AspectJ 的支持 |
运行时织入 | 在应用程序运行时进行织入,是 Spring AOP 的主要工作方式。 | 适用于 Spring 管理的 Bean。 Spring AOP 在运行时使用代理机制来织入切面。如果目标对象实现了接口,Spring 使用 JDK 的 Proxy 类创建代理;如果没有实现接口,Spring 使用 CGLIB 库来创建代理。 |
注意:Spring AOP 主要支持运行时织入,而编译时织入和加载时织入通常与 AspectJ 框架一起使用,它们提供了更广泛的 AOP 功能,但不是 Spring AOP 的一部分。
二、AOP 常见应用场景
- 日志记录
- 事务管理
- 权限验证
- 性能监测
...
三、AOP 实现方式
AOP有两种实现方式:静态代理和动态代理。
静态代理:代理类在编译阶段生成,在编译阶段将通知织入Java字节码中,也称编译时增强。AspectJ使用的是静态代理。
缺点:代理对象需要与目标对象实现一样的接口,并且实现接口的方法,会有冗余代码。同时,一旦接口增加方法,目标对象与代理对象都要维护。
动态代理:代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类。
四、Spring AOP动态代理
Spring AOP中的动态代理主要有两种方式:JDK动态代理和CGLIB动态代理。
-
JDK动态代理(生成的代理类实现了接口)。如果目标类实现了接口,Spring AOP会选择使用JDK动态代理目标类。代理类根据目标类实现的接口动态生成,不需要自己编写,生成的动态代理类和目标类都实现相同的接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。
缺点:目标类必须有实现的接口。如果某个类没有实现接口,那么这个类就不能用JDK动态代理。
-
CGLIB来动态代理(通过继承)。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library)可以在运行时动态生成类的字节码,动态创建目标类的子类对象,在子类对象中增强目标类。
CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
优点:目标类不需要实现特定的接口,更加灵活。
什么时候采用哪种动态代理?
如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
如果目标对象实现了接口,可以强制使用CGLIB实现AOP
如果目标对象没有实现了接口,必须采用CGLIB库
区别:
-
jdk动态代理使用jdk中的类Proxy来创建代理对象,它使用反射技术来实现,不需要导入其他依赖。cglib需要引入相关依赖:asm.jar,它使用字节码增强技术来实现。
-
当目标类实现了接口的时候Spring Aop默认使用jdk动态代理方式来增强方法,没有实现接口的时候使用cglib动态代理方式增强方法。
五、Spring AOP编程的实现方式
- XML配置文件
(1)基于Spring自带的AOP(xml配置文件)
(2)基于Aspectj实现切面(普通pojo类),使用spring aop进行配置(xml配置文件)
a. 首先,导入Aspectj相关依赖
b. 实现切面类,实现通知方法
c. 配置代理,配置切面,注入切面bean,定义切点、通知方法
具体了解请点击参考链接
- 基于注解的实现方式 (强烈推荐)
(1)基础步骤
a. 首先,导入Aspectj相关依赖
b. 实现Aspect切面,并交给Spring容器管理。使用@Aspect、@Component,使用@Order(1)可以控制执行顺序
c. 定义切点,使用@Pointcut。
d. 定义通知,使用@Before、@After、@AfterReturning、@AfterThrowing、@Around
e. 配置启用AspectJ自动代理
(2)实现例子
<!--aop依赖1:aspectjrt -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
<!--aop依赖2: aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
实现日志切面类
@Aspect
@Component
@Slf4j
public class LogAspect {
@Pointcut("execution(* com.example.controller..*(..))")
public void pointCut(){}
@Before("pointCut()")
public void before(JoinPoint joinPoint){
log.info("Before >>>");
}
@After("pointCut()")
public void after(JoinPoint joinPoint){
log.info("After >>>");
}
@AfterReturning("pointCut()")
public void afterReturning(JoinPoint joinPoint){
log.info("AfterReturning >>>");
}
@AfterThrowing("pointCut()")
public void afterThrowing(JoinPoint joinPoint){
log.info("AfterThrowing >>>");
}
//环绕通知
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("##########【环绕通知中的前置通知】##########");
Object returnVale = joinPoint.proceed();
log.info("##########【环绕通知中的后置通知】##########");
return returnVale;
}
}
注意:@Pointcut的切点表达式有很多种形式,示例是匹配特定包下的所有方法,其它的比如匹配特定注解 @Pointcut("@annotation(com.example.annotation.MyCustomAnnotation)")
创建配置类,启用AspectJ自动代理
package com.example.demo.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy
public class AppConfig {
}
注意:Spring项目配置启动Aspectj的自动代理的两种方式,一是在配置文件中添加配置<aop:aspectj-autoproxy/>,二是创建一个Java配置类来代替XML配置,使用@Configuration注解标记为配置类,并通过@ComponentScan注解来启用组件扫描,通过@EnableAspectJAutoProxy启用AspectJ自动代理。
无抛出异常时的输出:
2024-08-08 17:11:40.960 INFO 5144 --- [nio-8080-exec-4] com.example.common.aop.LogAspect : ##########【环绕通知中的前置通知】##########
2024-08-08 17:11:40.961 INFO 5144 --- [nio-8080-exec-4] com.example.common.aop.LogAspect : Before >>>
2024-08-08 17:11:40.961 INFO 5144 --- [nio-8080-exec-4] com.example.common.aop.LogAspect : AfterReturning >>>
2024-08-08 17:11:40.961 INFO 5144 --- [nio-8080-exec-4] com.example.common.aop.LogAspect : After >>>
2024-08-08 17:11:40.961 INFO 5144 --- [nio-8080-exec-4] com.example.common.aop.LogAspect : ##########【环绕通知中的后置通知】##########
有抛出异常时的输出:
2024-08-08 17:11:03.058 INFO 5144 --- [nio-8080-exec-1] com.example.common.aop.LogAspect : ##########【环绕通知中的前置通知】##########
2024-08-08 17:11:03.058 INFO 5144 --- [nio-8080-exec-1] com.example.common.aop.LogAspect : Before >>>
2024-08-08 17:11:03.090 INFO 5144 --- [nio-8080-exec-1] com.example.common.aop.LogAspect : AfterThrowing >>>
2024-08-08 17:11:03.090 INFO 5144 --- [nio-8080-exec-1] com.example.common.aop.LogAspect : After >>>
2024-08-08 17:11:03.104 ERROR 5144 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: Test the AfterThrowing Advice] with root cause
java.lang.RuntimeException: Test the AfterThrowing Advice
at com.example.controller.LogController.testAop(LogController.java:16)
...
问题:为什么spring boot项目中实现aop,没有任何地方显性显性的启动AspectJ代理?
原因:
此示例是基于Spring Boot的框架,如果,它会自动配置 AOP。Spring Boot 应用通常会引入了spring-boot-autoconfigure依赖,这是springboot项目自动装配的基础,此依赖的spring.factories文件中有一项org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,此类有一个注解@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true),意思是在配置文件中有spring.aop.auto的配置,默认为true,它的作用与@EnableAspectJAutoProxy是类似的。
六、参考链接