Skip to content

面向切面编程(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() 走的不是代理对象

解决方案:

  1. sendEmail 抽到另一个 Service 类中
  2. 或者 ((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 { ... }

面向 PHP 开发者的 Spring Boot 文档