从 Spring 2.0 引入 @AspectJ 注解支持开始,声明式切面就不再需要冗长的 XML 配置。通过一套简洁的注解,开发者可以将横切逻辑集中到独立的切面类中,并在运行时透明地织入目标 Bean。本节将聚焦于日常开发中最常用的注解式 AOP 开发方式,梳理 @Aspect、@Pointcut 及五种通知注解的用法、执行顺序和实践要点。
9.1.1 启用注解式 AOP
要让 Spring 识别并处理 @Aspect 注解,需要在配置类上添加 @EnableAspectJAutoProxy:
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.example")
public class AppConfig {
}
如果是 Spring Boot 应用,spring-boot-starter-aop 会自动引入相关依赖并启用 AOP 代理,无需手动添加该注解。
提示:Spring AOP 默认使用 JDK 动态代理(当目标类实现了接口时)或 CGLIB 代理(当目标类没有接口时)。
@EnableAspectJAutoProxy(proxyTargetClass = true)可强制使用 CGLIB,意味着代理对象是目标类的子类,因此 final 方法和 final 类无法被代理。Spring Boot 默认proxyTargetClass为true。
9.1.2 创建切面类:@Aspect + @Component
一个切面类就是一个普通的 Spring Bean,需要用 @Aspect 标记,并被容器管理:
@Aspect
@Component
public class LoggingAspect {
// 切点和通知定义
}
@Aspect 注解本身不会被 Spring 自动扫描为 Bean,因此必须配合 @Component 或通过 @Bean 方法显式注册。
9.1.3 定义切点:@Pointcut
切点(Pointcut)描述了“在哪里”应用通知。通过 @Pointcut 可以为重复使用的切点表达式赋予一个有意义的名称,提升可读性和可维护性。
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
@Pointcut("within(com.example.web..*)")
public void webLayer() {}
@Pointcut("serviceLayer() && webLayer()") // 组合切点
public void serviceAndWeb() {}
常用切点指示符一览:
| 指示符 | 说明 | 示例 |
|--------|------|------|
| execution | 匹配方法执行连接点 | execution(public com.example.service..*(..)) |
| within | 限定在某个包的类中 | within(com.example.service.*) |
| @annotation | 带有特定注解的方法 | @annotation(com.example.log.Loggable) |
| args | 方法参数类型匹配 | args(java.io.Serializable) |
| @args | 运行时参数带有特定注解 | 较少使用,略 |
| this/target | 代理/目标对象类型匹配 | this(com.example.Service) |
| bean | 按 Spring Bean 名称匹配 | bean(userService) |
execution 表达式详解
execution 是最常用的指示符,完整格式为:
execution(修饰符? 返回类型 包名.类名.方法名(参数列表) throws 异常类型?)
示例:
// 拦截所有 public 方法
execution(public * *(..))
// 拦截 service 包下所有方法,返回类型任意
execution(* com.example.service.*.*(..))
// 拦截 UserService 中以 find 开头,第一个参数为 Long 的方法
execution(* com.example.service.UserService.find*(Long,..))
自定义注解作为切点
通过自定义注解配合 @annotation,可以实现精准的切点控制:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
String value() default "";
}
切点定义:
@Pointcut("@annotation(loggable)")
public void loggableMethods(Loggable loggable) {}
后续通知方法可以直接拿到该注解实例,获取注解属性。
9.1.4 五大通知注解
Spring AOP 定义了五种通知类型,分别对应不同切入时机。通知方法可以接收一个 JoinPoint 类型的参数(对于环绕通知是 ProceedingJoinPoint),以获取方法签名、参数、目标对象等元信息。
1. 前置通知 @Before
在目标方法执行前运行,无法阻止目标方法的执行(除非抛出异常)。
@Before("serviceLayer()")
public void beforeAdvice(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().toShortString();
Object[] args = joinPoint.getArgs();
System.out.println("[Before] 方法:" + methodName + " 参数:" + Arrays.toString(args));
}
2. 后置通知 @After
在目标方法执行后运行,无论方法是正常结束还是抛出异常,都会执行。通常用于释放资源、记录结束时间等。
@After("serviceLayer()")
public void afterAdvice(JoinPoint joinPoint) {
System.out.println("[After] 方法:" + joinPoint.getSignature().getName() + " 执行完毕");
}
3. 返回通知 @AfterReturning
在目标方法正常返回后执行,可以获取返回值。若方法抛出异常则不会触发。
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
System.out.println("[AfterReturning] 返回值:" + result);
}
returning 属性值必须与方法参数名一致,类型可用 Object 接收任意返回值。
4. 异常通知 @AfterThrowing
在目标方法抛出异常后执行,可以获取异常对象。若方法正常结束则不会触发。
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
System.out.println("[AfterThrowing] 异常:" + ex.getMessage());
}
throwing 属性同理,用于绑定捕获的异常。
5. 环绕通知 @Around
功能最强大,在目标方法执行前后、异常时均可介入,并且可以控制目标方法是否执行、修改参数和返回值。必须显式调用 ProceedingJoinPoint.proceed() 才能执行目标方法。
@Around("serviceLayer()")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
String methodName = pjp.getSignature().toShortString();
long start = System.currentTimeMillis();
Object result;
try {
// 前置增强
System.out.println("[Around] 开始执行 " + methodName);
result = pjp.proceed(); // 执行目标方法
// 返回增强
System.out.println("[Around] 完成执行 " + methodName);
} catch (Exception e) {
// 异常增强
System.out.println("[Around] 异常 " + e.getMessage());
throw e; // 不吞掉异常
} finally {
System.out.println("[Around] 耗时:" + (System.currentTimeMillis() - start) + "ms");
}
return result; // 必须返回结果,否则调用方收到 null
}
重要:环绕通知的返回值必须返回
proceed()的结果,或者返回自定义的替代值。如果忘记返回,调用方将得到null,这是常见的 Bug 来源。
9.1.5 通知执行顺序
当一个切面内有多个通知,或者多个切面作用于同一个连接点时,执行顺序由 @Order 注解或实现 Ordered 接口决定。数字越小,优先级越高。
默认执行顺序如下:
Around (before proceed)
→ Before
→ 目标方法
→ AfterReturning / AfterThrowing
→ After
→ Around (after proceed)
如果存在多个优先级不同的切面,高优先级切面的前置和后置“包裹”着低优先级切面。示例:
@Aspect @Component @Order(1)
public class FirstAspect { ... }
@Aspect @Component @Order(2)
public class SecondAspect { ... }
当两者均对同一方法生效时,执行顺序为:
FirstAround.before → FirstBefore → SecondAround.before → SecondBefore
→ 目标方法
SecondAfterReturning → SecondAfter → SecondAround.after
FirstAfterReturning → FirstAfter → FirstAround.after
理解这一顺序对于调试事务、日志、缓存等多切面组合场景至关重要。
9.1.6 完整示例:方法耗时统计与日志切面
以下是一个生产环境中极为常见的组合:通过自定义注解标记需要记录日志的方法,并使用环绕通知打印方法调用信息与耗时。
1. 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Traceable {
String value() default ""; // 用于附加描述
}
2. 切面类
@Aspect
@Component
public class TraceAspect {
@Pointcut("@annotation(traceable)")
public void traceablePointcut(Traceable traceable) {}
@Around(value = "traceablePointcut(traceable)", argNames = "pjp,traceable")
public Object trace(ProceedingJoinPoint pjp, Traceable traceable) throws Throwable {
String description = traceable.value().isEmpty() ? "" : "【" + traceable.value() + "】";
String methodName = pjp.getSignature().toShortString();
Object[] args = pjp.getArgs();
log.info("{}开始执行 {},参数:{}", description, methodName, args);
long start = System.currentTimeMillis();
try {
Object result = pjp.proceed();
long elapsed = System.currentTimeMillis() - start;
log.info("{}执行完成 {},返回值:{},耗时:{} ms", description, methodName, result, elapsed);
return result;
} catch (Throwable e) {
long elapsed = System.currentTimeMillis() - start;
log.error("{}执行异常 {},耗时:{} ms,异常:{}", description, methodName, elapsed, e.getMessage());
throw e;
}
}
}
3. 业务代码使用
@Service
public class OrderService {
@Traceable("下单")
public String placeOrder(String product, int qty) {
// 业务逻辑
return "ORDER-20250316-001";
}
}
运行后日志输出类似:
[INFO] 【下单】开始执行 OrderService.placeOrder(..),参数:[iPhone, 2]
[INFO] 【下单】执行完成 OrderService.placeOrder(..),返回值:ORDER-20250316-001,耗时:152 ms
9.1.7 实用注意事项
1. 自调用不会触发 AOP
Spring AOP 基于代理实现,当同一个类内部的方法互相调用时(this.methodB()),this 指向的是目标对象而非代理对象,因此切面不会生效。解决方案:
- 将需要增强的方法移动到另一个 Bean 中(推荐)。
- 通过
AopContext.currentProxy()获取当前代理对象,然后调用((MyService) AopContext.currentProxy()).methodB(),同时需在配置类上开启@EnableAspectJAutoProxy(exposeProxy = true)。
2. 切点表达式越精确越好
避免使用过于宽泛的表达式(如 execution( (..)) 配合 within(*)),以免误拦容器初始化、销毁、toString() 等方法,导致性能下降或意外行为。
3. 异常不要随意吞掉
在环绕通知中,若捕获异常后没有重新抛出,目标方法声明的受检异常会被“吞掉”,调用方感知不到。除非你明确知道自己在做什么,否则正常流程中 throw e 是必需的。
4. 代理机制的限制
- Spring AOP 只能拦截
public方法,protected和private方法无法拦截。 - CGLIB 代理时,
final和static方法无法被拦截。 - 构造函数无法被拦截。
5. 与事务切面的协同
Spring 自身的事务管理 @Transactional 也是通过 AOP 实现的。当你同时使用自定义切面和事务时,需要注意 @Order 顺序。通常事务切面优先级较高(早于业务切面开启事务),必要时可用 @Order(Ordered.LOWEST_PRECEDENCE - 1) 调整。
9.1.8 小结
注解式 AOP 开发极大简化了横切逻辑的实现。核心步骤为:
- 启用 AOP 代理(
@EnableAspectJAutoProxy或 Spring Boot 自动配置)。 - 定义切面类(
@Aspect+@Component)。 - 声明切点(
@Pointcut+ 表达式)。 - 编写通知方法,选择合适的通知注解(
@Before、@After、@AfterReturning、@AfterThrowing、@Around)。
掌握切点表达式的书写和五种通知的执行时机,就能应付 90% 以上的 AOP 场景。下一节将深入探讨 AOP 的底层代理原理,帮助你理解这一切是如何在运行时织入的。