我把 AOP(面向切面编程) 在 Spring Boot 里的方方面面讲透:从概念、实现原理、常见切点表达式、Advice 类型、与事务/代理的交互,到常见坑、调试与生产实践。篇幅会比较长,但尽量条理清楚、实用可复制。
核心概念(一句话)
AOP 的目的就是把“横切关注点”(日志、事务、权限、限流、埋点、监控等)从业务代码中分离出来,通过“切面(Aspect)”在运行时把这些代码织入到目标方法的执行流程中,从而实现横向复用与解耦。
为什么用 AOP?常见用途
- 日志记录(入口/出口/耗时/参数)
- 事务管理(在 Spring 中一般由 AOP 实现)
- 权限/鉴权检查
- 重试/限流/熔断(或配合 Resilience4j)
- 性能埋点 / 指标上报(Micrometer)
- 缓存切面(缓存读取/更新/失效)
- 事件发布 / 异常统一处理
Spring AOP vs AspectJ(重大的选择)
- Spring AOP(默认)
- 实现方式:基于代理(proxy-based),Spring 在运行时为目标对象生成代理并在代理里织入 advice。
- 支持的 join point:仅限方法执行(method execution)。
- 适用场景:绝大多数 Web/Service 场景,轻量、简单配置(spring-boot-starter-aop)。
- 优点:集成简单、对性能影响小、与 Spring 容器紧密集成。
- 限制:不能拦截 private/final 方法(proxy 限制),无法拦截构造器、字段访问等。
- AspectJ(完整 AOP)
- 实现方式:编译时织入 (CTW)、后编译织入 或 加载时织入 (LTW)(需要 aspectjweaver)。
- 支持的 join point:超级全面(方法、构造器、字段、异常处理点等)。
- 适用场景:需要拦截 private 方法、构造器、字段访问,或必须避免代理带来的自调用问题时。
- 成本:配置复杂些、可能需要 -javaagent 或编译时支持。
实务提议:先用 Spring AOP,只有当的确 需要拦截 private/构造/字段或必须要在类内部也被织入时,才思考 AspectJ。
Advice(通知)类型(行为与用法)
- @Before:在目标方法执行前执行(不能修改目标方法返回值)。
- @AfterReturning:目标方法正常返回后执行(可以拿到返回值)。
- @AfterThrowing:目标方法抛异常时执行(可以拿到异常)。
- @After(finally):目标方法执行结束后执行(无论正常或异常)。
- @Around:最强劲的类型,可以在目标方法前后环绕、决定是否执行目标方法、修改参数与返回值(需要 ProceedingJoinPoint.proceed())。
注意:@Around 可以替代 @Before/@After 的许多场景,但写 @Around 时要小心异常传播和 finally 逻辑。
示例(@Around):
@Around("execution(public * com.example..service..*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long t0 = System.currentTimeMillis();
try {
Object result = pjp.proceed();
return result;
} finally {
long t1 = System.currentTimeMillis();
log.info("{} executed in {} ms", pjp.getSignature(), (t1 - t0));
}
}
Pointcut(切点)表达式实战
常用语法与示例:
- execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
- 例:execution(public * com.example.service.*.*(..)) —— 所有 com.example.service 包下 public 方法
- within(typePattern):限制在某个类/包内
- within(com.example..controller..*)
- args(…):根据运行时参数类型匹配
- args(java.lang.String, ..)
- @annotation(…):方法上有某注解
- @annotation(org.springframework.transaction.annotation.Transactional)
- @within(…):类上有注解
- bean(beanName):根据 Spring bean 名称匹配
- target(type) / this(type):target 表明目标对象类型,this 表明代理对象类型(proxy)
- 复合写法:
- @Pointcut(“execution(* com.example..*(..)) && @annotation(com.example.Loggable)”)
注意:
- execution 的匹配是基于方法签名(编译时),args 是基于运行时参数类型。
- @annotation 超级方便:自定义注解 + 基于注解切面是最常见的实践(显式、可读)。
Spring AOP 的实现细节(代理类型、开启方式)
启用 AOP
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
public class DemoApplication { ... }
- proxyTargetClass = true:强制使用 CGLIB(类代理),即使目标有接口。否则默认优先用 JDK 动态代理(若有接口)。
- exposeProxy = true:允许在运行时通过 AopContext.currentProxy() 获取当前代理(用于解决自调用问题的一种方法)。
代理类型区别
- JDK 动态代理:生成实现目标接口的代理类。只有通过接口访问的方法会被拦截,目标类没有接口则不能用。
- CGLIB(子类代理):通过继承目标类创建子类并覆盖方法(目标方法不能是 final)。可拦截更多场景(没有接口的类)。
能被拦截的方法
- 代理必须处理“通过代理对象调用”的方法。因此:
- 外部通过代理调用 public/protected(非 final)方法会触发 advice(CGLIB);
- JDK 代理只能拦截接口声明的方法;
- 私有方法、final 方法、类构造器、类初始化块不能被 Spring AOP 拦截(除非用 AspectJ)。
常见坑与“自调用”问题(最容易踩的)
场景:
@Service
public class AService {
@Transactional
public void outer() {
// ...
inner(); // <-- 直接调用,不会走代理 => inner 上的 @Transactional/@Aspect 都不会被触发
}
@Transactional
public void inner() { ... }
}
缘由:this.inner() 是在同一实例内直接调用,没有经过代理,因此 AOP 拦截器不会触发(事务、切面都绕过)。
解决方案(按难易/推荐顺序):
- 把 inner 提取到另一个 Bean(最推荐):OtherService.inner(),通过 Spring 注入并调用,这样能走代理。
- 通过 AopContext.currentProxy() 使用代理来调用(需设置 exposeProxy=true):((AService) AopContext.currentProxy()).inner()
- 使用 AspectJ 的编织(AspectJ LTW / CTW):不依赖代理,能织入类内部调用。
- 重构设计:思考方法是否应为公共/独立职责,尽量避免需要自调用触发切面。
Advice 顺序(@Order)和嵌套 @Around 的行为
- 在多个切面/Advice 同时匹配一个 join point 时,@Order(或实现 Ordered)控制优先级:数值越小越优先(越“外层”)。
- 对于多个 @Around,假设有 A(order=1) 和 B(order=2),它们的调用顺序是:
- A 的 @Around 先执行(外层),在 A 的 proceed() 中调用 B 的 @Around,最后到目标方法。
- 这种嵌套会影响异常的传播与处理(外层可以捕获并处理异常,阻止内层看到异常)。
AOP 与 @Transactional 的交互要点
- Spring 的事务支持本质上就是通过 AOP 拦截实现的(事务是个切面)。因此 @Transactional 也受代理限制(同样会有自调用问题)。
- 顺序问题:若自定义切面与事务切面都存在,要注意 @Order 排序(例如日志切面一般希望在事务外层或内层,依据需求):
- 若日志切面在事务外层(order 小),日志能记录事务开始前后的情况;若在事务内层(order 大),日志能看到事务已开启的上下文(例如 DB 连接已绑定)。
- 如果你需要确保事务在某些切面之前开启,设置切面的 @Order 值较大以保证事务先被套上或反之,视具体逻辑。
常见的切面模式(实战模板)
- 基于注解的切面(最推荐):定义 @TrackTime / @Loggable 自定义注解,然后切 @annotation(…)。
- 基于包/类模式的切面:execution(* com.example.service..*(..))。
- Controller 层切面:聚焦处理请求日志、参数脱敏、接口耗时、异常映射(注意不要在 Controller 切面做阻塞或长耗时逻辑)。
- 异常统一处理 + 报警:@AfterThrowing 捕捉异常并发出告警(但应确保不要在生产切面里做阻塞行为)。
- 指标/埋点切面:在切点中记录 Micrometer 指标,如计时器 Timer、计数器 Counter。
调试技巧(快速排查 AOP 问题)
- 检查 bean 是否被代理:
import org.springframework.aop.support.AopUtils;
AopUtils.isAopProxy(bean); // true/false
AopUtils.isCglibProxy(bean);
AopUtils.isJdkDynamicProxy(bean);
- 打开 Spring AOP debug 日志:logging.level.org.springframework.aop=DEBUG。
- 在切面里打印 pjp.getSignature()、pjp.getTarget().getClass()、Proxy 类名等。
- 若切面没被触发,确认调用路径是否走了代理(外部调用 vs 自调用)以及 pointcut 是否准确匹配。
- 使用单元测试 @SpringBootTest + @SpyBean 监视切面的执行。
最小可运行示例(Spring Boot)
下面给出一个超级实用的最小 demo:包含 pom.xml、启动类、一个 Service、一个控制器、两个切面(日志 + 注解式耗时统计)、以及如何解决自调用问题的演示。
1) pom.xml(核心依赖)
<!-- 省略 parent/版本管理,只列关键依赖 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2) 启动类(开启 exposeProxy 以便示例里演示)
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
public class DemoApplication {
public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }
}
3) 自定义注解(用于标记方法埋点)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackTime {}
4) 日志切面(通用)
@Aspect
@Component
@Order(20)
public class LoggingAspect {
private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);
@Pointcut("execution(public * com.example.demo.service..*(..))")
public void serviceMethods() {}
@Around("serviceMethods()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
String name = pjp.getSignature().toShortString();
Object[] args = pjp.getArgs();
log.info("Enter {} args={}", name, Arrays.toString(args));
long t0 = System.currentTimeMillis();
try {
Object r = pjp.proceed();
log.info("Exit {} return={} time={}ms", name, r, System.currentTimeMillis() - t0);
return r;
} catch (Throwable ex) {
log.error("Exception in {} : {}", name, ex.toString());
throw ex;
}
}
}
5) 注解式耗时切面(只对带 @TrackTime 的方法)
@Aspect
@Component
@Order(10)
public class TrackTimeAspect {
private static final Logger log = LoggerFactory.getLogger(TrackTimeAspect.class);
@Pointcut("@annotation(com.example.demo.annotation.TrackTime)")
public void annotatedMethods() {}
@Around("annotatedMethods()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long t0 = System.nanoTime();
try {
return pjp.proceed();
} finally {
long t1 = System.nanoTime();
log.info("{} took {} ms", pjp.getSignature().toShortString(), (t1 - t0) / 1_000_000);
}
}
}
6) Service 示例(演示自调用问题与解决)
public interface MyService {
String hello(String name);
String compCall(String name);
}
@Service
public class MyServiceImpl implements MyService {
@Override
@TrackTime
public String hello(String name) {
return "Hello " + name;
}
@Override
public String compCall(String name) {
// 直接自调用(下面的 hello 方法带 @TrackTime)
// this.hello(name); // <- 这样不会走切面(自调用)
// 正确做法:通过代理调用
MyService proxy = (MyService) AopContext.currentProxy();
return proxy.hello(name); // 需要在 @EnableAspectJAutoProxy 设置 exposeProxy=true
}
}
7) Controller
@RestController
@RequestMapping("/api")
public class DemoController {
private final MyService myService;
public DemoController(MyService myService) { this.myService = myService; }
@GetMapping("/hello")
public String hello(@RequestParam(defaultValue = "world") String name) {
return myService.hello(name);
}
@GetMapping("/comp")
public String comp(@RequestParam(defaultValue = "world") String name) {
return myService.compCall(name);
}
}
启动并访问 /api/hello?name=Ray 将会触发 TrackTimeAspect;访问 /api/comp 展示自调用通过 AopContext 触发切面(注意必须启用 exposeProxy=true)。
单元测试切面(思路)
- 用 @SpringBootTest 启动容器,@SpyBean 注入切面实例,然后 verify() 切面方法被调用。
- 示例:
@SpringBootTest
public class AspectTest {
@Autowired MyService myService;
@SpyBean TrackTimeAspect trackTimeAspect;
@Test
void aspectInvoked() {
myService.hello("t");
verify(trackTimeAspect, times(1)).around(any(ProceedingJoinPoint.class));
}
}
注意:around 方法是一个普通方法(非 final)且切面是 Spring 管理的 bean,因此 @SpyBean 能 spy。
生产环境最佳实践(要点速查)
- 尽量使用基于注解的切点(显式可读,不会误伤)
- 避免在切面里做长耗时或阻塞操作(影响主业务线程),改为异步/消息
- 不要用过宽的 pointcut(如 execution(* *(..))),会带来性能和维护问题
- 切面中尽量不要抛出未被处理的异常(这会影响业务异常流)
- 注意切面调用顺序(@Order),特别是与事务、鉴权相关的切面
- 测试覆盖切面场景(外部调用、自调用、异常路径)
- 监控切面开销(用 metrics 记录切面耗时,避免在高 QPS 热点方法上做重操作)
- 代码可见性:把切点逻辑写清楚(注释、单元测试文档)以便别人理解切面会在哪些地方生效
常见问题快速答案
- 为什么我的 @Transactional 不生效? 多数缘由是自调用导致没有走代理;解决见上文“自调用”部分。
- 为什么切面没有被触发? 检查 pointcut 是否匹配、调用是否通过代理、方法是否为 private/final、切面是否在 Spring 容器中。
- 如何拦截 private 方法? Spring AOP 拦截不了 private;除非改用 AspectJ(CTW/LTW)。
- 如何确认 bean 是否被代理? 使用 AopUtils.isAopProxy(bean) 或打印 bean.getClass() 看是否为 CGLIB 生成的类(含 $$EnhancerByCGLIB$$)。
总结(要点回顾)
- AOP 是解耦横切逻辑的利器,Spring AOP 用代理实现、简单高效;AspectJ 提供更强劲的织入能力。
- 重点掌握:pointcut 写法、各类 advice 行为、代理实现(JDK/CGLIB)与自调用限制、与事务的交互,以及切面顺序。
- 生产上提议:使用注解驱动的切点、保持切面轻量、严格测试与监控切面性能。