Spring对于AOP的实现包括以下3种方式:
实际开发中,都是Spring+Aspectj来实现AOP,目前主流上通过注解方式。我们讲解基于注解方式 ,XML自行查阅相关文档。
那么它们实现AOP底层啥基于啥实现的呢?这里暂时给个概述,以后有机会学习底层原理在详细讲解。
Spring AOP的实现主要基于动态代理技术和字节码增强技术。
Spring AOP中使用了JDK动态代理和CGLIB动态代理两种方式来实现AOP的功能。JDK动态代理是基于接口的代理,只能为接口创建代理对象;而CGLIB动态代理则是通过继承的方式来实现代理,可以为任意类创建代理对象。
对于使用JDK动态代理的目标类,在运行时会生成一个实现了目标类接口的代理类,代理类中会调用一个InvocationHandler接口的invoke()方法,在该方法中进行拦截和增强操作。
对于无法使用JDK动态代理的目标类,Spring AOP则会使用CGLIB动态代理来创建代理对象。CGLIB动态代理是通过继承目标类来实现代理的,代理类会重写目标类的方法并在其中加入拦截和增强代码。
除了动态代理技术,Spring AOP还可以通过字节码增强技术来实现AOP的功能。字节码增强技术是通过修改类的字节码来实现对目标类的增强,可以在编译期或运行时对类进行修改。
在Spring AOP中,字节码增强技术主要是通过AspectJ来实现的。AspectJ是一个基于Java语言的切面编程框架,可以在编译期或运行时对Java类进行字节码增强。
通过AspectJ的注解或XML配置,可以指定哪些类和方法需要被拦截和增强,然后通过AspectJ提供的编译器或运行时织入工具,在编译期或运行时对目标类进行字节码增强。
即Spring 自己实现的AOP底层基于我们之前讲解的动态代理;AspectJ基于字节码增强来实现。
构建一个新练习项目,pom.xml引入依赖2-1如下所示:
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>6.0.6version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aspectsartifactId>
<version>6.0.6version>
dependency>
<dependency>
<groupId>org.junit.jupitergroupId>
<artifactId>junit-jupiter-apiartifactId>
<version>5.9.0version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-coreartifactId>
<version>2.19.0version>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-slf4j2-implartifactId>
<version>2.19.0version>
dependency>
dependencies>
新建简单的目标类OrderService,代码如下2-1所示:
package com.gaogzhen.spring6.aop.service;
import org.springframework.stereotype.Service;
/**
* 订单服务
* 目标类
* @author gaogzhen
*/
@Service
public class OrderService {
public void generate() {
System.out.println("系统正在生成订单。。。");
// if (true) {
// throw new RuntimeException("运行时异常");
// }
}
}
新建一个配置类AopConfig,用于指定包扫描和开启AspectJ的自动代理,代码如下2-2所示:
package com.gaogzhen.spring6.aop.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* 配置aop
* @author gaogzhen
*/
@Configuration
@ComponentScan({"com.gaogzhen.spring6.aop.service"})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopConfig {
}
新建切面类LogAspect,里面包括我们常用的通知类型,代码如下2-3所示:
package com.gaogzhen.spring6.aop.service;
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;
/**
* 日志切面
* 切面=通知+切点
* 通知:方法增强
*
* @author gaogzhen
*/
@Component
@Aspect
@Order(3)
public class LogAspect {
@Pointcut("execution(* com.gaogzhen.spring6.aop.service..*(..))")
public void common() {}
/**
* 前置通知
*/
@Before("common()")
public void beforeAdvice(JoinPoint joinPoint) {
System.out.println("前置通知");
Signature signature = joinPoint.getSignature();
System.out.println("目标方法名:" + signature.getName());
}
/**
* 后置通知
*/
@AfterReturning("common()")
public void afterReturningAdvice() {
System.out.println("后置通知");
}
/**
* 环绕通知(环绕在前置通知之前,在后置通知之后)
*/
@Around("common()")
public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// 前处理
System.out.println("前环绕");
// 目标方法
joinPoint.proceed();
// 后处理
System.out.println("后环绕");
}
/**
* 异常通知
* 发生异常之后通知
*/
@AfterThrowing("common()")
public void afterThrowingAdvice() {
System.out.println("异常通知");
}
/**
* 最终通知
* fianlly 语句快中的通知
*/
@After("common()")
public void afterAdvice() {
System.out.println("最终通知");
}
}
通用切点只是一个标记,方法名随意,不需要方法体。
几种通知参数,可以说切点表达式,也可以说通用切点;如果想要在其他切面中使用通用切点,需要加上类的全路径。
通知注解标记的方法,可以接收连接点参数,环绕通知为ProceedingJoinpoint,其他通知为Joinpoint;连接点用于获取目标方法相关信息。
测试类代码2-4如下所示:
@Test
public void testAnnotation() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AopConfig.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
orderService.generate();
}
OrderService在注释异常时,我们看下通知的执行顺序如下所示:
前环绕
前置通知
目标方法名:generate
系统正在生成订单。。。
后置通知
最终通知
后环绕
程序正常的通知执行顺序:前环绕通知=>前置通知=>目标方法=>后置通知=>最终通知=>后环绕通知;前环绕通知即环绕通知中目标方法之前的部分,后环绕通知即环绕通知中目标方法执行之和的部分。
OrderService添加异常后,通知执行顺序如下:
前环绕
前置通知
目标方法名:generate
系统正在生成订单。。。
异常通知
最终通知
Tests run: 2, Failures: 2, Errors: 0, Skipped: 0, Time elapsed: 0.758 sec <<< FAILURE!
com.gaogzhen.spring6.test.SpringAOPTest.testBefore() Time elapsed: 0.728 sec <<< FAILURE!
java.lang.RuntimeException: 运行时异常
程序产生异常后,执行异常通知,因为最终通知在finally代码块中,会执行。
不同切面的执行顺序指定,下面我们在添加一个SecurityAspect。通过在类上添加@Order(数值)注解,来指定执行顺序。数值越小,优先级越高,即越早执行。
测试我们指定LogAspect @Order(3),SecurityAspect @Order(1),看下执行顺序,异常通知不在测试,测试结果如下:
前环绕:安全切面----
前置通知:安全切面----
前环绕
前置通知
目标方法名:generate
系统正在生成订单。。。
后置通知
最终通知
后环绕
后置通知:安全切面----
最终通知:安全切面----
后环绕:安全切面----
日常开发中,事务处理和安全日志记录是我们经常要用到的交叉业务。
事务的四个处理过程:
应用AOP解决方案:
通常我们安全记录用于记录信息如下:
应用AOP传统解决方案:
一般情况下我们应用环绕通知比较多。
上面解决方法为同步方法,记录信息,存储都需要消耗一定时间,在实时性要求高场景下不适用。这时可以通过消息队列来异步解耦,这时候耗时需要拆分出去;异步获取单一结果可以采用Mono异步背压方式或者消息队列解决。
如果小伙伴什么问题或者指教,欢迎交流。
❓QQ:806797785
⭐️源代码仓库地址:https://gitee.com/gaogzhen/spring6-study
参考:
[1]Spring框架视频教程[CP/OL].P106-117.
[2]ChatGPT