Java AOP 学习文档:从原理到实战

一、为什么需要 AOP?

想象一下,你正在开发一个电商系统。突然产品经理要求:“所有业务方法都要记录日志,把方法名、参数和返回值存下来。”

你可能会写出这样的代码:

Java
public void addItemToCart(Long userId, Long itemId) {
    log.info("方法开始:addItemToCart,参数:userId={}, itemId={}", userId, itemId);
    // 业务逻辑
    log.info("方法结束:addItemToCart,返回值:{}", result);
}

public Order createOrder(Long userId) {
    log.info("方法开始:createOrder,参数:userId={}", userId);
    // 业务逻辑
    log.info("方法结束:createOrder,返回值:{}", order);
}

很快你就会发现:

  • 代码重复:同样的日志代码散落在几十个方法中。
  • 核心业务被淹没:日志、权限检查、事务等非核心代码让业务逻辑变得混乱。
  • 难以维护:某天想改日志格式,需要修改几十个地方。

这种横切关注点(cross-cutting concerns)的代码,就是 AOP 要解决的问题。

AOP(Aspect-Oriented Programming,面向切面编程)允许你把这些“横切逻辑”抽离出来,统一管理,然后在需要的地方“织入”。就像在机场,所有乘客都要经过安检——安检不是在每个登机口重复部署,而是统一设在一个位置,所有登机口共享。

二、AOP 核心概念(用生活场景理解)

1. 切面(Aspect)

横切逻辑的模块化。 比如“日志记录”就是一个切面,它包含了“在方法前后打印日志”这一整套逻辑。

就像机场的“安检流程”这个完整的规章制度。

2. 连接点(Join point)

程序执行过程中能够插入切面的点。 比如方法调用前、方法调用后、抛出异常时、字段访问等。

机场里每一个可能检查旅客的位置(登机口、转机通道、到达厅)都是连接点。

3. 切入点(Pointcut)

用来匹配连接点的表达式。 不是所有连接点都需要切入,切入点帮我们筛选出真正需要增强的那些方法。

例如表达式:“所有国际航班的登机口” —— 这就是一个切入点,只在国际登机口执行安检加强。

4. 通知(Advice)

切面在某个切入点上要执行的具体动作。 包括时机(前、后、环绕等)和要执行的代码。

“检查护照和机票”就是通知,它会在国际航班登机口这个切入点上执行。

通知类型(以 Spring AOP 为例):

类型说明
前置通知 @Before在目标方法执行前运行
后置通知 @After在目标方法执行后运行(无论是否异常)
返回通知 @AfterReturning在目标方法正常返回后运行
异常通知 @AfterThrowing在目标方法抛出异常后运行
环绕通知 @Around包裹目标方法,可控制方法执行前后、参数和返回值

5. 目标对象(Target)

被通知的对象,也就是真正包含业务逻辑的那个类。

6. 代理(Proxy)

AOP 框架创建的对象,用来拦截目标方法并织入通知。在 Spring 中,你拿到的 Bean 通常是代理对象,而不是原始对象。

7. 织入(Weaving)

将切面应用到目标对象并创建代理的过程。Spring AOP 在运行时通过动态代理完成织入。

三、Spring AOP 原理简述

Spring AOP 基于动态代理技术,在运行时为目标对象生成代理对象。主要有两种方式:

  • JDK 动态代理:目标对象必须实现了接口,通过 java.lang.reflect.Proxy 创建代理。
  • CGLIB 代理:目标对象没有实现接口时,通过继承目标类生成子类代理。Spring Boot 2.x 默认使用 CGLIB(即使有接口也可通过配置强制使用 CGLIB)。

Spring AOP 是方法级别的 AOP,连接点仅限方法执行。更细粒度(如字段访问)可以使用 AspectJ,但学习成本较高,Spring 集成 AspectJ 也需额外配置。

一句话总结:你从容器里拿到的 Bean,很可能是一个“假冒”的代理对象,它会在你调用方法时执行那些额外的通知逻辑。

四、Spring AOP 实战入门(基于注解)

1. 启用 AOP 支持

在配置类上添加 @EnableAspectJAutoProxy(Spring Boot 一般自动配置,可省略)。

2. 定义一个切面类

使用 @Aspect@Component 标记类。

Java
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    // 定义切入点:匹配 com.example.service 包下所有类的所有方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {}
    
    @Before("serviceLayer()")
    public void beforeAdvice() {
        System.out.println("前置通知:准备执行服务层方法");
    }
    
    @AfterReturning(pointcut = "serviceLayer()", returning = "result")
    public void afterReturningAdvice(Object result) {
        System.out.println("返回通知:方法返回值 = " + result);
    }
    
    @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
    public void afterThrowingAdvice(Exception ex) {
        System.out.println("异常通知:发生异常 " + ex.getMessage());
    }
    
    @After("serviceLayer()")
    public void afterFinallyAdvice() {
        System.out.println("后置通知:方法执行完毕(类似 finally)");
    }
}

3. 切入点表达式速查

表达式是 AOP 的灵魂,最常用的是 execution

Text
execution(修饰符模式? 返回类型模式 方法名模式(参数模式) 异常模式?)
  • * com.example.service.*.*(..):匹配 service 包下任意类、任意方法、任意参数。
  • * com.example..*.*(..):匹配 com.example 包及其子包下所有方法。
  • public * com.example.*.UserService.*(..):匹配 UserService 中的公开方法。
  • * com.example.service.*.find*(Long,..):匹配所有以 find 开头、第一个参数为 Long 的方法。

其他常用表达式:

  • within(com.example.service.*):匹配某个包或类内的方法。
  • args(java.lang.String):匹配方法参数中有 String 类型的情况。
  • @annotation(org.springframework.transaction.annotation.Transactional):匹配标注了 @Transactional 的方法。
  • bean(userService):按 Bean 名称匹配(Spring 特有)。

4. 环绕通知 @Around 高级用法

环绕通知是最强大的一种,可以控制方法是否执行、修改参数、修改返回值,甚至吞掉异常。

Java
@Around("serviceLayer()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    String methodName = joinPoint.getSignature().getName();
    Object[] args = joinPoint.getArgs();
    System.out.println("环绕前:" + methodName);
    try {
        // 前置增强
        Object result = joinPoint.proceed(args); // 执行目标方法
        // 返回增强
        System.out.println("环绕返回:" + result);
        return result;
    } catch (Exception e) {
        // 异常增强
        System.out.println("环绕异常:" + e.getMessage());
        throw e;
    } finally {
        // 后置增强
        System.out.println("环绕后:" + methodName);
    }
}

ProceedingJoinPoint 只存在于环绕通知中,提供了控制方法执行的必要方法。

5. 多个切面的执行顺序

当一个方法匹配多个切面时,可通过 @Order 注解或实现 Ordered 接口控制顺序。数值越小,优先级越高,前置通知先执行,后置通知后执行(类似同心圆)。

Java
@Aspect
@Component
@Order(1)
public class FirstAspect { /* ... */ }

@Aspect
@Component
@Order(2)
public class SecondAspect { /* ... */ }

五、真实项目应用场景与代码示例

场景 1:统一操作日志记录

需求:记录所有 Controller 方法的调用信息,包括方法名、请求参数、IP、耗时等,并存入数据库。

Java
@Aspect
@Component
public class WebLogAspect {

    @Pointcut("execution(public * com.example.controller.*.*(..))")
    public void webLog() {}

    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        
        // 记录请求信息
        LogEntry log = new LogEntry();
        log.setUrl(request.getRequestURL().toString());
        log.setHttpMethod(request.getMethod());
        log.setClassMethod(joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        log.setArgs(Arrays.toString(joinPoint.getArgs()));
        log.setIp(request.getRemoteAddr());
        
        try {
            Object result = joinPoint.proceed();
            log.setResult(result != null ? result.toString() : null);
            return result;
        } catch (Exception e) {
            log.setException(e.getMessage());
            throw e;
        } finally {
            log.setTimeCost(System.currentTimeMillis() - start);
            // 异步保存日志,这里使用伪代码代表保存动作
            // logService.save(log);
        }
    }
}

场景 2:声明式事务管理(Spring @Transactional 原理)

Spring 的事务管理本质上就是一个 AOP 切面。当你标注 @Transactional 时,Spring 在方法前后织入了事务开启、提交、回滚的逻辑。

Java
@Aspect
@Component
public class TransactionalAspect {
    // 简化版事务切面示意
    @Around("@annotation(org.springframework.transaction.annotation.Transactional)")
    public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        // 开启事务
        // TransactionStatus status = transactionManager.begin();
        try {
            Object result = joinPoint.proceed();
            // transactionManager.commit(status);
            return result;
        } catch (Exception e) {
            // transactionManager.rollback(status);
            throw e;
        }
    }
}

场景 3:权限控制(自定义注解 + AOP)

需求:有些接口需要管理员权限,用自定义注解标记,AOP 自动校验。

自定义注解:

Java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
    String value() default "admin";
}

切面:

Java
@Aspect
@Component
public class PermissionAspect {

    @Around("@annotation(requirePermission)")
    public Object checkPermission(ProceedingJoinPoint joinPoint, RequirePermission requirePermission) throws Throwable {
        // 获取当前用户角色(例如从 token 解析)
        // String currentRole = SecurityContextHolder.getCurrentUserRole();
        String currentRole = "user"; // 示例
        if (!requirePermission.value().equals(currentRole)) {
            throw new AccessDeniedException("权限不足");
        }
        return joinPoint.proceed();
    }
}

Controller 中使用:

Java
@GetMapping("/admin/users")
@RequirePermission("admin")
public List<User> listUsers() { 
    // 业务逻辑
    return new ArrayList<>(); 
}

场景 4:方法结果缓存

需求:对查询方法做缓存,避免重复查库。第一次调用执行方法并缓存结果,后续返回缓存。

Java
@Aspect
@Component
public class CacheAspect {
    private final Map<String, Object> cache = new ConcurrentHashMap<>();

    @Around("@annotation(com.example.cache.Cacheable)")
    public Object cacheMethodResult(ProceedingJoinPoint joinPoint) throws Throwable {
        String key = generateKey(joinPoint); // 根据类名、方法名、参数生成 key
        if (cache.containsKey(key)) {
            return cache.get(key);
        }
        Object result = joinPoint.proceed();
        cache.put(key, result);
        return result;
    }
    
    private String generateKey(ProceedingJoinPoint joinPoint) {
        return joinPoint.getSignature().toShortString() + Arrays.toString(joinPoint.getArgs());
    }
}

实际项目中常用 Redis 实现分布式缓存,AOP 负责统一管理缓存的读、写、失效。

场景 5:全局异常处理与报警

需求:Service 层抛出特定异常时,发送报警邮件或钉钉消息。

Java
@Aspect
@Component
public class AlarmAspect {

    @AfterThrowing(pointcut = "execution(* com.example.service..*.*(..))", throwing = "ex")
    public void doAlarm(Exception ex) {
        if (ex instanceof RuntimeException) {
            // 发送钉钉报警
            // dingTalkClient.send("业务异常:" + ex.getMessage());
            System.out.println("发送钉钉报警:" + ex.getMessage());
        }
    }
}

场景 6:接口防刷 / 限流

需求:对同一用户调用某接口的频率进行限制,例如 1 分钟内不能超过 10 次。

Java
@Aspect
@Component
public class RateLimiterAspect {
    private final Map<String, AtomicInteger> counter = new ConcurrentHashMap<>();

    @Around("@annotation(rateLimiter)")
    public Object limit(ProceedingJoinPoint joinPoint, RateLimiter rateLimiter) throws Throwable {
        String userId = "user_123"; // 模拟获取当前用户
        String key = joinPoint.getSignature().toShortString() + ":" + userId;
        AtomicInteger count = counter.computeIfAbsent(key, k -> new AtomicInteger(0));
        
        // 注意:实际开发中需定时清空计数器,这里仅做示意。实际常用 Redis+Lua 实现。
        if (count.incrementAndGet() > rateLimiter.value()) {
            throw new RuntimeException("请求过于频繁,请稍后重试");
        }
        return joinPoint.proceed();
    }
}

场景 7:性能监控

需求:统计每个方法的平均响应时间,输出慢方法。

Java
@Aspect
@Component
public class PerformanceAspect {
    private static final long SLOW_THRESHOLD = 500_000_000L; // 500ms(纳秒为单位)

    @Around("execution(* com.example..*.*(..))")
    public Object profile(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.nanoTime();
        try {
            return joinPoint.proceed();
        } finally {
            long time = System.nanoTime() - start;
            String method = joinPoint.getSignature().toShortString();
            // 上报到监控系统如 Prometheus
            // metricsCollector.record(method, time);
            if (time > SLOW_THRESHOLD) {
                System.out.printf("慢方法:%s,耗时:%d ms%n", method, TimeUnit.NANOSECONDS.toMillis(time));
            }
        }
    }
}

六、注意事项与常见陷阱

1. 代理失效场景

因为 Spring AOP 依赖代理,以下情况切面不会生效:

  • 同一个类内部方法调用this.methodB() 调用不会被代理拦截,因为调用发生在原始对象内部。最推荐的做法是把需要增强的方法拆到另一个 Bean 中,由外部代理对象调用;AopContext.currentProxy() 只适合作为补充方案来理解代理机制,不建议把“自己注入自己”当成主流写法。
  • private / final 方法:CGLIB 无法代理 private 和 final 方法,JDK 动态代理只能代理接口方法。
  • 静态方法:无法被 Spring AOP 代理。

2. 性能考虑

AOP 本身有一定性能开销(代理调用、反射等),但通常可忽略不计。避免在循环内的方法上使用重量级的环绕通知,也不要在切入点表达式中使用过于宽泛的匹配,避免所有 Bean 都被代理。

3. 切入点表达式要尽量精确

避免使用 execution(* *(..)),这会代理所有方法,严重影响启动速度和运行时性能。

4. 通知中正确处理异常

环绕通知一定要记得将异常抛出,否则会吞掉业务异常,导致事务不回滚等问题。

七、总结

AOP 是 Java 开发中非常重要的技术,它帮助我们将横切关注点从业务逻辑中解耦,让代码更干净、更易维护。

  • 核心思想:找出横切逻辑 → 定义切面 → 声明切入点 → 编写通知 → 织入。
  • 常用实现:Spring AOP(运行时代理)、AspectJ(编译时/类加载时织入)。
  • 落地场景:日志、事务、权限、缓存、限流、监控、异常处理等任何可以“横切”的功能。

掌握了 AOP,你就拥有了一种更优雅的代码复用和组织方式。试着在你的项目中找出那些重复的、非业务的代码,把它们变成一个个切面吧!