1. Java中的切面编程(AOP)概述
切面编程(Aspect-Oriented Programming,AOP)是一种编程范式,旨在将那些贯穿于多个模块的横切关注点(如日志记录、安全检查、事务管理)与核心业务逻辑分离开来。通过AOP,我们可以提高代码的模块化程度,减少代码重复,并使代码更加可维护。
概念 | 定义 |
---|---|
切面(Aspect) | 切面是横切关注点的模块化实现,它包含多个通知(Advice)和切入点(Pointcut)。 |
通知(Advice) | 通知是在特定连接点(Join Point)上执行的操作。它可以在方法执行前、执行后、抛出异常后或包围整个方法的执行过程。 |
切入点(Pointcut) | 切入点是通过(execution)表达式定义的,用于指定在哪些连接点上应用通知。 |
连接点(Join Point) | 程序执行的具体点,如方法调用或异常抛出,切入点通常匹配这些连接点。 |
代理(Proxy) | 代理是AOP框架生成的对象,负责在目标对象方法调用前后插入通知逻辑。 |
类比:
切面(Aspect):影院的自动灯光系统控制电影放映时的灯光。
通知(Advice):当电影开始时,灯光变暗;电影结束时,灯光变亮。
切入点(Pointcut):灯光系统只在“电影开始”和“电影结束”这两个时刻进行操作。
连接点(Join Point):实际发生的时刻,比如电影开场的那一瞬间。
代理(Proxy):灯光系统自动帮你完成这些操作,你不需要亲自去调节灯光。
2. execution 表达式(定义切入点)
execution
表达式用于定义切入点,它指定了在什么方法上应用通知。表达式的结构如下:
execution(修饰符模式? 返回类型模式 声明类型模式? 方法名模式(参数模式) 异常模式?)
部分 | 说明 |
---|---|
修饰符模式 | 可选。方法的修饰符,如public、protected。 |
返回类型模式 | 方法的返回类型,可以是具体类型或* (表示任意类型)。 |
声明类型模式 | 可选。方法所在的类或接口。 |
方法名模式 | 方法名称,可以是具体名称或* (表示任意方法)。 |
参数模式 | 方法的参数列表,可以具体匹配参数类型,或使用.. 表示任意数量和类型的参数。 |
异常模式 | 可选。方法可能抛出的异常。 |
? | 表示该部分是可选的,而不是必须要包含的。 |
2.1 匹配特定方法
execution(public void org.example.service.UserService.registerUser(String))
解释:这个表达式匹配UserService
类中名为registerUser
的方法。该方法必须是public
修饰符,返回类型为void
,且接受一个String
类型的参数。
2.2 匹配包中的所有方法
execution(* org.example.service..*.*(..))
解释:这个表达式匹配com.example.service
包及其所有子包中的所有类的所有方法。..
表示递归地匹配包及其子包,*.*(..)
表示所有类中的所有方法,不论返回类型和参数类型。
2.3 匹配特定类中的所有方法
execution(* org.example.service.UserService.*(..))
解释:这个表达式匹配UserService
类中的所有方法,不论方法的返回类型、方法名或参数列表。具体来说,*
表示任意返回类型,com.example.service.UserService.*
表示UserService
类中的所有方法,(..)
表示任意数量和类型的参数。
2.4 匹配带有特定参数的方法
execution(* org.example.service.UserService.*(String, ..))
解释:这个表达式匹配UserService
类中所有以String
作为第一个参数的方法。*
表示任意返回类型,UserService.*
表示UserService
类中的所有方法,(String, ..)
表示方法的第一个参数是String
类型,后面可以有任意数量和类型的其他参数。
2.5 匹配带有特定返回值的方法
execution(String org.example.service.*.*(..))
解释:这个表达式匹配com.example.service
包下所有类中返回类型为String
的所有方法。String
指定了返回类型,*.*(..)
表示匹配所有类中的所有方法,不论方法名和参数类型。
3. Advice(通知)
Advice
是切面中定义的实际操作。Spring AOP提供了五种类型的Advice
,用于在程序执行的不同阶段插入代码。
3.1 前置通知(Before Advice)
@Before("execution(* org.example.service.UserService.*(..))")
public void logBefore() {
System.out.println("方法执行前: 准备注册用户");
}
解释:在目标方法执行前执行,用于日志记录、权限检查等。
3.2 后置通知(After Advice)
@After("execution(* org.example.service.UserService.*(..))")
public void logAfter() {
System.out.println("方法执行后: 用户注册过程完成");
}
解释:在目标方法执行后执行,可以用于释放资源、记录日志等操作。
3.3 返回通知(After Returning Advice)
语法:
@AfterReturning(pointcut = "execution(表达式)", returning = "变量名")
public void 方法名(变量类型 变量名) {
// 使用返回值的逻辑
}
案例:
@AfterReturning(pointcut = "execution(* org.example.service.UserService.*(..))", returning = "result")
public void logAfterReturning(Object result) {
System.out.println("方法返回: " + result);
}
解释:在目标方法成功返回结果后执行,可以用于处理返回值、记录返回值日志等操作。
3.4 异常通知(After Throwing Advice)
语法:
@AfterThrowing(pointcut = "execution(表达式)", throwing = "异常变量名")
public void 方法名(异常类型 异常变量名) {
// 使用异常对象的逻辑
}
案例:
@AfterThrowing(pointcut = "execution(* org.example.service.UserService.*(..))", throwing = "error")
public void logAfterThrowing(Throwable error) {
System.out.println("方法执行时发生异常: " + error.getMessage());
}
解释:在目标方法抛出异常后执行,可以用于记录异常信息、执行补偿逻辑等操作。
3.5 环绕通知(Around Advice)
@Around("execution(* org.example.service.UserService.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知: 方法执行前");
Object result;
try {
result = joinPoint.proceed(); // 执行目标方法
} catch (Throwable throwable) {
System.out.println("环绕通知: 方法执行时发生异常: " + throwable.getMessage());
throw throwable;
}
System.out.println("环绕通知: 方法执行后");
return result;
}
解释:包裹目标方法的执行,在方法执行前后都插入逻辑,甚至可以完全控制方法执行。
目标方法:指的是在你的业务代码中实际被AOP切面拦截的方法。**joinPoint.proceed()
是环绕通知中的核心,它决定了目标方法的执行。**如果你不调用 proceed()
,目标方法将不会被执行。在调用 proceed()
之前和之后,你可以插入任何自定义的逻辑,这使得环绕通知非常灵活。
4. 简单代码案例
4.1 项目结构
4.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>aop</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>3.3.2</version>
</dependency>
</dependencies>
</project>
4.2 UserService.java (业务逻辑类)
package org.example.service;
import org.springframework.stereotype.Service;
@Service
public class UserService {
public String registerUser(String username) {
if (username == null) {
throw new IllegalArgumentException("用户名不能为空");
}
System.out.println("正在注册用户: " + username);
return "用户 " + username + " 注册成功";
}
}
4.3 LoggingAspect.java (切面类)
package org.example.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
/**
* 前置通知 (Before Advice)
* 这个方法会在目标方法 `registerUser(String)` 执行之前执行。
* `execution` 表达式指定了要拦截的目标方法。
*/
@Before("execution(public void org.example.service.UserService.registerUser(String))")
public void logBefore() {
System.out.println("方法执行前(Before): 准备注册用户");
}
/**
* 后置通知 (After Advice)
* 这个方法会在目标方法执行之后执行,无论方法是否成功完成或抛出异常。
* 这里的 `execution` 表达式匹配 `org.example.service` 包及其子包中的所有方法。
*/
@After("execution(* org.example.service..*.*(..))")
public void logAfter() {
System.out.println("方法执行后(After): 用户注册过程完成");
}
/**
* 返回通知 (After Returning Advice)
* 这个方法会在目标方法成功返回结果之后执行。
* `returning` 属性指定了一个参数,用来接收目标方法的返回值。
* `execution` 表达式匹配 `UserService` 类中的所有方法。
*
* @param result 目标方法的返回值
*/
@AfterReturning(pointcut = "execution(* org.example.service.UserService.*(..))", returning = "result")
public void logAfterReturning(Object result) {
System.out.println("方法返回(AfterReturning): " + result);
}
/**
* 异常通知 (After Throwing Advice)
* 这个方法会在目标方法抛出异常时执行。
* `throwing` 属性指定了一个参数,用来接收目标方法抛出的异常。
* `execution` 表达式匹配 `UserService` 类中接受一个 `String` 参数的所有方法。
*
* @param error 目标方法抛出的异常
*/
@AfterThrowing(pointcut = "execution(* org.example.service.UserService.*(String, ..))", throwing = "error")
public void logAfterThrowing(Throwable error) {
System.out.println("方法执行时发生异常(AfterThrowing): " + error.getMessage());
}
/**
* 环绕通知 (Around Advice)
* 这个方法会包裹住目标方法的执行,即在目标方法执行前后都可以插入逻辑。
* `execution` 表达式匹配 `org.example.service` 包中的所有返回类型为 `String` 的方法。
*
* @param joinPoint 提供对目标方法的访问控制
* @return 目标方法的返回值
* @throws Throwable 如果目标方法抛出异常,则重新抛出该异常
*/
@Around("execution(String org.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知(Around): 方法执行前");
Object result;
try {
// 执行目标方法
result = joinPoint.proceed();
} catch (Throwable throwable) {
System.out.println("环绕通知: 方法执行时发生异常: " + throwable.getMessage());
throw throwable; // 重新抛出异常
}
System.out.println("环绕通知(Around): 方法执行后");
return result; // 返回目标方法的返回值
}
}
4.3 AopApplication.java(测试类)
package org.example;
import org.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AopApplication implements CommandLineRunner {
// 自动注入 UserService 实例
@Autowired
private UserService userService;
// 主方法,Spring Boot 应用的入口
public static void main(String[] args) {
SpringApplication.run(AopApplication.class, args);
}
// 在应用启动后执行的方法
@Override
public void run(String... args) {
// 测试用例1:注册用户 "张三"
try {
System.out.println(userService.registerUser("张三"));
} catch (Exception e) {
e.printStackTrace(); // 打印异常堆栈信息
}
System.out.println("--------------------------------------------");
// 测试用例2:尝试注册 null 用户,触发异常
try {
userService.registerUser(null);
} catch (Exception e) {
e.printStackTrace(); // 打印异常堆栈信息
}
}
}
4.4 测试
测试1:
前提准备:把这部分注释掉。
@Around("execution(String org.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知(Around): 方法执行前");
Object result;
try {
// 执行目标方法
result = joinPoint.proceed();
} catch (Throwable throwable) {
System.out.println("环绕通知: 方法执行时发生异常: " + throwable.getMessage());
throw throwable; // 重新抛出异常
}
System.out.println("环绕通知(Around): 方法执行后");
return result; // 返回目标方法的返回值
}
运行结果:
测试2:
前提准备:把这部分注释掉。
@Before("execution(public void org.example.service.UserService.registerUser(String))")
public void logBefore() {
System.out.println("方法执行前(Before): 准备注册用户");
}
@After("execution(* org.example.service..*.*(..))")
public void logAfter() {
System.out.println("方法执行后(After): 用户注册过程完成");
}
@AfterReturning(pointcut = "execution(* org.example.service.UserService.*(..))", returning = "result")
public void logAfterReturning(Object result) {
System.out.println("方法返回(AfterReturning): " + result);
}
@AfterThrowing(pointcut = "execution(* org.example.service.UserService.*(String, ..))", throwing = "error")
public void logAfterThrowing(Throwable error) {
System.out.println("方法执行时发生异常(AfterThrowing): " + error.getMessage());
}
运行结果:
5. 总结
环绕通知 VS 其他四种通知
通知类型 | 适用场景 | 不适用场景 |
---|---|---|
环绕通知(Around) | 需要在方法执行前后都执行逻辑 ,例如记录日志或性能监控。需要决定是否执行目标方法。 需要统一捕获并处理异常。 需要修改方法的返回值。 | 只需在方法前或方法后做简单的操作时,使用环绕通知会显得过于复杂。 |
前置通知(Before) | 仅在方法执行前执行逻辑,如参数验证、日志记录、权限检查等。 | 需要在方法执行后、处理返回值或捕获异常的场景。 |
后置通知(After) | 仅在方法执行后执行逻辑,如资源清理、记录状态等。 | 需要处理返回值或捕获异常,或在方法执行前执行逻辑的场景。 |
返回通知(After Returning) | 仅在方法成功返回结果后执行逻辑,如记录返回值或对返回值进行处理。 | 需要捕获异常,或在方法执行前/后执行逻辑的场景。 |
异常通知(After Throwing) | 仅在方法抛出异常时执行逻辑,如记录异常信息、发送报警或执行补偿逻辑。 | 需要在方法执行前、执行后或成功返回时执行逻辑的场景。 |
解释:
- 环绕通知适用于对方法的执行有全面控制的需求,包括方法执行前、后,异常处理,以及返回值的修改。
- 前置通知适用于只需要在方法执行前执行逻辑的场景。
- 后置通知适用于只需要在方法执行后执行逻辑的场景。
- 返回通知适用于只关心方法成功返回后的处理逻辑。
- 异常通知适用于只处理方法抛出异常的情况。