跳到主要内容

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 使用示例

LogAspect.java
@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 异常)

切点表达式

Pointcut 表达式
// 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 动态代理

目标类实现了接口时使用:

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 代理

目标类没有实现接口时使用,通过继承生成子类:

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 默认使用 CGLIB

Spring Boot 2.x 默认 spring.aop.proxy-target-class=true,即使目标类实现了接口也使用 CGLIB。这样避免了注入时类型不匹配的问题。

AOP 失效场景

自调用导致 AOP 失效

同一个类中方法 A 调用方法 B,方法 B 上的 AOP 不会生效,因为是通过 this 调用而非代理对象:

@Service
public class UserService {
@Transactional
public void methodA() {
this.methodB(); // ❌ 直接调用,不走代理,@Transactional 失效
}

@Transactional
public void methodB() { /* ... */ }
}

解决方案:

  1. 注入自身:@Autowired private UserService self;
  2. 使用 AopContext.currentProxy()
  3. 将方法提取到另一个 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 AOPAspectJ
织入时机运行时(动态代理)编译时/加载时
功能范围仅支持方法级别支持方法、字段、构造函数等
性能有代理开销编译后无额外开销
使用复杂度简单(注解即可)需要特殊编译器
自调用AOP 失效正常生效

Spring AOP 够用于绝大多数场景,只有需要非方法级别的 AOP 时才考虑 AspectJ。

Q4: AOP 有哪些常见应用场景?

答案

  1. 日志记录:统一记录方法入参、返回值、耗时
  2. 事务管理@Transactional 就是 AOP 实现的
  3. 权限校验:自定义 @RequirePermission 注解
  4. 缓存@Cacheable 基于 AOP
  5. 性能监控:统计方法执行耗时
  6. 异常处理:统一异常捕获和转换
  7. 限流:自定义 @RateLimit 注解

Q5: @Around 通知不调用 proceed() 会怎样?

答案

目标方法不会执行。@Around 是最强大的通知类型,它完全控制了目标方法的执行。如果不调用 pjp.proceed(),目标方法将被跳过。这个特性可以用于实现缓存(命中缓存就不执行方法)、权限校验(无权限就不执行)等。

Q6: 多个切面的执行顺序如何控制?

答案

使用 @Order(n) 注解(值越小优先级越高):

@Aspect @Order(1) // 先执行
public class LogAspect { }

@Aspect @Order(2) // 后执行
public class SecurityAspect { }

执行顺序类似洋葱模型:Order 小的在外层,先进后出。

相关链接