AOP 面向切面编程
问题
Spring AOP 的实现原理是什么?JDK 动态代理和 CGLIB 代理的区别?AOP 常见的通知类型有哪些?
答案
什么是 AOP?
AOP(Aspect-Oriented Programming,面向切面编程) 是对 OOP 的补充,将横切关注点(日志、事务、安全、缓存等)从业务逻辑中分离出来,避免代码重复。
AOP 核心概念
| 概念 | 说明 | 举例 |
|---|---|---|
| 切面(Aspect) | 横切关注点的模块化 | 日志切面、事务切面 |
| 连接点(JoinPoint) | 程序执行中可以插入切面的点 | 方法调用、方法执行 |
| 切点(Pointcut) | 定义在哪些连接点执行切面 | execution(* com.*.service.*.*(..)) |
| 通知(Advice) | 切面在特定连接点执行的动作 | @Before、@After、@Around |
| 织入(Weaving) | 将切面应用到目标对象的过程 | Spring 在运行时通过代理织入 |
| 目标对象(Target) | 被代理的原始对象 | UserService |
| 代理对象(Proxy) | AOP 创建的增强对象 | UserService$$Proxy |
Spring AOP 使用示例
@Aspect
@Component
public class LogAspect {
// 定义切点:匹配 service 包下所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {}
// 前置通知:方法执行前
@Before("servicePointcut()")
public void before(JoinPoint joinPoint) {
String method = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("调用方法: {},参数: {}", method, Arrays.toString(args));
}
// 后置通知:方法正常返回后
@AfterReturning(pointcut = "servicePointcut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
log.info("方法 {} 返回: {}", joinPoint.getSignature().getName(), result);
}
// 异常通知:方法抛出异常后
@AfterThrowing(pointcut = "servicePointcut()", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
log.error("方法 {} 异常: {}", joinPoint.getSignature().getName(), ex.getMessage());
}
// 最终通知:无论是否异常都执行(类似 finally)
@After("servicePointcut()")
public void after(JoinPoint joinPoint) {
log.info("方法 {} 执行完毕", joinPoint.getSignature().getName());
}
// 环绕通知:最强大,可以控制方法是否执行
@Around("servicePointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
Object result = pjp.proceed(); // 执行目标方法
return result;
} finally {
long cost = System.currentTimeMillis() - start;
log.info("方法 {} 耗时: {}ms", pjp.getSignature().getName(), cost);
}
}
}
五种通知类型及执行顺序
| 通知类型 | 注解 | 执行时机 |
|---|---|---|
| 前置通知 | @Before | 目标方法执行前 |
| 后置返回通知 | @AfterReturning | 目标方法正常返回后 |
| 异常通知 | @AfterThrowing | 目标方法抛出异常后 |
| 最终通知 | @After | 目标方法执行后(无论正常/异常) |
| 环绕通知 | @Around | 包裹目标方法,可控制是否执行 |
正常执行顺序:
@Around(前半部分)
→ @Before
→ 目标方法
→ @AfterReturning
→ @After
@Around(后半部分)
异常执行顺序:
@Around(前半部分)
→ @Before
→ 目标方法(抛异常)
→ @AfterThrowing
→ @After
@Around(catch 异常)
切点表达式
// execution:匹配方法执行
@Pointcut("execution(public * com.example.service.*.*(..))")
// 修饰符 返回类型 包名.类名.方法名(参数)
// within:匹配类
@Pointcut("within(com.example.service.*)")
// @annotation:匹配注解
@Pointcut("@annotation(com.example.annotation.Log)")
// @within:匹配类上的注解
@Pointcut("@within(org.springframework.stereotype.Service)")
// bean:匹配 Spring Bean 名称
@Pointcut("bean(userService)")
// 组合表达式
@Pointcut("execution(* com.example.service.*.*(..)) && !execution(* com.example.service.*.get*(..))")
实现原理:动态代理
Spring AOP 默认在运行时通过动态代理实现:
JDK 动态代理
目标类实现了接口时使用:
// 目标接口
public interface UserService {
void save(User user);
}
// 目标实现
public class UserServiceImpl implements UserService {
public void save(User user) { /* 业务逻辑 */ }
}
// JDK 代理:基于接口生成代理类
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class[]{UserService.class},
(proxyObj, method, args) -> {
log.info("Before: " + method.getName());
Object result = method.invoke(target, args); // 调用真实对象
log.info("After: " + method.getName());
return result;
}
);
CGLIB 代理
目标类没有实现接口时使用,通过继承生成子类:
// 目标类(无接口)
public class UserService {
public void save(User user) { /* 业务逻辑 */ }
}
// CGLIB:生成目标类的子类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
log.info("Before: " + method.getName());
Object result = proxy.invokeSuper(obj, args); // 调用父类方法
log.info("After: " + method.getName());
return result;
});
UserService proxy = (UserService) enhancer.create();
JDK 代理 vs CGLIB 代理
| 对比 | JDK 动态代理 | CGLIB 代理 |
|---|---|---|
| 实现方式 | 基于接口 | 基于继承(生成子类) |
| 要求 | 目标类需实现接口 | 目标类不能是 final |
| 代理对象 | 实现目标接口 | 目标类的子类 |
| 性能 | 创建快,调用略慢 | 创建慢,调用快(方法索引) |
| Spring 默认 | Spring Framework 默认 | Spring Boot 默认 |
Spring Boot 2.x 默认 spring.aop.proxy-target-class=true,即使目标类实现了接口也使用 CGLIB。这样避免了注入时类型不匹配的问题。
AOP 失效场景
同一个类中方法 A 调用方法 B,方法 B 上的 AOP 不会生效,因为是通过 this 调用而非代理对象:
@Service
public class UserService {
@Transactional
public void methodA() {
this.methodB(); // ❌ 直接调用,不走代理,@Transactional 失效
}
@Transactional
public void methodB() { /* ... */ }
}
解决方案:
- 注入自身:
@Autowired private UserService self; - 使用
AopContext.currentProxy() - 将方法提取到另一个 Bean 中
常见面试问题
Q1: Spring AOP 的实现原理?
答案:
Spring AOP 基于动态代理实现。目标类实现了接口时使用 JDK 动态代理,没有接口时使用 CGLIB 代理(Spring Boot 默认全部使用 CGLIB)。容器创建 Bean 时,如果匹配到切面规则,会自动用代理对象替代原始 Bean。
Q2: JDK 动态代理和 CGLIB 的区别?
答案:
JDK 动态代理基于接口,通过 Proxy.newProxyInstance() 生成实现了目标接口的代理类。CGLIB 基于继承,通过 ASM 字节码技术生成目标类的子类。
JDK 代理要求目标类实现接口,CGLIB 不要求但目标类不能是 final。Spring Boot 默认使用 CGLIB。
Q3: Spring AOP 和 AspectJ 的区别?
答案:
| 对比 | Spring AOP | AspectJ |
|---|---|---|
| 织入时机 | 运行时(动态代理) | 编译时/加载时 |
| 功能范围 | 仅支持方法级别 | 支持方法、字段、构造函数等 |
| 性能 | 有代理开销 | 编译后无额外开销 |
| 使用复杂度 | 简单(注解即可) | 需要特殊编译器 |
| 自调用 | AOP 失效 | 正常生效 |
Spring AOP 够用于绝大多数场景,只有需要非方法级别的 AOP 时才考虑 AspectJ。
Q4: AOP 有哪些常见应用场景?
答案:
- 日志记录:统一记录方法入参、返回值、耗时
- 事务管理:
@Transactional就是 AOP 实现的 - 权限校验:自定义
@RequirePermission注解 - 缓存:
@Cacheable基于 AOP - 性能监控:统计方法执行耗时
- 异常处理:统一异常捕获和转换
- 限流:自定义
@RateLimit注解
Q5: @Around 通知不调用 proceed() 会怎样?
答案:
目标方法不会执行。@Around 是最强大的通知类型,它完全控制了目标方法的执行。如果不调用 pjp.proceed(),目标方法将被跳过。这个特性可以用于实现缓存(命中缓存就不执行方法)、权限校验(无权限就不执行)等。
Q6: 多个切面的执行顺序如何控制?
答案:
使用 @Order(n) 注解(值越小优先级越高):
@Aspect @Order(1) // 先执行
public class LogAspect { }
@Aspect @Order(2) // 后执行
public class SecurityAspect { }
执行顺序类似洋葱模型:Order 小的在外层,先进后出。
相关链接
- Spring AOP 官方文档
- IoC 容器与依赖注入 - Spring 另一核心功能
- 事务管理 - 基于 AOP 实现的事务
- Bean 生命周期 - BeanPostProcessor 与 AOP 代理创建
- 代理模式 - 代理模式设计理念