AOP(Aspect-Oriented Programming,面向切面编程)是对 OOP 的补充,用于将横切关注点(cross-cutting concerns)从业务逻辑中分离出来,集中管理。典型横切关注点包括:日志记录、事务管理、权限校验、性能监控、缓存、同步等——这些逻辑如果分散在每个方法里,既冗余又难以维护。
AOP 实际是 GoF 设计模式的延续,设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,AOP 可以说也是这种目标的一种实现。
AOP 解决了什么问题
在没有 AOP 的情况下,每个业务方法都要亲自处理横切逻辑:
// 没有 AOP:每个方法都要重复写日志、事务、权限检查
public User findById(Long id) {
log.info("开始查询 user={}", id); // 日志
checkPermission("user:read"); // 权限
Transaction tx = db.beginTransaction(); // 事务
try {
User user = db.query(id); // 真正的业务逻辑
tx.commit();
return user;
} catch (Exception e) {
tx.rollback();
throw e;
} finally {
log.info("查询结束"); // 日志
}
}
这带来三个问题:
- 重复代码:日志、事务、权限逻辑散落在每个方法里,修改一处要改几十个地方
- 职责混乱:业务方法承担了与业务无关的基础设施职责,难以阅读
- 无法集中管控:想统一修改日志格式或事务策略,必须逐一修改每个方法
AOP 把这些横切逻辑抽取到切面里统一管理,业务方法只写业务:
// 有 AOP:业务方法只写业务,其余由切面处理
@Transactional
@PreAuthorize("hasRole('USER')")
public User findById(Long id) {
return db.query(id); // 只剩业务逻辑
}
Spring AOP
Spring AOP 基于动态代理实现,不修改原始类的字节码,而是在运行时生成代理对象,在方法调用前后插入额外逻辑。
版本历史
Spring AOP 从 Spring 1.0(2004年) 就已存在,是 Spring Framework 最早的核心特性之一:
| 版本 | 发布时间 | AOP 能力 |
|---|---|---|
| Spring 1.0 | 2004年3月 | Spring AOP 首次发布,只能用 XML + 接口(MethodInterceptor)写切面,配置繁琐 |
| Spring 1.2 | 2004年 | 支持 AspectJ 切点表达式,但仍需 XML 配置 |
| Spring 2.0 | 2006年 | 引入 @Aspect、@Around、@Before 等注解,切面写法大幅简化 |
| Spring 3.0 | 2009年 | 支持 @EnableAspectJAutoProxy,可以完全不写 XML |
| Spring Boot 1.0 | 2014年 | spring-boot-starter-aop 自动配置,零配置启用 |
@Around、@Before 等注解来自 AspectJ 项目(org.aspectj.lang.annotation.*),Spring 2.0 开始借用这套注解语法,底层仍用自己的动态代理执行,不依赖 AspectJ 的编译器或织入器。
Spring MVC 时代 vs Spring Boot 时代
@Around 等注解本身没有变化,区别在于谁来开启代理机制。
Spring MVC 时代(需手动开启):
必须在 applicationContext.xml 里显式声明,否则 @Aspect 注解完全不生效:
<!-- 开启 AspectJ 注解代理 -->
<aop:aspectj-autoproxy/>
<!-- 切面类也要注册成 Bean -->
<bean class="com.example.aspect.LogAspect"/>
或 Java Config 写法(Spring 3.0+,不写 XML,但仍需显式声明):
@Configuration
@EnableAspectJAutoProxy // 必须加,否则 @Aspect 不生效
public class AppConfig {
@Bean
public LogAspect logAspect() {
return new LogAspect();
}
}
Spring Boot 时代(自动开启):
加一个依赖,框架自动完成上述所有配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Spring Boot 的 AopAutoConfiguration 检测到 aspectjweaver 在 classpath 上时,自动等效于加了 @EnableAspectJAutoProxy。切面类只需 @Component + @Aspect 即可被扫描并生效,不需要任何手动配置。
核心概念
| 术语 | 说明 |
|---|---|
| Aspect(切面) | 横切关注点的模块化封装,用 @Aspect 标注的类 |
| Join Point(连接点) | 程序执行过程中可以被拦截的点,Spring AOP 中只支持方法执行这一种 |
| Pointcut(切入点) | 定义「拦截哪些方法」的表达式,筛选出感兴趣的 Join Point |
| Advice(通知) | 在切入点处执行的逻辑,即「做什么」 |
| Weaving(织入) | 将切面应用到目标对象的过程,Spring AOP 在运行时织入 |
Advice 类型
| 注解 | 执行时机 |
|---|---|
@Before |
方法执行前 |
@AfterReturning |
方法正常返回后 |
@AfterThrowing |
方法抛出异常后 |
@After |
方法执行后(无论正常还是异常,类似 finally) |
@Around |
方法执行前后,最灵活,可以控制是否执行原方法 |
示例:记录接口耗时
@Aspect
@Component
public class TimingAspect {
// Pointcut:拦截 com.example.controller 包下所有类的所有方法
@Around("execution(* com.example.controller..*(..))")
public Object logTiming(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed(); // 执行原方法
} finally {
long elapsed = System.currentTimeMillis() - start;
System.out.printf("%s 耗时 %dms%n", pjp.getSignature(), elapsed);
}
}
}
不需要修改任何 Controller,所有接口自动被计时。
Pointcut 表达式常用语法
execution(修饰符? 返回类型 类路径? 方法名(参数) 异常?)
// 拦截指定包下所有方法(含子包)
execution(* com.example.service..*(..))
// 拦截特定注解标注的方法
@annotation(org.springframework.transaction.annotation.Transactional)
// 拦截特定类的特定方法
execution(* com.example.UserService.findById(Long))
多个切入点可以用 &&、||、! 组合。
*.* 只匹配当前包,不含子包;..(两个点)匹配当前包及所有子包:
// 只匹配 com.example.service 包(不含子包)
execution(* com.example.service.*.*(..))
// 匹配 com.example.service 包及所有子包
execution(* com.example.service..*(..))
实际项目中通常用两个点,否则 Service 类一旦移入子包切面就失效。
Spring AOP vs AspectJ
Spring AOP 基于动态代理,只能拦截 Spring Bean 的方法调用,且同一个 Bean 内部的方法互调不会触发 AOP(因为内部调用绕过了代理)。
AspectJ 是完整的 AOP 框架,在编译期或类加载期织入字节码,可以拦截任意方法、构造器、字段访问,功能更强但配置更复杂。Spring AOP 可以集成 AspectJ 的注解语法(@Aspect、@Pointcut 等),但底层仍是代理,不是字节码织入。
绝大多数业务场景 Spring AOP 已经够用,只有需要拦截非 Spring 管理对象、同类内部方法调用等场景才需要引入完整 AspectJ。
Spring AOP 与 AspectJ 混合使用
两者可以在同一应用中共存,常见的三种组合方式:
方式 1:Spring AOP 借用 AspectJ 注解(最常见,默认方式)
@Aspect、@Around、@Before 等注解来自 AspectJ,但织入由 Spring 代理完成。spring-boot-starter-aop 已自动引入 aspectjweaver,无需额外配置。
@Aspect // AspectJ 注解
@Component
public class MyAspect {
@Around("execution(* com.example.service..*(..))") // AspectJ Pointcut 语法
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// Spring 代理执行,不是 AspectJ 字节码织入
return pjp.proceed();
}
}
方式 2:启用 AspectJ LTW(类加载期织入)
AspectJ 真正接管织入,可拦截内部方法调用和非 Spring Bean。需加 JVM 参数:
java -javaagent:aspectjweaver.jar -jar app.jar
或在 Spring 配置中启用:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig { }
方式 3:两种切面并存
部分切面走 Spring AOP(代理),部分切面走 AspectJ 字节码织入,互不干扰。
| 场景 | 选择 |
|---|---|
| 只拦截 Spring Bean 方法 | Spring AOP(默认,无需额外配置) |
| 需要拦截内部调用 / 非 Spring 对象 | AspectJ LTW |
| 两种需求都有 | 可以共存,不冲突 |
内部调用问题
@Service
public class OrderService {
public void placeOrder() {
validate(); // ❌ 内部调用,AOP 不生效,validate() 上的切面被绕过
}
@Transactional
public void validate() { ... }
}
解决方法:通过 ApplicationContext 获取自身的代理对象,或拆分为两个 Bean。
在 Spring Web 开发中的具体应用
Spring 内置了大量基于 AOP 的功能,开发 Web 应用时几乎每个层次都在用:
Controller 层
| 场景 | 注解 / 机制 | 说明 |
|---|---|---|
| 权限校验 | @PreAuthorize("hasRole('ADMIN')") |
Spring Security 在方法执行前校验权限 |
| 限流 | @RateLimiter(Resilience4j) |
拦截方法,超过阈值直接拒绝 |
| 接口日志 | 自定义 @Around 切面 |
记录请求参数、响应结果、耗时 |
Service 层
| 场景 | 注解 / 机制 | 说明 |
|---|---|---|
| 事务管理 | @Transactional |
方法前开启事务,正常返回提交,异常回滚 |
| 缓存 | @Cacheable / @CacheEvict |
命中缓存直接返回,不执行方法体;或方法后清除缓存 |
| 熔断降级 | @CircuitBreaker(Resilience4j) |
故障率超阈值时自动降级 |
| 重试 | @Retryable(Spring Retry) |
方法抛出异常时自动重试 |
各层通用
| 场景 | 实现方式 |
|---|---|
| 慢方法监控 | @Around 切面,记录超过阈值的调用 |
| 异常统一处理 | @AfterThrowing 切面,上报异常监控 |
| 方法级参数校验 | @Validated + @Valid(底层也是 AOP) |
一个完整的调用链示意
HTTP 请求
→ Controller.findUser()
↕ Spring Security AOP(权限校验)
↕ 日志切面(记录入参)
→ Service.findUser()
↕ @Transactional AOP(开启事务)
↕ @Cacheable AOP(查缓存,命中则返回)
→ Repository.findById() ← 真正的数据库查询
↕ @Transactional AOP(提交事务)
↕ 日志切面(记录耗时)
→ 返回响应
可以看到,@Transactional、@PreAuthorize、@Cacheable 这些日常常用的注解,底层全都是 AOP。Spring 把这些通用能力封装成注解,让业务代码只需要贴一个注解,背后的切面由框架统一处理。
@Transactional
@Transactional 是 Spring 基于 AOP 封装的事务管理注解。Spring 在方法执行前开启数据库事务,正常返回时提交,抛出异常时回滚。
典型使用场景
1. 多步写操作必须原子完成
@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
accountDao.deduct(fromId, amount); // 扣款
accountDao.add(toId, amount); // 入账
// 任一步失败,两步都回滚,不会出现"扣了款但没入账"
}
2. 批量写入
@Transactional
public void batchInsert(List<Order> orders) {
for (Order order : orders) {
orderDao.insert(order);
}
// 全部成功才提交;任何一条失败,全部回滚
}
3. 读写组合(防幻读)
@Transactional
public void deductStock(Long productId, int qty) {
Product p = productDao.findById(productId); // 读
if (p.getStock() < qty) throw new RuntimeException("库存不足");
productDao.deductStock(productId, qty); // 写
// 同一事务内,读到的库存和写时的库存一致
}
常用参数
| 参数 | 说明 |
|---|---|
rollbackFor |
指定哪些异常触发回滚,默认只回滚 RuntimeException 和 Error |
noRollbackFor |
指定哪些异常不回滚 |
propagation |
事务传播行为,默认 REQUIRED(没有事务就新建,有就加入) |
readOnly = true |
只读事务,数据库可做优化(如跳过脏检查),适合纯查询 |
timeout |
超时秒数,超时自动回滚 |
常见的坑
坑 1:受检异常不回滚
@Transactional // 默认只回滚 RuntimeException
public void save() throws IOException {
fileService.write(); // IOException 是受检异常,不触发回滚!
dao.insert(record);
}
// 修复:
@Transactional(rollbackFor = Exception.class)
坑 2:内部调用失效
@Service
public class OrderService {
public void placeOrder() {
saveOrder(); // ❌ 内部调用,@Transactional 不生效
}
@Transactional
public void saveOrder() { ... }
}
原因同 AOP 内部调用问题:内部调用绕过了代理对象。
坑 3:private 方法无效
@Transactional // ❌ 无效,代理无法重写 private 方法
private void doSave() { ... }
@Transactional 应标注在 public 方法上。