Appearance
AOP 详解
AOP(Aspect Oriented Programming,面向切面编程)是 Spring 两大核心之一。 它把横切关注点(日志、事务、权限)从业务代码中抽离出来,形成独立的"切面"。
一、为什么需要 AOP
没有 AOP 的时代:
public void createOrder() {
log.info("开始创建订单"); // ① 日志
checkPermission(); // ② 权限
// ===== 业务逻辑 =====
orderDao.insert(order);
// ===== 业务逻辑 =====
transaction.commit(); // ③ 事务
log.info("订单创建成功"); // ④ 日志
}
问题:日志、权限、事务散落在每个方法中,重复代码多,维护成本高
有了 AOP 之后:
public void createOrder() {
// ===== 只有业务逻辑 =====
orderDao.insert(order);
}
日志、权限、事务由切面自动织入,业务代码零侵入二、AOP 核心概念
AOP 的五个核心概念:
① JoinPoint(连接点):
程序执行过程中的某个点,如方法调用、异常抛出
Spring AOP 只支持方法级别的连接点
② Pointcut(切点):
匹配连接点的表达式,决定在哪些方法上应用切面
例:execution(* com.example.service.*.*(..))
→ 匹配 service 包下所有方法
③ Advice(通知):
在切点处执行的动作,即"增强的逻辑"
分为:Before、After、AfterReturning、AfterThrowing、Around
④ Aspect(切面):
切点 + 通知的组合,即"在哪里、做什么"
@Aspect 注解的类
⑤ Weaving(织入):
将切面应用到目标对象并创建代理对象的过程
编译期织入、类加载期织入、运行期织入
Spring AOP 使用运行期织入(动态代理)三、五种通知类型
java
@Aspect
@Component
public class LogAspect {
// ① @Before:方法执行前
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint joinPoint) {
String method = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println(">>> 开始执行 " + method + ",参数:" + Arrays.toString(args));
}
// ② @AfterReturning:方法正常返回后(可以获取返回值)
@AfterReturning(value = "execution(* com.example.service.*.*(..))",
returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println(">>> 执行成功,返回值:" + result);
}
// ③ @AfterThrowing:方法抛出异常后
@AfterThrowing(value = "execution(* com.example.service.*.*(..))",
throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
System.out.println(">>> 执行异常:" + ex.getMessage());
}
// ④ @After:方法执行后(无论成功还是异常,相当于 finally)
@After("execution(* com.example.service.*.*(..))")
public void after(JoinPoint joinPoint) {
System.out.println(">>> 执行结束");
}
// ⑤ @Around:环绕通知(最强大,可以控制方法是否执行)
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String method = joinPoint.getSignature().getName();
long start = System.currentTimeMillis();
System.out.println(">>> " + method + " 开始");
Object result = joinPoint.proceed(); // 执行目标方法(必须手动调用)
long elapsed = System.currentTimeMillis() - start;
System.out.println(">>> " + method + " 结束,耗时:" + elapsed + "ms");
return result;
}
}四、切点表达式
execution 表达式语法:
execution(修饰符? 返回值类型 包名.类名?方法名(参数类型) throws 异常?)
例:
execution(* com.example.service.*.*(..))
↑ ↑ ↑ ↑ ↑
| | | | └── 任意参数
| | | └── 任意方法名
| | └── 任意类名
| └── service 包
└── 任意返回值
常用切点表达式:
// 匹配 service 包下所有方法
execution(* com.example.service.*.*(..))
// 匹配 service 包及其子包下所有方法
execution(* com.example.service..*.*(..))
// 匹配以 save 开头的方法
execution(* com.example..*.save*(..))
// 匹配指定参数类型的方法(第一个参数为 Long)
execution(* com.example..*.*(Long, ..))
// 匹配所有 public 方法
execution(public * *(..))
// 匹配标有 @Transactional 的方法
@annotation(org.springframework.transaction.annotation.Transactional)五、AOP 底层原理
5.1 JDK 动态代理 vs CGLIB
JDK 动态代理:
条件:目标类必须实现接口
原理:Proxy.newProxyInstance() 生成代理对象
代理对象和目标对象是兄弟关系(实现同一接口)
┌──────────────┐ ┌──────────────────┐
│ OrderService │ │ $Proxy (代理) │
│ (目标对象) │ │ (实现相同接口) │
└──────────────┘ └──────────────────┘
↑ ↑
└──── 都实现 OrderDao ──┘
CGLIB 代理:
条件:不需要接口
原理:继承目标类,生成子类代理
代理对象是目标对象的子类
┌──────────────────┐
│ OrderService │
│ (目标对象) │
└────────┬─────────┘
│ 继承
┌────────▼─────────┐
│ OrderService$$ │
│ EnhancerByCGLIB │ ← 代理对象是子类
└──────────────────┘java
// Spring Boot 2.x 默认使用 CGLIB(proxy-target-class=true)
// Spring Boot 3.x 默认使用 CGLIB(不再需要接口)
// 强制使用 JDK 动态代理
@EnableAspectJAutoProxy(proxyTargetClass = false)5.2 代理对象创建时机
AOP 代理对象在 Bean 生命周期中创建:
① 实例化 → ② 属性填充 → ...
→ ⑨ BeanPostProcessor.postProcessAfterInitialization()
→ AbstractAutoProxyCreator.wrapIfNecessary()
→ 判断是否需要创建代理
→ 需要 → 创建代理对象
→ 不需要 → 返回原始对象
→ ⑩ Bean 就绪(此时容器中已经是代理对象了)六、AOP 实战场景
java
// ① 方法耗时统计
@Aspect
@Component
public class PerformanceAspect {
@Around("@annotation(com.example.annotation.Monitor)")
public Object monitor(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long elapsed = System.currentTimeMillis() - start;
if (elapsed > 1000) {
log.warn("慢方法:{} 耗时 {}ms", joinPoint.getSignature(), elapsed);
}
return result;
}
}
// ② 统一异常处理
@Aspect
@Component
public class ExceptionAspect {
@Around("execution(* com.example.controller..*.*(..))")
public Object handleException(ProceedingJoinPoint joinPoint) {
try {
return joinPoint.proceed();
} catch (BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("系统异常", e);
return Result.error("500", "系统异常");
}
}
}
// ③ 分布式锁(自定义注解 + AOP)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {
String key();
long timeout() default 10;
}
@Aspect
@Component
public class RedisLockAspect {
@Autowired
private RedissonClient redisson;
@Around("@annotation(redisLock)")
public Object lock(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {
RLock lock = redisson.getLock(redisLock.key());
if (lock.tryLock(redisLock.timeout(), TimeUnit.SECONDS)) {
try {
return joinPoint.proceed();
} finally {
lock.unlock();
}
}
throw new RuntimeException("获取锁失败");
}
}七、AOP 失效场景
java
// ① 同类内部调用(this 调用)
@Service
public class OrderService {
public void outer() {
this.inner(); // ❌ AOP 不生效!this 是目标对象,不是代理对象
}
@Transactional
public void inner() { }
}
// 解决:注入自己
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入代理对象
public void outer() {
self.inner(); // ✅ 通过代理调用,AOP 生效
}
}
// ② private 方法
@Transactional
private void inner() { } // ❌ CGLIB 不能代理 private 方法
// ③ final 方法
@Transactional
public final void inner() { } // ❌ CGLIB 不能代理 final 方法
// ④ static 方法
@Transactional
public static void inner() { } // ❌ 代理对象不拦截 static 方法八、面试要点
Q1:JDK 动态代理和 CGLIB 的区别?
JDK 动态代理要求目标类实现接口,生成的是兄弟关系(实现同一接口);CGLIB 通过继承目标类生成子类代理,不需要接口。JDK 原生支持,CGLIB 是第三方库。Spring Boot 默认使用 CGLIB。
Q2:@Around 和 @Before/@After 的区别?
@Around 可以完全控制方法执行(决定是否调用、修改参数、修改返回值),必须手动调用 joinPoint.proceed()。@Before/@After 只能做前置/后置处理,不能控制方法是否执行。
Q3:AOP 在什么情况下会失效?
同类 this 内部调用(绕过了代理)、private 方法、final 方法、static 方法、调用方和被调用方不在同一个 Spring 容器中。
