Spring AOP
什么是 AOP?
AOP(Aspect-Oriented Programming):面向切面编程,将横切关注点(日志、事务、权限等)从业务逻辑中分离出来。
核心概念
| 概念 | 说明 | 示例 |
|---|---|---|
| 切面(Aspect) | 横切关注点的模块化 | @Aspect 类 |
| 连接点(Join Point) | 程序执行的某个点 | 方法调用、异常抛出 |
| 切点(Pointcut) | 匹配连接点的表达式 | execution(* com.example..(..)) |
| 通知(Advice) | 在切点执行的动作 | @Before、@After、@Around |
| 目标对象(Target) | 被通知的对象 | 业务 Bean |
| 代理(Proxy) | AOP 创建的对象 | JDK 动态代理、CGLIB |
| 织入(Weaving) | 将切面应用到目标对象 | 编译期、类加载期、运行期 |
通知类型
@Aspect
@Component
public class LoggingAspect {
// 前置通知
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint joinPoint) {
System.out.println("方法执行前: " + joinPoint.getSignature().getName());
}
// 后置通知(无论是否异常都执行)
@After("execution(* com.example.service.*.*(..))")
public void after(JoinPoint joinPoint) {
System.out.println("方法执行后");
}
// 返回通知(方法成功返回)
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("方法返回: " + result);
}
// 异常通知
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
System.out.println("方法异常: " + ex.getMessage());
}
// 环绕通知(最强大)
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕前");
try {
Object result = joinPoint.proceed(); // 执行目标方法
System.out.println("环绕后");
return result;
} catch (Exception e) {
System.out.println("环绕异常");
throw e;
} finally {
System.out.println("环绕最终");
}
}
}通知执行顺序
正常执行:
Around 前 → Before → 目标方法 → Around 后 → After → AfterReturning
异常执行:
Around 前 → Before → 目标方法抛异常 → Around 异常 → After → AfterThrowing切点表达式
execution(最常用)
// 匹配所有 public 方法
execution(public * *(..))
// 匹配指定包下所有方法
execution(* com.example.service.*.*(..))
// 匹配指定类所有方法
execution(* com.example.service.UserService.*(..))
// 匹配指定方法名
execution(* com.example.service.UserService.getUser(..))
// 匹配特定参数
execution(* com.example.service.UserService.getUser(Long))
// 匹配任意参数
execution(* com.example.service.UserService.getUser(..))
// 匹配返回值
execution(String com.example.service.*.*(..))其他切点指示符
// within:匹配类型
@Pointcut("within(com.example.service..*)")
public void serviceLayer() {}
// @annotation:匹配有特定注解的方法
@Pointcut("@annotation(com.example.annotation.Log)")
public void logAnnotation() {}
// @within:匹配有特定注解的类
@Pointcut("@within(org.springframework.stereotype.Service)")
public void serviceClass() {}
// args:匹配参数类型
@Pointcut("args(String, ..)")
public void firstArgString() {}
// bean:匹配 Bean 名称
@Pointcut("bean(userService)")
public void userServiceBean() {}
// 组合切点
@Pointcut("execution(* com.example.service.*.*(..)) && args(id, ..)")
public void serviceMethodWithFirstArg(Long id) {}切点表达式组合
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
@Pointcut("execution(* com.example.dao.*.*(..))")
public void daoLayer() {}
// && 与
@Before("serviceLayer() && args(id)")
public void beforeService(Long id) {}
// || 或
@Before("serviceLayer() || daoLayer()")
public void beforeServiceOrDao() {}
// ! 非
@Before("serviceLayer() && !execution(* com.example.service.UserService.*(..))")
public void beforeServiceExcludeUserService() {}代理机制
JDK 动态代理 vs CGLIB
| 特性 | JDK 动态代理 | CGLIB |
|---|---|---|
| 实现方式 | 基于接口 | 基于继承 |
| 要求 | 目标类实现接口 | 目标类不能是 final |
| 性能 | 调用稍慢(反射) | 创建慢,调用快 |
| Spring 默认 | 接口用 JDK | 无接口用 CGLIB |
Spring 代理选择策略
1. 目标类实现接口?
├── 是 → 默认使用 JDK 动态代理
└── 否 → 使用 CGLIB
2. 配置强制使用 CGLIB
@EnableAspectJAutoProxy(proxyTargetClass = true)
或
spring.aop.proxy-target-class=true代理对象创建过程
1. Spring 创建目标 Bean
2. 初始化后(postProcessAfterInitialization)
3. 检查是否有切面匹配该 Bean
4. 有匹配 → 创建代理对象
├── 实现接口 → JDK 动态代理
└── 无接口 → CGLIB 代理
5. 返回代理对象,替换原 Bean代码示例
// 目标类
@Service
public class UserService {
public User getUser(Long id) {
return new User(id, "张三");
}
}
// 切面
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.service.UserService.*(..))")
public Object log(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("调用方法: " + pjp.getSignature());
Object result = pjp.proceed();
System.out.println("返回结果: " + result);
return result;
}
}
// 代理后的调用链
UserService proxy = context.getBean(UserService.class);
proxy.getUser(1L);
// 实际执行:
// proxy.getUser() → LoggingAspect.log() → target.getUser()@EnableAspectJAutoProxy
@Configuration
@EnableAspectJAutoProxy(
proxyTargetClass = true, // 强制使用 CGLIB
exposeProxy = true // 暴露代理对象到 AopContext
)
public class AopConfig {
}exposeProxy 用途
// 问题:同类方法调用不走代理
@Service
public class UserService {
@Transactional
public void methodA() {
// 直接调用 methodB,不走代理,@Transactional 失效
methodB();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// ...
}
}
// 解决:通过代理调用
@Service
public class UserService {
@Transactional
public void methodA() {
// 通过代理调用
((UserService) AopContext.currentProxy()).methodB();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// ...
}
}实际应用
日志切面
@Aspect
@Component
@Slf4j
public class LogAspect {
@Around("@annotation(logAnnotation)")
public Object log(ProceedingJoinPoint pjp, LogAnnotation logAnnotation) throws Throwable {
String methodName = pjp.getSignature().getName();
Object[] args = pjp.getArgs();
log.info("方法 {} 开始执行,参数: {}", methodName, Arrays.toString(args));
long start = System.currentTimeMillis();
try {
Object result = pjp.proceed();
long cost = System.currentTimeMillis() - start;
log.info("方法 {} 执行成功,耗时: {}ms,结果: {}", methodName, cost, result);
return result;
} catch (Exception e) {
log.error("方法 {} 执行异常: {}", methodName, e.getMessage());
throw e;
}
}
}
// 使用
@LogAnnotation
public User getUser(Long id) {
return userDao.findById(id);
}权限切面
@Aspect
@Component
public class PermissionAspect {
@Before("@annotation(requirePermission)")
public void checkPermission(JoinPoint jp, RequirePermission requirePermission) {
String permission = requirePermission.value();
User user = SecurityContext.getCurrentUser();
if (!user.hasPermission(permission)) {
throw new PermissionDeniedException("无权限: " + permission);
}
}
}
// 使用
@RequirePermission("user:delete")
public void deleteUser(Long id) {
userDao.deleteById(id);
}性能监控切面
@Aspect
@Component
public class PerformanceAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
String methodName = pjp.getSignature().toShortString();
long start = System.nanoTime();
Object result = pjp.proceed();
long cost = System.nanoTime() - start;
if (cost > 1_000_000) { // 超过 1ms 记录
MetricsCollector.recordSlowMethod(methodName, cost / 1_000_000.0);
}
return result;
}
}面试高频问题
Q1: AOP 是如何实现的?
回答要点:
- 编译期织入:AspectJ(需要特殊编译器)
- 类加载期织入:AspectJ LTW(Load-Time Weaving)
- 运行期织入:Spring AOP(动态代理)
Spring AOP 使用运行期织入:
- 通过 BeanPostProcessor 在 Bean 初始化后创建代理
- JDK 动态代理或 CGLIB
Q2: Spring AOP 和 AspectJ 的区别?
| 特性 | Spring AOP | AspectJ |
|---|---|---|
| 织入时机 | 运行期 | 编译期/类加载期 |
| 实现方式 | 动态代理 | 字节码增强 |
| 功能 | 方法级别 | 方法、字段、构造函数等 |
| 性能 | 稍慢 | 更快 |
| 依赖 | Spring 容器 | 独立使用 |
Q3: 什么情况下 AOP 会失效?
回答要点:
- 同类方法调用:直接调用不走代理
// 解决方案:
// 1. 注入自己
@Autowired
private UserService self;
public void methodA() {
self.methodB(); // 走代理
}
// 2. 使用 AopContext
((UserService) AopContext.currentProxy()).methodB();-
private/protected 方法:CGLIB 无法代理
-
final 方法/类:CGLIB 无法继承
-
构造函数内调用:Bean 未完成初始化
Q4: JDK 动态代理和 CGLIB 的区别?
| 对比 | JDK 动态代理 | CGLIB |
|---|---|---|
| 实现原理 | 反射 + 接口 | 继承 + 字节码生成 |
| 要求 | 必须实现接口 | 不能是 final 类 |
| 性能 | 反射调用稍慢 | FastClass 优化 |
| Spring 选择 | 接口优先 | 无接口时使用 |
Q5: @Around 和其他通知的执行顺序?
@Around 前 → @Before → 目标方法 → @Around 后 → @After → @AfterReturning/@AfterThrowing多个切面的顺序:
@Order(1) // 数字越小优先级越高
@Aspect
public class Aspect1 {}
@Order(2)
@Aspect
public class Aspect2 {}
// 执行顺序:
// Aspect1 Around 前 → Aspect2 Around 前 → Aspect1 Before → Aspect2 Before → 目标方法总结
Spring AOP 核心要点:
1. 核心概念:切面、切点、通知、连接点
2. 通知类型:@Before、@After、@AfterReturning、@AfterThrowing、@Around
3. 切点表达式:execution 最常用
4. 代理机制:JDK 动态代理(接口)、CGLIB(类)
5. 失效场景:同类调用、private/final 方法