一、AOP基础认知
1.1 什么是AOP
AOP(Aspect Oriented Programming),即面向切面编程,是Spring框架的核心特性之一,与OOP(面向对象编程)相辅相成。OOP以“类和对象”为核心,关注业务逻辑的纵向封装;而AOP以“切面”为核心,关注业务逻辑的横向扩展,将多个模块共用的通用功能(如日志、事务、权限校验)抽取出来,独立实现,再动态植入到需要的业务方法中,实现“解耦通用逻辑与业务逻辑”。
简单来说,AOP的核心思想是“分离关注点”——将那些不影响核心业务、但多个业务都需要的通用功能(称为“横切逻辑”),从业务代码中剥离出来,单独维护,既减少代码冗余,又便于后续维护和扩展。
1.2 为什么要用AOP(核心优势)
在没有AOP的情况下,通用功能(如日志)需要在每个业务方法中重复编写,不仅代码冗余,而且后续修改时(如修改日志格式),需要修改所有相关业务方法,维护成本极高。AOP的出现,正是为了解决这个问题,核心优势如下:
-
解耦:将通用横切逻辑与核心业务逻辑分离,业务代码只关注核心功能,通用逻辑单独维护,降低代码耦合度。
-
减少代码冗余:通用逻辑只需编写一次,即可植入到多个业务方法中,避免重复编码。
-
便于维护和扩展:修改通用逻辑时,只需修改一处,所有植入该逻辑的业务方法都会生效,无需逐个修改。
-
不侵入业务代码:无需修改业务方法的代码,即可为其添加通用功能,符合“开闭原则”(对扩展开放,对修改关闭)。
1.3 AOP的应用场景(实战常用)
AOP在Java后端开发中应用广泛,常见场景如下:
-
日志记录:记录方法的调用时间、参数、返回值、异常信息(如接口请求日志、操作日志)。
-
事务管理:控制方法的事务提交、回滚(如Service层方法添加事务,无需手动编写事务代码)。
-
权限校验:在方法执行前,校验用户是否有操作权限(如后台接口的权限拦截)。
-
性能监控:统计方法的执行耗时,定位性能瓶颈。
-
异常处理:统一捕获方法执行过程中的异常,进行统一处理(如返回标准化错误提示)。
二、AOP核心原理(动态代理)
AOP的底层实现依赖动态代理,Spring AOP默认提供两种动态代理方式,根据目标类是否实现接口自动选择:
-
JDK动态代理:目标类实现接口时,Spring会使用JDK原生的动态代理,生成目标接口的代理对象,代理对象增强目标方法(植入横切逻辑)。
-
CGLIB动态代理:目标类未实现接口时,Spring会使用CGLIB(第三方类库),生成目标类的子类作为代理对象,通过重写目标方法实现增强(植入横切逻辑)。
补充说明:Spring Boot 2.x版本后,默认集成CGLIB,即使目标类实现接口,也可配置使用CGLIB动态代理;动态代理的核心是“不修改目标类代码,通过代理对象间接调用目标方法,在调用过程中植入横切逻辑”。
三、AOP核心组件
Spring AOP有5个核心组件,相互配合实现横切逻辑的植入,需牢记每个组件的作用,以及它们之间的关系:
3.1 切面(Aspect)
切面是AOP的核心,是“横切逻辑的载体”,本质是一个Java类,包含了横切逻辑(如日志记录方法)和切入点(指定哪些方法需要植入该横切逻辑)。
简单来说,切面 = 切入点 + 通知(横切逻辑),是“要做什么”(通知)和“对哪些方法做”(切入点)的结合。
3.2 通知(Advice)
通知是横切逻辑的具体实现,即“要植入的通用功能”,如日志记录的具体代码、权限校验的具体逻辑。
Spring AOP提供5种通知类型,覆盖方法执行的全生命周期,实战中常用前4种:
-
前置通知(@Before):在目标方法执行之前执行(如权限校验,先校验权限,再执行方法)。
-
后置通知(@After):在目标方法执行之后执行(无论方法是否抛出异常,都会执行,如关闭资源)。
-
返回通知(@AfterReturning):在目标方法正常执行完成后执行(方法无异常,如记录方法返回值)。
-
异常通知(@AfterThrowing):在目标方法抛出异常后执行(如捕获异常,记录异常信息)。
-
环绕通知(@Around):包裹目标方法,在目标方法执行前后都能执行,可控制目标方法的执行(如性能监控,统计方法执行耗时),功能最强大,也最复杂。
3.3 切入点(Pointcut)
切入点是用于指定“哪些方法需要植入通知(横切逻辑)”的规则,通过切入点表达式定义,Spring AOP支持多种切入点表达式,最常用的是“execution表达式”。
核心作用:精准定位需要增强的方法,避免不必要的方法被增强,提升性能。
3.4 连接点(JoinPoint)
连接点是方法执行过程中的某个时机,如方法执行前、执行后、抛出异常时,这些时机都是可以植入通知的“节点”。
注意:连接点是“所有可能被增强的时机”,切入点是“被选中的连接点”——只有符合切入点规则的连接点,才会植入通知。
3.5 目标对象(Target)
目标对象是被增强的原始对象,即包含核心业务逻辑的对象(如Service层的实现类),AOP通过动态代理生成代理对象,代理对象调用目标对象的方法,并植入横切逻辑。
组件关系总结
用一句话概括:切面(Aspect)通过切入点(Pointcut)定位到目标对象(Target)的连接点(JoinPoint),在对应时机执行通知(Advice),实现横切逻辑的植入。
四、AOP切入点表达式(重点,实战必备)
切入点表达式的核心是“定位需要增强的方法”,Spring AOP支持多种表达式,其中execution表达式最常用、最灵活,掌握它即可满足大部分实战场景。
4.1 execution表达式语法
语法格式(空格分隔,顺序不可乱):
execution(访问修饰符 返回值类型 包名.类名.方法名(参数列表) throws 异常类型)
说明:其中“访问修饰符”“throws 异常类型”可省略,其他部分根据需求灵活配置,支持通配符。
4.2 常用通配符
-
*:匹配任意一个字符(如匹配任意返回值类型、任意方法名、任意包名的一级目录)。
-
..:匹配任意多个字符(如匹配任意层级的包、任意个数和类型的参数)。
-
+:匹配某个类及其子类(如com.example.service.UserService+,匹配UserService及其子类)。
五、Spring AOP实战实现(Spring Boot版,最常用)
Spring Boot集成AOP非常简单,无需额外配置,只需导入依赖、编写切面类,即可实现横切逻辑的植入,以下以“日志记录”为例,演示完整实战流程。
5.1 步骤1:导入AOP依赖(Maven)
Spring Boot自带AOP依赖,导入spring-boot-starter-aop即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
5.2 步骤2:编写业务类(目标对象)
编写一个Service层业务类,作为被增强的目标对象(核心业务逻辑):
// 业务接口
public interface UserService {
// 新增用户
void addUser(String username);
// 查询用户
String getUserById(Integer id);
}
// 业务实现类(目标对象)
@Service
public class UserServiceImpl implements UserService {
@Override
public void addUser(String username) {
// 核心业务逻辑(模拟新增用户)
System.out.println("新增用户:" + username);
// 模拟异常(用于测试异常通知)
// int i = 1 / 0;
}
@Override
public String getUserById(Integer id) {
// 核心业务逻辑(模拟查询用户)
System.out.println("查询用户,用户ID:" + id);
return "用户" + id;
}
}
5.3 步骤3:编写切面类(核心,植入横切逻辑)
切面类需添加@Aspect注解(标识为切面)和@Component注解(交给Spring容器管理),然后定义切入点和通知:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
// 标识为切面类 + 交给Spring管理
@Aspect
@Component
public class LogAspect {
// 1. 定义切入点(指定哪些方法需要植入日志逻辑)
// 切入点表达式:匹配com.example.service包下所有类的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void logPointcut() {} // 切入点方法,无实际逻辑,仅用于承载切入点表达式
// 2. 前置通知:目标方法执行前执行,记录方法调用信息
@Before("logPointcut()")
public void beforeAdvice(JoinPoint joinPoint) {
// JoinPoint:连接点对象,可获取目标方法的信息(方法名、参数等)
String methodName = joinPoint.getSignature().getName(); // 获取方法名
Object[] args = joinPoint.getArgs(); // 获取方法参数
System.out.println("【前置通知】方法" + methodName + "开始执行,参数:" + Arrays.toString(args));
}
// 3. 返回通知:目标方法正常执行完成后执行,记录返回值
@AfterReturning(value = "logPointcut()", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("【返回通知】方法" + methodName + "执行完成,返回值:" + result);
}
// 4. 异常通知:目标方法抛出异常后执行,记录异常信息
@AfterThrowing(value = "logPointcut()", throwing = "e")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception e) {
String methodName = joinPoint.getSignature().getName();
System.out.println("【异常通知】方法" + methodName + "执行异常,异常信息:" + e.getMessage());
}
// 5. 后置通知:目标方法执行后执行(无论是否异常),记录方法结束
@After("logPointcut()")
public void afterAdvice(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("【后置通知】方法" + methodName + "执行结束\n");
}
}
5.4 步骤4:测试AOP效果
编写测试类,调用业务方法,观察控制台输出(横切逻辑是否植入成功):
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class AopTest {
@Autowired
private UserService userService;
@Test
public void testAop() {
// 调用新增用户方法
userService.addUser("张三");
// 调用查询用户方法
userService.getUserById(1);
}
}
5.5 测试结果分析
控制台输出如下(符合通知执行顺序):
【前置通知】方法addUser开始执行,参数:[张三]
新增用户:张三
【返回通知】方法addUser执行完成,返回值:null
【后置通知】方法addUser执行结束
【前置通知】方法getUserById开始执行,参数:[1]
查询用户,用户ID:1
【返回通知】方法getUserById执行完成,返回值:用户1
【后置通知】方法getUserById执行结束
若打开addUser方法中的“int i = 1 / 0;”(模拟异常),则控制台输出:
【前置通知】方法addUser开始执行,参数:[张三]
新增用户:张三
【异常通知】方法addUser执行异常,异常信息:/ by zero
【后置通知】方法addUser执行结束
说明:异常通知执行后,返回通知不会执行;后置通知无论是否异常,都会执行。
5.6 环绕通知实战(补充)
环绕通知包裹目标方法,可控制目标方法的执行(如是否执行、执行前后增强),以下以“性能监控(统计方法执行耗时)”为例:
// 在LogAspect切面类中添加环绕通知
@Around("logPointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// ProceedingJoinPoint:是JoinPoint的子类,可控制目标方法的执行
long startTime = System.currentTimeMillis(); // 开始时间
// 执行目标方法(必须调用proceed()方法,否则目标方法不会执行)
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis(); // 结束时间
String methodName = joinPoint.getSignature().getName();
System.out.println("【环绕通知】方法" + methodName + "执行耗时:" + (endTime - startTime) + "ms");
return result; // 返回目标方法的返回值
}
添加环绕通知后,测试结果会新增“执行耗时”的输出,体现环绕通知的强大功能。
六、AOP实战注意事项(避坑重点)
-
切入点表达式精准性:切入点表达式要精准,避免“过度增强”(如误增强不需要的方法),导致性能损耗;可通过缩小包范围、指定方法名等方式优化。
-
通知执行顺序:牢记通知执行顺序(环绕通知前置 → 前置通知 → 目标方法 → 环绕通知后置 → 返回/异常通知 → 后置通知),避免因顺序问题导致逻辑错误。
-
环绕通知的注意事项:环绕通知必须调用ProceedingJoinPoint的proceed()方法,否则目标方法不会执行;若需要修改目标方法的参数或返回值,可通过proceed()方法的重载实现。
-
动态代理的选择:目标类实现接口时,默认使用JDK动态代理;未实现接口时,使用CGLIB动态代理;若需强制使用CGLIB,可在application.properties中添加配置:spring.aop.proxy-target-class=true。
-
切面类的扫描:切面类必须添加@Component和@Aspect注解,且确保Spring能扫描到该类(如切面类所在包在Spring Boot的@ComponentScan扫描范围内),否则切面不生效。
-
避免循环增强:不要让切面类的方法被自身的切入点表达式匹配,否则会出现循环增强,导致栈溢出(如切面类的log方法,被切入点表达式匹配,会反复增强自身)。
-
异常处理:异常通知仅捕获目标方法抛出的异常,若通知本身抛出异常,会影响目标方法的执行;建议在通知中做好异常捕获,避免影响核心业务。
七、AOP与OOP的区别与联系
7.1 区别
-
OOP(面向对象):关注“纵向封装”,将业务逻辑按功能拆分到不同的类和对象中,核心是“对象”。
-
AOP(面向切面):关注“横向扩展”,将多个类共用的横切逻辑抽取出来,核心是“切面”。
7.2 联系
AOP不是对OOP的替代,而是对OOP的补充和完善。OOP解决了核心业务的封装问题,AOP解决了通用逻辑的复用问题,二者协同工作,让代码更简洁、更易维护。
八、总结
AOP的核心是“分离关注点”,通过动态代理将横切逻辑(日志、事务、权限)与核心业务逻辑解耦,减少代码冗余,提升维护性。
实战重点:牢记5个核心组件(切面、通知、切入点、连接点、目标对象),掌握execution切入点表达式,能熟练编写切面类,实现常用的横切逻辑(日志、性能监控等)。
注意:AOP的核心价值是“通用逻辑的复用”,不要过度使用AOP——对于不通用、仅单个方法需要的逻辑,直接写在业务方法中即可,避免过度设计导致代码复杂度提升。