什么是 AOP
AOP 是指通过预编译方式和运行期动态代理的方式,在不修改源代码的情况下对程序进行功能增强的一种技术。AOP 不是面向对象编程(OOP)的替代品,而是 OOP 的补充和扩展。它是一个新的维度,用来表达横切问题,并提供一个声明式的处理方案。AOP 是 Spring 框架中的一个重要特性。
AOP 的使用场景
AOP 的使用场景一般是在某些纵向逻辑和多个相对独立的横向逻辑中,将横向逻辑进行抽象和封装,使得横向逻辑不再与纵向逻辑混杂在一起,使得应用程序更加易于维护和扩展。在实际开发中,AOP 的使用场景比较广泛,例如:
- 日志记录:在应用程序中,可以通过 AOP 对方法调用进行拦截,在方法调用前后记录日志信息。
- 安全处理:通过 AOP 实现安全方案,例如在应用程序中对某些敏感方法添加权限验证。
- 性能监控:对应用程序进行性能监控,实现性能分析和调优。
- 事物管理:通过 AOP 对事物进行管理,例如实现事物的回滚和提交。
- 缓存管理:对应用程序进行缓存管理,例如在读写操作中进行缓存。
AOP 的核心概念
在学习 AOP 的过程中,有一些核心概念是相当重要的。
- 连接点(JointPoint)
连接点是程序中可能被拦截的方法。在 AOP 中,连接点是指所有被拦截到的方法。连接点包含两个信息:一个是方法的位置信息,另一个是方法的名称。
- 切点(Pointcut)
切点是一组连接点的集合,是要被拦截的连接点。在 Spring AOP 中,切点采用 AspectJ 的切点表达式进行描述,格式如 @Pointcut("execution(public * com.example.demo.controller.*.*(..))")
。
- 通知(Advice)
通知是指拦截到连接点后要执行的代码,包括 @Before
、@AfterReturning
、@AfterThrowing
、@After
和 @Around
五种类型。
- 切面(Aspect)
切面是一个包含通知和切点的对象,主要用来维护切点和通知之间的关系。
- 织入(Weaving)
织入是将切面应用到目标对象来创建新的代理对象的过程。在 Spring AOP 中,织入可以在编译时、类加载时和运行时进行。
AOP 的实现方式
在 SpringBoot 中,AOP 的实现方式主要有两种:Java 代理(JDK Proxy)和字节码增强(CGLIB)。
- Java 代理(JDK Proxy)
Java 代理是一种基于接口的代理,通过实现 Java 动态代理接口 InvocationHandler
来实现对代理类方法的调用。Java 代理只能代理实现了接口的类,在运行时通过生成代理类的方式来实现。Java 代理的优点是操作简单,劣势是只能代理接口。
- 字节码增强(CGLIB)
字节码增强是一种基于继承的代理,通过生成代理类来完成对目标对象方法的调用。CGLIB 代理不需要实现接口,对目标对象进行继承并重写其中的方法,从而实现对方法的调用拦截。字节码增强的优点是能够代理非接口的类,劣势是需要引入 CGLIB 依赖包。
SpringBoot AOP 的例子
我们创建一个日志切面来记录调用方法开始时间、结束时间、持续时间等(方法名、参数、返回值...)。
pom.xml引入以下依赖包
xml
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>${spring-boot.version}</version>
</dependency>
</dependencies>
定义SysLog日志注解
java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SysLog {
String name() default "";
}
定义切面 LogAspect
java
@Aspect
public class LogAspect {
// 定义需要被拦截的切点
@Pointcut("execution(public * com.example.demo.controller.*.*(..))")
public void pointcut() {
}
// 在方法执行时进行通知
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SysLog sysLog = method.getAnnotation(SysLog.class);
if (sysLog != null) {
System.out.println("日志名称:" + sysLog.name());
}
System.out.println("开始时间:" + LocalDateTime.now());
System.out.println("执行方法 " + joinPoint.getSignature().getName() + " 前置环绕通知");
Object o = joinPoint.proceed();
System.out.println("结束时间:" + LocalDateTime.now());
System.out.println("执行方法 " + joinPoint.getSignature().getName() + " 后置环绕通知");
return o;
}
// 在方法执行之前进行通知
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
System.out.println("执行方法 " + joinPoint.getSignature().getName() + " 前置通知");
}
// 在方法执行之后进行通知
@After("pointcut()")
public void after(JoinPoint joinPoint) {
System.out.println("执行方法 " + joinPoint.getSignature().getName() + " 后置通知");
}
// 在方法执行之后返回结果时进行通知
@AfterReturning(returning = "result", pointcut = "pointcut()")
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("执行方法 " + joinPoint.getSignature().getName() + " 返回通知,返回值:" + result);
}
// 在方法抛出异常时进行通知
@AfterThrowing(throwing = "ex", pointcut = "pointcut()")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
System.err.println("执行方法 " + joinPoint.getSignature().getName() + " 异常通知,异常:" + ex.getMessage());
}
}
整个通知流程如下:
注意:如下必须注入LogAspect Bean。
java
@Configuration
public class MyConfiguration {
@Bean
public LogAspect logAspect() {
return new LogAspect();
}
}
测试,在Controller方法上添加@SysLog(name = "这是一个日志名称")注解。
java
@RestController()
@RequestMapping("user")
public class UserController {
@SysLog(name = "这是一个日志名称")
@GetMapping("test")
public String test() {
return "hello world";
}
}
测试:访问 http://localhost:8080/user/test,打印如下结果:
日志名称:这是一个日志名称
开始时间:2023-07-17T16:33:11.935
执行方法 test 前置环绕通知
执行方法 test 前置通知
执行方法 test 返回通知,返回值:hello world
执行方法 test 后置通知
结束时间:2023-07-17T16:33:11.940
执行方法 test 后置环绕通知
总结
以上就是 SpringBoot AOP 的基本用法,通过使用 SpringBoot AOP,我们可以在不修改源代码的情况下对程序进行功能增强,实现对方法的拦截、日志记录、权限验证、性能监控等功能。