面向切面编程(AOP)⚠️
PHP 没有直接的 AOP 对应概念。最接近的类比是:
- 中间件(请求前后处理) +
- 事件/监听器(解耦横切关注点) +
- Trait(复用公共代码)
AOP 是这三者的结合,但比它们更强大、更底层。
什么时候用 AOP
AOP 解决的是横切关注点(Cross-cutting Concerns)——那些散布在多个类中的公共逻辑:
java
// 没有 AOP 时,每个方法都要写重复的日志代码
@Service
public class UserService {
public User findById(Long id) {
log.info("调用 findById, 参数: {}", id); // 重复
long start = System.currentTimeMillis(); // 重复
User result = userRepository.findById(id);
log.info("findById 耗时: {}ms", System.currentTimeMillis() - start); // 重复
return result;
}
public List<User> findAll() {
log.info("调用 findAll"); // 重复
long start = System.currentTimeMillis(); // 重复
List<User> result = userRepository.findAll();
log.info("findAll 耗时: {}ms", System.currentTimeMillis() - start); // 重复
return result;
}
}AOP 可以把这些横切逻辑抽到一个地方,统一管理:
java
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(); // 执行目标方法
long duration = System.currentTimeMillis() - start;
log.info("{} 耗时 {}ms", joinPoint.getSignature(), duration);
return result;
}
}核心概念
| 概念 | 说明 | 类比 |
|---|---|---|
| Aspect(切面) | 横切关注点的模块化类 | 一个包含通知和切点的类 |
| Join Point(连接点) | 程序执行中的某个点(方法调用) | 可以被拦截的地方 |
| Advice(通知) | 在连接点执行的代码 | 相当于 handle() 中的逻辑 |
| Pointcut(切点) | 匹配连接点的表达式 | 告诉 Spring "拦截哪些方法" |
| Target(目标对象) | 被通知的对象 | 被代理的原始 Bean |
五种通知类型
| 通知 | 执行时机 | 对应 PHP |
|---|---|---|
@Before | 目标方法执行前 | 中间件的 $request 处理阶段 |
@After | 目标方法执行后(不管是否异常) | finally 块 |
@AfterReturning | 目标方法成功返回后 | 正常执行后 |
@AfterThrowing | 目标方法抛出异常后 | catch 块 |
@Around | 包裹整个方法 | 中间件的 $next($request) |
java
@Aspect
@Component
public class ServiceAspect {
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint jp) {
log.info("调用: {}", jp.getSignature());
}
@AfterReturning(value = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturn(JoinPoint jp, Object result) {
log.info("返回: {}", result);
}
@Around("@annotation(io.micrometer.core.annotation.Timed)")
public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed();
} finally {
long ms = (System.nanoTime() - start) / 1_000_000;
log.info("耗时 {}ms", ms);
}
}
}切点表达式
execution(返回类型 包.类.方法(参数))常用写法:
| 表达式 | 含义 |
|---|---|
execution(* com.example.service.*.*(..)) | service 包下所有类的所有方法 |
execution(* com.example..*.*(..)) | com.example 及其子包所有方法 |
execution(public * com.example.service.UserService.*(..)) | UserService 的所有 public 方法 |
@annotation(org.springframework.transaction.annotation.Transactional) | 所有带 @Transactional 的方法 |
within(com.example.service.*) | service 包下的所有类 |
bean(userService) | 名为 userService 的 Bean |
切点表达式是 AOP 中最难记忆的部分。绝大多数场景只需要
execution(* 包名..*.*(..))和@annotation(...)这两种。
常见应用场景
java
// 1. 性能监控
@Around("execution(* com.example..*.*(..))")
public Object monitor(ProceedingJoinPoint pjp) throws Throwable { ... }
// 2. 操作日志
@AfterReturning("@annotation(OperateLog)")
public void logOperation(JoinPoint jp) { ... }
// 3. 权限检查
@Before("@annotation(PreAuthorize)")
public void checkPermission(JoinPoint jp) { ... }
// 4. 缓存
@Around("@annotation(Cacheable)") // Spring 自己用 AOP 实现了 @Cacheable
public Object cache(ProceedingJoinPoint pjp) throws Throwable { ... }
// 5. 重试机制
@Around("@annotation(Retryable)")
public Object retry(ProceedingJoinPoint pjp) throws Throwable { ... }⚠️ 常见坑
1. 同一个类中的方法调用不会触发 AOP
java
@Service
public class UserService {
@Transactional
public void createUser(User user) {
save(user);
sendEmail(user); // ❌ 内部调用不会触发事务/切面
}
@Transactional
public void sendEmail(User user) { ... }
}
// 外部调用 createUser() 时,createUser 的事务生效,但 sendEmail 的事务不生效
// 因为 AOP 基于代理,内部调用 this.sendEmail() 走的不是代理对象解决方案:
- 把
sendEmail抽到另一个 Service 类中- 或者
((UserService) AopContext.currentProxy()).sendEmail(user)(不推荐)
2. private 方法不会被拦截
AOP 基于代理,代理对象只覆盖 public 方法。private 方法的
@Transactional/@Cacheable等注解不会生效。
3. AOP 只对 Spring Bean 生效
new UserService()的对象不会被 AOP 拦截。只有通过 IoC 容器获取的 Bean 才会被增强。
4. 过度使用 AOP 会让代码难调试
当一个方法有很多切面时(事务 + 缓存 + 日志 + 权限),执行顺序和互相影响变得难以追踪。AOP 适合做"基础设施级"的事情,不适合做业务逻辑。
5. 启动 @Aspect 需要 @EnableAspectJAutoProxy
Spring Boot 自动配置了 AOP,一般不需要手动加。但如果需要暴露代理对象(解决坑 1):
java@SpringBootApplication @EnableAspectJAutoProxy(exposeProxy = true) public class DemoApplication { ... }