Java AOP 学习文档:从原理到实战
一、为什么需要 AOP?
想象一下,你正在开发一个电商系统。突然产品经理要求:“所有业务方法都要记录日志,把方法名、参数和返回值存下来。”
你可能会写出这样的代码:
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 标记类。
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:
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 高级用法
环绕通知是最强大的一种,可以控制方法是否执行、修改参数、修改返回值,甚至吞掉异常。
@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 接口控制顺序。数值越小,优先级越高,前置通知先执行,后置通知后执行(类似同心圆)。
@Aspect
@Component
@Order(1)
public class FirstAspect { /* ... */ }
@Aspect
@Component
@Order(2)
public class SecondAspect { /* ... */ }
五、真实项目应用场景与代码示例
场景 1:统一操作日志记录
需求:记录所有 Controller 方法的调用信息,包括方法名、请求参数、IP、耗时等,并存入数据库。
@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 在方法前后织入了事务开启、提交、回滚的逻辑。
@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 自动校验。
自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String value() default "admin";
}
切面:
@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 中使用:
@GetMapping("/admin/users")
@RequirePermission("admin")
public List<User> listUsers() {
// 业务逻辑
return new ArrayList<>();
}
场景 4:方法结果缓存
需求:对查询方法做缓存,避免重复查库。第一次调用执行方法并缓存结果,后续返回缓存。
@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 层抛出特定异常时,发送报警邮件或钉钉消息。
@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 次。
@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:性能监控
需求:统计每个方法的平均响应时间,输出慢方法。
@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,你就拥有了一种更优雅的代码复用和组织方式。试着在你的项目中找出那些重复的、非业务的代码,把它们变成一个个切面吧!