AOP核心概念
定义与作用
AOP(Aspect Oriented Programming,面向切面编程) 是对 OOP(Object Oriented Programming,面向对象编程)的补充,用于将**横切关注点(Cross-cutting Concerns)**从业务逻辑中解耦。
在传统的面向对象编程中,日志记录、权限校验、事务管理、性能监控等功能往往散落在各个业务类中,导致代码重复度高、维护困难。AOP 通过将这些横切关注点抽取为独立的切面(Aspect),在程序运行期动态织入到目标方法中,实现"业务代码专注于业务,切面代码专注于增强"的清晰分层。
Spring AOP 基于代理模式实现,默认只对运行时的方法调用进行拦截。它与 AspectJ 的关系是:Spring 使用 AspectJ 的注解语法(如 @Aspect、@Pointcut)定义切面,但运行时仍基于 Spring AOP 代理完成织入,而非 AspectJ 的编译期或加载期织入。
AOP核心术语
| 术语 | 英文 | 含义 |
|---|---|---|
| 切面 | Aspect | 横切关注点的模块化,包含 Advice 和 Pointcut |
| 连接点 | Join Point | 程序执行过程中的一个点,如方法调用、异常抛出 |
| 通知 | Advice | 切面在特定连接点采取的动作 |
| 切点 | Pointcut | 匹配连接点的谓词,定义 Advice 何时触发 |
| 引入 | Introduction | 为现有类型声明额外的方法或字段 |
| 织入 | Weaving | 将切面应用到目标对象创建代理对象的过程 |
| 代理 | Proxy | 包装目标对象的中间对象,客户端通过代理间接调用目标方法 |
| 目标对象 | Target | 被切面织入通知的原始业务对象 |
Aspect(切面)
切面是横切关注点的模块化封装。一个切面类通常包含两类核心成员:
- Pointcut(切入点):定义"在哪里切入",即匹配哪些方法
- Advice(通知):定义"切入后做什么",即在匹配的方法上执行何种增强逻辑
JoinPoint(连接点)
连接点是程序执行过程中的某个特定点。在 Spring AOP 中,由于基于代理实现,只支持方法级别的连接点,不支持字段修改、构造器调用等更细粒度的连接点(完整支持需使用 AspectJ)。
Advice(通知/增强)
Advice 是切面在特定连接点执行的动作。Spring AOP 支持五种 Advice,对应方法执行的不同阶段:
| Advice 类型 | 注解 | 执行时机 |
|---|---|---|
| Before | @Before | 方法执行前 |
| After | @After | 方法执行后(无论是否异常) |
| AfterReturning | @AfterReturning | 方法成功返回后 |
| AfterThrowing | @AfterThrowing | 方法抛出异常后 |
| Around | @Around | 包围方法执行,可控制是否执行原方法 |
Pointcut(切入点)
Pointcut 是匹配连接点的谓词表达式,定义了 Advice 应该在哪些方法上触发。Spring 支持多种 Pointcut 表达式:
| 表达式 | 含义 |
|---|---|
execution(* com.example.service.*.*(..)) | 匹配 service 包下所有类的所有方法 |
execution(public * *(..)) | 匹配所有 public 方法 |
@annotation(com.example.Loggable) | 匹配带有 @Loggable 注解的方法 |
within(com.example.service.*) | 匹配 service 包下所有类的所有方法 |
args(java.lang.String) | 匹配参数为 String 类型的方法 |
bean(*Service) | 匹配 Bean 名以 Service 结尾的方法 |
Weaving(织入)
织入是将切面应用到目标对象以创建代理对象的过程。Spring AOP 在运行时通过代理完成织入(Runtime Weaving),而 AspectJ 支持编译期织入(Compile-time Weaving)和加载期织入(Load-time Weaving)。
Proxy(代理)
代理是包装目标对象的中间对象。客户端通过代理间接调用目标方法,从而在不修改源码的情况下插入增强逻辑。Spring AOP 的代理方式有两种:JDK 动态代理和 CGLIB 代理。
核心原理:Spring AOP代理机制
AOP核心概念关系图
JDK动态代理 vs CGLIB代理
Spring AOP 基于代理模式实现,根据目标类的特性选择不同的代理方式:
| 代理方式 | 实现机制 | 适用场景 | 限制 |
|---|---|---|---|
| JDK 动态代理 | 目标类实现接口时,生成接口代理 | 面向接口编程 | 只能代理接口方法,无法代理类内部 this 调用 |
| CGLIB 代理 | 生成目标类的子类,通过方法重写拦截 | 无接口的类 | 无法代理 final 类和方法 |
Spring Boot 2.x+ 默认配置:
spring.aop.proxy-target-class=true,即优先使用 CGLIB。即使实现了接口,也可强制使用 JDK 代理:@EnableAspectJAutoProxy(proxyTargetClass = false)。
Advice执行顺序时序图
当多个 Advice 同时作用于一个方法时,Spring 按照严格的顺序执行:
执行顺序总结(正常返回):
@Around(proceed 前)→ @Before → 目标方法 → @AfterReturning → @After → @Around(proceed 后)
执行顺序总结(异常抛出):
@Around(proceed 前)→ @Before → 目标方法 → @AfterThrowing → @After → @Around(捕获异常)
完整示例:飞翔科技学生管理系统日志切面
场景简述
广州飞翔科技公司的技术部正在开发一套学生成绩管理系统。架构师白歌提出:每个 Service 层方法的执行耗时都需要记录,以便大翔(CTO)在系统上线后排查性能瓶颈。小崔(后端开发)决定使用 AOP 实现这一需求,避免在每个业务方法中重复编写计时代码。
操作前代码/配置
在未使用 AOP 之前,小崔需要在每个 Service 方法中手动编写日志和计时逻辑:
@Service
public class StudentScoreService {
public Score queryScore(Long studentId) {
long start = System.currentTimeMillis();
System.out.println("[LOG] 开始查询学生成绩,studentId=" + studentId);
// 业务逻辑:查询数据库
Score score = scoreDao.findByStudentId(studentId);
long duration = System.currentTimeMillis() - start;
System.out.println("[LOG] 查询完成,耗时 " + duration + "ms");
return score;
}
public void updateScore(Long studentId, Double newScore) {
long start = System.currentTimeMillis();
System.out.println("[LOG] 开始更新学生成绩,studentId=" + studentId);
// 业务逻辑:更新数据库
scoreDao.updateScore(studentId, newScore);
long duration = System.currentTimeMillis() - start;
System.out.println("[LOG] 更新完成,耗时 " + duration + "ms");
}
}
问题:每个方法都重复了计时和日志代码,业务逻辑被淹没在横切关注点中。如果大翔要求增加"记录操作人"功能,所有方法都需要再次修改。
使用AOP的完整代码
第一步:开启 AspectJ 自动代理
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.feixiang.student")
public class AppConfig {
}
第二步:定义切面类
@Aspect
@Component
public class PerformanceLogAspect {
// 定义切点:Service 层所有方法
@Pointcut("execution(* com.feixiang.student.service.*.*(..))")
public void serviceLayer() {}
// 环绕通知:记录方法耗时
@Around("serviceLayer()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
System.out.printf("[飞翔科技-性能监控] %s.%s 开始执行%n", className, methodName);
try {
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - start;
System.out.printf("[飞翔科技-性能监控] %s.%s 执行成功,耗时 %dms%n",
className, methodName, duration);
return result;
} catch (Exception e) {
long duration = System.currentTimeMillis() - start;
System.err.printf("[飞翔科技-性能监控] %s.%s 执行失败,耗时 %dms,异常:%s%n",
className, methodName, duration, e.getMessage());
throw e;
}
}
}
第三步:纯净的业务 Service
@Service
public class StudentScoreService {
private final ScoreDao scoreDao;
public StudentScoreService(ScoreDao scoreDao) {
this.scoreDao = scoreDao;
}
public Score queryScore(Long studentId) {
// 纯粹的业务逻辑,无任何日志/计时代码
return scoreDao.findByStudentId(studentId);
}
public void updateScore(Long studentId, Double newScore) {
// 纯粹的业务逻辑
scoreDao.updateScore(studentId, newScore);
}
}
操作后运行结果及分析
运行测试代码:
public class AopDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
StudentScoreService service = ctx.getBean(StudentScoreService.class);
service.queryScore(2024001L);
ctx.close();
}
}
控制台输出:
[飞翔科技-性能监控] StudentScoreService.queryScore 开始执行
[飞翔科技-性能监控] StudentScoreService.queryScore 执行成功,耗时 23ms
分析:
- 业务代码中完全消除了计时和日志逻辑,
StudentScoreService只关注"查询成绩"和"更新成绩" - 切面
PerformanceLogAspect统一处理所有 Service 方法的性能监控 - 当大翔需要增加新功能(如记录操作人 IP)时,只需修改切面类,无需触碰业务代码
- 符合开闭原则(Open/Closed Principle):对扩展开放,对修改关闭
易错场景与面试考点
易错场景一:同类内部 this.method() 调用不触发 AOP
这是 Spring AOP 代理机制下最常见也最容易踩坑的问题。
反例代码:
@Service
public class StudentScoreService {
public void batchUpdate(List<StudentScore> scores) {
for (StudentScore score : scores) {
// 危险!这里调用的是目标对象本身,而非代理对象
this.updateScore(score.getStudentId(), score.getNewScore());
}
}
public void updateScore(Long studentId, Double newScore) {
// 此方法上有 AOP 权限校验切面
scoreDao.updateScore(studentId, newScore);
}
}
问题:batchUpdate 中通过 this.updateScore() 调用同类方法时,不会触发 AOP 拦截。因为 this 指向的是目标对象本身,而非 Spring 容器生成的代理对象。代理对象上的权限校验、日志记录等 Advice 全部失效。
解决方案:
方案一:注入自身代理(推荐用于循环依赖已解决的场景)
@Service
public class StudentScoreService {
@Autowired
private StudentScoreService self;
public void batchUpdate(List<StudentScore> scores) {
for (StudentScore score : scores) {
// 通过代理对象调用,AOP 生效
self.updateScore(score.getStudentId(), score.getNewScore());
}
}
public void updateScore(Long studentId, Double newScore) {
scoreDao.updateScore(studentId, newScore);
}
}
方案二:使用 AopContext.currentProxy()
@Service
public class StudentScoreService {
public void batchUpdate(List<StudentScore> scores) {
for (StudentScore score : scores) {
// 获取当前代理对象
((StudentScoreService) AopContext.currentProxy())
.updateScore(score.getStudentId(), score.getNewScore());
}
}
public void updateScore(Long studentId, Double newScore) {
scoreDao.updateScore(studentId, newScore);
}
}
使用
AopContext.currentProxy()时,需在@EnableAspectJAutoProxy上设置exposeProxy = true:@EnableAspectJAutoProxy(exposeProxy = true)
方案三:重构避免内部调用
将 updateScore 抽取到另一个 Service 中,通过依赖注入调用:
@Service
public class StudentScoreService {
private final ScoreUpdateHelper helper;
public StudentScoreService(ScoreUpdateHelper helper) {
this.helper = helper;
}
public void batchUpdate(List<StudentScore> scores) {
for (StudentScore score : scores) {
helper.updateScore(score.getStudentId(), score.getNewScore());
}
}
}
面试高频考点
考点一:JDK 动态代理和 CGLIB 代理的区别?Spring 如何选择?
JDK 代理要求目标类实现接口,基于
InvocationHandler.invoke()拦截;CGLIB 生成目标类的子类,基于MethodInterceptor.intercept()拦截。Spring 默认优先使用 JDK 代理(目标有接口时),但 Spring Boot 2.x+ 默认proxyTargetClass=true优先使用 CGLIB。CGLIB 无法代理final类和方法,JDK 代理只能代理接口方法。
考点二:AOP 的五种 Advice 分别在什么时候执行?
Before(方法前)、After(方法后,无论异常)、AfterReturning(成功返回后)、AfterThrowing(异常抛出后)、Around(包围整个方法执行,可控制是否执行原方法)。正常返回顺序:Around(前)→ Before → 目标方法 → AfterReturning → After → Around(后)。异常顺序:Around(前)→ Before → 目标方法 → AfterThrowing → After → Around(捕获)。
考点三:同类内部方法调用为什么 AOP 不生效?如何解决?
因为
this.method()调用的是目标对象本身,而非代理对象。解决方案:① 注入自身代理;② 使用AopContext.currentProxy()(需exposeProxy = true);③ 重构避免内部调用,将方法抽取到另一个 Bean 中。