Spring 核心概念深入:IoC 与 AOP
本文是 Spring 学习文档的扩展篇,针对前端开发者深入讲解 IoC 和 AOP 这两个最核心的概念。理解了这两章,再看 Spring 的任何高级特性都会豁然开朗。
目录
第一部分:IoC 深入
- [从一个真实场景开始理解 IoC](#从一个真实场景开始理解 IoC "#1-%E4%BB%8E%E4%B8%80%E4%B8%AA%E7%9C%9F%E5%AE%9E%E5%9C%BA%E6%99%AF%E5%BC%80%E5%A7%8B%E7%90%86%E8%A7%A3-ioc")
- [Bean:Spring 容器里的对象](#Bean:Spring 容器里的对象 "#2-beanspring-%E5%AE%B9%E5%99%A8%E9%87%8C%E7%9A%84%E5%AF%B9%E8%B1%A1")
- [Bean 的生命周期](#Bean 的生命周期 "#3-bean-%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F")
- [Bean 的作用域(Scope)](#Bean 的作用域(Scope) "#4-bean-%E7%9A%84%E4%BD%9C%E7%94%A8%E5%9F%9Fscope")
- 依赖注入的三种方式深入对比
- 循环依赖问题
- [@Bean、@Component、@Configuration 的本质区别](#@Bean、@Component、@Configuration 的本质区别 "#7-beancomponentconfiguration-%E7%9A%84%E6%9C%AC%E8%B4%A8%E5%8C%BA%E5%88%AB")
- [条件装配与 Profile](#条件装配与 Profile "#8-%E6%9D%A1%E4%BB%B6%E8%A3%85%E9%85%8D%E4%B8%8E-profile")
- [IoC 容器的内部工作流程](#IoC 容器的内部工作流程 "#9-ioc-%E5%AE%B9%E5%99%A8%E7%9A%84%E5%86%85%E9%83%A8%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B")
- [IoC 常见坑与最佳实践](#IoC 常见坑与最佳实践 "#10-ioc-%E5%B8%B8%E8%A7%81%E5%9D%91%E4%B8%8E%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5")
第二部分:AOP 深入
- [AOP 解决的根本问题](#AOP 解决的根本问题 "#11-aop-%E8%A7%A3%E5%86%B3%E7%9A%84%E6%A0%B9%E6%9C%AC%E9%97%AE%E9%A2%98")
- [AOP 核心术语全解析](#AOP 核心术语全解析 "#12-aop-%E6%A0%B8%E5%BF%83%E6%9C%AF%E8%AF%AD%E5%85%A8%E8%A7%A3%E6%9E%90")
- 五种通知(Advice)类型详解
- [切入点表达式(Pointcut Expression)](#切入点表达式(Pointcut Expression) "#14-%E5%88%87%E5%85%A5%E7%82%B9%E8%A1%A8%E8%BE%BE%E5%BC%8Fpointcut-expression")
- [Spring AOP 的实现原理:动态代理](#Spring AOP 的实现原理:动态代理 "#15-spring-aop-%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86")
- [JDK 代理 vs CGLIB 代理](#JDK 代理 vs CGLIB 代理 "#16-jdk-%E4%BB%A3%E7%90%86-vs-cglib-%E4%BB%A3%E7%90%86")
- [AOP 失效的常见场景](#AOP 失效的常见场景 "#17-aop-%E5%A4%B1%E6%95%88%E7%9A%84%E5%B8%B8%E8%A7%81%E5%9C%BA%E6%99%AF")
- [@Transactional 的工作原理(AOP 实战)](#@Transactional 的工作原理(AOP 实战) "#18-transactional-%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86aop-%E5%AE%9E%E6%88%98")
- [手写一个自定义注解 + AOP](#手写一个自定义注解 + AOP "#19-%E6%89%8B%E5%86%99%E4%B8%80%E4%B8%AA%E8%87%AA%E5%AE%9A%E4%B9%89%E6%B3%A8%E8%A7%A3--aop")
- [AOP 性能与使用建议](#AOP 性能与使用建议 "#20-aop-%E6%80%A7%E8%83%BD%E4%B8%8E%E4%BD%BF%E7%94%A8%E5%BB%BA%E8%AE%AE")
第一部分:IoC 深入
1. 从一个真实场景开始理解 IoC
1.1 没有 IoC 的世界
假设你在做一个订单系统,订单服务依赖:用户服务、库存服务、支付服务、短信服务。
java
public class OrderService {
private UserService userService;
private InventoryService inventoryService;
private PaymentService paymentService;
private SmsService smsService;
public OrderService() {
// 自己创建所有依赖
this.userService = new UserService();
this.inventoryService = new InventoryService();
this.paymentService = new PaymentService(
new PaymentGateway("https://pay.example.com", "api-key-xxx")
);
this.smsService = new SmsService(
new SmsProvider("aliyun", "secret-xxx")
);
}
}
问题暴露:
- 耦合死了 :
OrderService必须知道每个依赖怎么创建,包括它们各自的依赖(PaymentGateway、SmsProvider)。 - 配置硬编码:URL、API Key 写在代码里,改一个要重新编译。
- 难以测试 :单元测试时想 Mock
PaymentService,但它是在构造器里硬new的,根本换不掉。 - 重复创建 :另一个
RefundService也需要PaymentService,又得new一份,浪费资源。 - 缺乏统一管理 :应用关闭时谁来释放
SmsProvider的连接池?
1.2 IoC 怎么解决
IoC 的英文是 Inversion of Control(控制反转),指的是:
对象的创建和依赖关系的维护,从"自己控制"反转为"由容器控制"。
类比前端:你写 React 组件时,不会自己手动 new 一个 Redux Store,而是用 <Provider> 包起来,组件通过 useSelector 获取------Store 的创建、维护由"框架/容器"负责。Spring 容器扮演的就是类似角色,但管理的是后端的所有"服务对象"。
用了 Spring 的写法:
java
@Service
public class OrderService {
private final UserService userService;
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final SmsService smsService;
// 构造器声明依赖,Spring 自动注入
public OrderService(UserService userService,
InventoryService inventoryService,
PaymentService paymentService,
SmsService smsService) {
this.userService = userService;
this.inventoryService = inventoryService;
this.paymentService = paymentService;
this.smsService = smsService;
}
}
OrderService 只声明"我需要什么",不关心这些依赖怎么来的。这就是控制反转。
1.3 IoC 与 DI 的关系
很多人把 IoC 和 DI 混着说,其实有微妙区别:
- IoC(控制反转):一种设计思想------把对象创建权交给容器。
- DI(依赖注入,Dependency Injection):实现 IoC 的一种具体手段------通过构造器/Setter/字段把依赖传进来。
简单说:IoC 是"是什么",DI 是"怎么做"。 平时讨论时不必太纠结。
2. Bean:Spring 容器里的对象
2.1 什么是 Bean?
被 Spring 容器管理的对象就叫 Bean。可以理解为"注册到 Spring 仓库里的对象"。
java
@Component
public class UserService { } // UserService 成为一个 Bean
Spring 启动时会扫描所有带特定注解(@Component、@Service、@Repository、@Controller 等)的类,创建它们的实例并放入容器。
2.2 如何"注册" Bean
有三种主要方式:
方式 1:注解扫描(最常用)
java
@Service
public class UserService { }
方式 2:在 @Configuration 类中用 @Bean 声明
java
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate(); // 这个 RestTemplate 成为容器里的一个 Bean
}
@Bean
public PaymentGateway paymentGateway(@Value("${pay.url}") String url) {
return new PaymentGateway(url);
}
}
方式 3:XML 配置(老项目里能看到,新项目几乎不用了)
xml
<bean id="userService" class="com.example.UserService"/>
2.3 什么时候用哪种?
| 场景 | 推荐方式 |
|---|---|
| 自己写的业务类 | @Service / @Component |
| 第三方库的类(你改不了它的源码) | @Bean 方法注册 |
| 需要复杂初始化逻辑 | @Bean 方法注册 |
| 老项目维护 | XML(按现有风格走) |
例子:你想用 RestTemplate 发 HTTP 请求,它是 Spring 提供的类,不能给它加 @Component,就只能用 @Bean 注册:
java
@Configuration
public class WebConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate template = new RestTemplate();
template.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
return template;
}
}
3. Bean 的生命周期
理解生命周期对调试问题、合理使用扩展点都很关键。
3.1 完整生命周期流程
markdown
1. 实例化(调用构造器)
↓
2. 属性赋值(依赖注入)
↓
3. 调用 Aware 接口(BeanNameAware、ApplicationContextAware 等)
↓
4. BeanPostProcessor.postProcessBeforeInitialization()
↓
5. @PostConstruct 注解的方法
↓
6. InitializingBean.afterPropertiesSet()
↓
7. 自定义的 init-method
↓
8. BeanPostProcessor.postProcessAfterInitialization() ← AOP 代理在这里产生
↓
9. Bean 可用,业务正常使用
↓
10. 容器关闭时:@PreDestroy
↓
11. DisposableBean.destroy()
↓
12. 自定义的 destroy-method
3.2 常用的两个钩子:@PostConstruct 和 @PreDestroy
java
@Service
public class CacheService {
private Map<String, Object> cache;
@PostConstruct // 对象创建并完成依赖注入后立即执行
public void init() {
cache = new ConcurrentHashMap<>();
System.out.println("缓存初始化完毕");
}
@PreDestroy // 容器关闭前执行
public void cleanup() {
cache.clear();
System.out.println("缓存已清理");
}
}
使用场景:
@PostConstruct:初始化数据、预热缓存、注册定时任务@PreDestroy:关闭连接池、释放资源、保存状态
3.3 为什么不在构造器里做初始化?
java
@Service
public class UserService {
private final UserRepository repo;
private Map<Long, User> cache;
public UserService(UserRepository repo) {
this.repo = repo;
this.cache = repo.findAll().stream() // ⚠️ 危险!
.collect(Collectors.toMap(User::getId, u -> u));
}
}
风险:构造器执行时,可能有些 Spring 的处理还没完成(比如 AOP 代理还没生效、事务还没启用),调用依赖的方法可能行为异常。
正确做法 用 @PostConstruct:
java
@PostConstruct
public void warmupCache() {
this.cache = repo.findAll().stream()
.collect(Collectors.toMap(User::getId, u -> u));
}
4. Bean 的作用域(Scope)
4.1 五种作用域
| 作用域 | 说明 | 使用频率 |
|---|---|---|
singleton |
单例(默认):整个容器只有一个实例 | ⭐⭐⭐⭐⭐ |
prototype |
每次注入/获取都创建一个新实例 | ⭐⭐ |
request |
每个 HTTP 请求一个实例(Web 场景) | ⭐ |
session |
每个 HTTP 会话一个实例(Web 场景) | ⭐ |
application |
整个 ServletContext 一个实例 | ⭐ |
java
@Service
@Scope("prototype")
public class ShoppingCart { }
4.2 Singleton 的注意事项
默认所有 Bean 都是单例,这意味着:
java
@Service
public class UserService {
private int count = 0; // ⚠️ 所有请求共享这个变量!
public void incr() {
count++; // 多线程下会有问题
}
}
Spring Bean 默认不是线程安全的------它只负责创建和管理,并发问题需要你自己处理。在 Service 里要避免使用可变的成员变量。
4.3 Singleton 注入 Prototype 的陷阱
java
@Service // singleton
public class UserService {
@Autowired
private ShoppingCart cart; // prototype,但只注入一次!
}
UserService 是单例,只创建一次,所以 cart 也只注入一次------即使你声明它是 prototype,每次用的还是同一个实例!
解决方案 1:用 ObjectProvider
java
@Service
public class UserService {
@Autowired
private ObjectProvider<ShoppingCart> cartProvider;
public void process() {
ShoppingCart cart = cartProvider.getObject(); // 每次都是新的
}
}
解决方案 2:用 @Lookup 注解
java
@Service
public abstract class UserService {
@Lookup
public abstract ShoppingCart getCart(); // Spring 会重写这个方法
public void process() {
ShoppingCart cart = getCart(); // 每次调用都是新实例
}
}
5. 依赖注入的三种方式深入对比
5.1 构造器注入(强烈推荐)
java
@Service
public class OrderService {
private final UserService userService;
private final PaymentService paymentService;
public OrderService(UserService userService, PaymentService paymentService) {
this.userService = userService;
this.paymentService = paymentService;
}
}
优点:
- 依赖关系一目了然,看构造器就知道这个类需要什么
- 可以用
final修饰,对象创建后依赖不可变,线程安全 - 不依赖 Spring,纯 Java 也能测试(直接 new 一个传 Mock 进去即可)
- 强制依赖检查:少传一个就编译报错
配合 Lombok 进一步简化:
java
@Service
@RequiredArgsConstructor // 自动为所有 final 字段生成构造器
public class OrderService {
private final UserService userService;
private final PaymentService paymentService;
}
5.2 Setter 注入(可选依赖时使用)
java
@Service
public class OrderService {
private NotificationService notificationService;
@Autowired(required = false) // 可选依赖
public void setNotificationService(NotificationService service) {
this.notificationService = service;
}
}
适用场景:依赖是可选的,没有也能工作。
5.3 字段注入(不推荐但常见)
java
@Service
public class OrderService {
@Autowired
private UserService userService;
}
缺点:
- 字段不能是
final - 单元测试时需要反射或 Spring 工具来注入
- 容易写出过长的"上帝类"(依赖太多也没感觉)
- 隐式依赖,看不到构造器无法快速了解类的依赖结构
唯一优点:写起来短。
5.4 三种方式对比表
| 维度 | 构造器 | Setter | 字段 |
|---|---|---|---|
| 不可变依赖(final) | ✅ | ❌ | ❌ |
| 强制依赖 | ✅ | ❌ | ❌ |
| 可选依赖 | ❌ | ✅ | ❌ |
| 易于测试 | ⭐⭐⭐ | ⭐⭐ | ⭐ |
| 循环依赖支持 | ❌ | ✅ | ✅ |
| 代码量 | 多 | 多 | 少 |
结论:默认用构造器,必要时用 Setter,尽量不用字段注入。
6. 循环依赖问题
6.1 什么是循环依赖
java
@Service
public class A {
@Autowired
private B b;
}
@Service
public class B {
@Autowired
private A a;
}
A 依赖 B,B 又依赖 A,形成循环。
6.2 Spring 怎么处理
Spring 通过 三级缓存 解决基于 Setter/字段的循环依赖:
一级缓存(singletonObjects):完整的 Bean
二级缓存(earlySingletonObjects):早期暴露的 Bean(已实例化但未填充属性)
三级缓存(singletonFactories):Bean 工厂
简化流程:
- 创建 A,A 实例化后(属性未填充),把 A 的工厂放入三级缓存
- 给 A 注入属性时发现需要 B,去创建 B
- B 实例化后,要注入 A,去三级缓存拿到 A 的"半成品"
- B 完成创建,放入一级缓存
- 回到 A 的注入流程,把完整的 B 注入给 A
- A 完成创建
6.3 构造器循环依赖无法解决
java
@Service
public class A {
public A(B b) { } // 构造器
}
@Service
public class B {
public B(A a) { }
}
启动报错:BeanCurrentlyInCreationException。
原因:A 还没实例化完成(构造器没执行完),就要 B;B 又需要完整的 A------死锁。
6.4 真正的解决方案:重构!
循环依赖通常是设计问题。常见做法:
方法 1:抽取共同逻辑到第三个类
java
@Service
public class A {
private final CommonService common;
}
@Service
public class B {
private final CommonService common;
}
@Service
public class CommonService { } // 提取共同依赖
方法 2:用事件解耦
java
@Service
public class A {
private final ApplicationEventPublisher publisher;
public void doSomething() {
publisher.publishEvent(new SomethingDoneEvent(...));
}
}
@Service
public class B {
@EventListener
public void onSomething(SomethingDoneEvent event) {
// 不再直接依赖 A
}
}
方法 3:用 @Lazy 延迟注入(治标不治本)
java
@Service
public class A {
public A(@Lazy B b) { // B 用到时才真正初始化
this.b = b;
}
}
7. @Bean、@Component、@Configuration 的本质区别
这是新人常混淆的概念。
7.1 @Component(及其变体)
@Component、@Service、@Repository、@Controller 都是同一类东西------类级别的注解,告诉 Spring:"扫描到我时,把我创建为一个 Bean。"
java
@Service // 类上加注解
public class UserService { }
适用:你能修改源码的、自己写的类。
7.2 @Bean
@Bean 是方法级别 的注解,写在 @Configuration 类的方法上,由方法返回一个对象,这个对象成为 Bean。
java
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
适用:你不能修改源码的类(第三方库),或者需要复杂创建逻辑的对象。
7.3 @Configuration 的特殊之处
java
@Configuration
public class AppConfig {
@Bean
public A a() {
return new A();
}
@Bean
public B b() {
return new B(a()); // 调用 a() 方法
}
}
问题 :如果别处也调用了 a(),是不是创建了两个 A?
答案 :不是!@Configuration 类会被 Spring CGLIB 增强,方法调用会被拦截:如果 a() 已经创建过,就从容器直接返回,保证单例。
7.4 @Configuration vs @Component
如果把上例中的 @Configuration 换成 @Component:
java
@Component // 注意:不是 @Configuration
public class AppConfig {
@Bean
public A a() { return new A(); }
@Bean
public B b() { return new B(a()); } // 这里的 a() 是直接调用!
}
会真的创建两个 A:一个由 Spring 通过 a() 方法创建并放入容器,另一个由 b() 中的 a() 直接调用 new 出来。
记住 :写配置类、需要在 @Bean 方法间互相调用,必须用 @Configuration。
8. 条件装配与 Profile
8.1 @Profile:不同环境用不同 Bean
java
@Configuration
public class DataSourceConfig {
@Bean
@Profile("dev")
public DataSource devDataSource() {
// H2 内存数据库
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
// 真实 MySQL
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://prod-db:3306/app");
return ds;
}
}
启动时通过 spring.profiles.active=dev 选择激活的环境。
8.2 @Conditional 系列
更细粒度的条件控制:
java
@Bean
@ConditionalOnProperty(name = "feature.cache.enabled", havingValue = "true")
public CacheManager cacheManager() {
return new CaffeineCacheManager();
}
@Bean
@ConditionalOnMissingBean(CacheManager.class)
public CacheManager defaultCacheManager() {
return new ConcurrentMapCacheManager();
}
@Bean
@ConditionalOnClass(name = "redis.clients.jedis.Jedis")
public RedisTemplate redisTemplate() { ... }
这就是 Spring Boot 自动配置的核心 ------spring-boot-autoconfigure 内部就是一堆 @ConditionalOnXxx 组合,判断 classpath 上有什么、配置文件里有什么,自动决定加载哪些 Bean。
9. IoC 容器的内部工作流程
9.1 容器启动的简化流程
markdown
1. 创建 ApplicationContext
↓
2. 加载配置(注解扫描、@Configuration 类、XML 等)
↓
3. 构建 BeanDefinition(Bean 的"蓝图",包含类名、作用域、依赖等元信息)
↓
4. 注册到 BeanDefinitionRegistry
↓
5. BeanFactoryPostProcessor 处理(可以修改 BeanDefinition)
↓
6. 实例化所有非懒加载的单例 Bean
├─ 解析依赖关系(按拓扑排序)
├─ 调用构造器创建实例
├─ 填充属性(依赖注入)
├─ 执行 BeanPostProcessor(包括 AOP 代理生成)
└─ 执行初始化方法
↓
7. 容器就绪,可以使用
9.2 核心组件
| 组件 | 职责 |
|---|---|
BeanDefinition |
Bean 的元数据(类名、作用域、依赖等) |
BeanFactory |
IoC 容器的基础接口,负责管理 Bean |
ApplicationContext |
高级容器,扩展了 BeanFactory,提供事件、国际化等 |
BeanPostProcessor |
在 Bean 初始化前后插入逻辑(AOP 就靠它) |
BeanFactoryPostProcessor |
在 BeanDefinition 阶段插入逻辑 |
9.3 类比前端
如果用前端开发的视角强行类比:
| Spring 概念 | 前端类比 |
|---|---|
| BeanDefinition | 组件的 schema/配置 |
| BeanFactory | 渲染器 |
| ApplicationContext | 整个 App 实例 |
| BeanPostProcessor | 类似 Vite 的插件、Webpack 的 loader |
| AOP 代理 | 高阶组件包装 |
10. IoC 常见坑与最佳实践
10.1 同类型多个 Bean 的歧义
java
public interface PaymentService { }
@Service
public class AlipayService implements PaymentService { }
@Service
public class WechatPayService implements PaymentService { }
@Service
public class OrderService {
@Autowired
private PaymentService paymentService; // ⚠️ 报错:找到两个匹配的 Bean
}
解决方案:
方案 1:用 @Qualifier 指定
java
@Autowired
@Qualifier("alipayService")
private PaymentService paymentService;
方案 2:用 @Primary 标记默认
java
@Service
@Primary
public class AlipayService implements PaymentService { }
方案 3:直接注入 List 或 Map
java
@Autowired
private List<PaymentService> allPayments; // 注入所有实现
@Autowired
private Map<String, PaymentService> paymentMap; // key 是 Bean 名
后一种特别适合策略模式:
java
public Order pay(String channel, BigDecimal amount) {
PaymentService service = paymentMap.get(channel + "Service");
return service.pay(amount);
}
10.2 在静态方法中获取 Bean
java
public class Utils {
public static void doSomething() {
UserService service = ???; // 怎么拿到 Spring Bean?
}
}
反模式,但有时不得不用。可以做一个工具类:
java
@Component
public class SpringContextHolder implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext ctx) {
SpringContextHolder.context = ctx;
}
public static <T> T getBean(Class<T> clazz) {
return context.getBean(clazz);
}
}
// 使用
UserService service = SpringContextHolder.getBean(UserService.class);
但这是绕过 Spring 的依赖管理,能不用就不用。
10.3 最佳实践清单
✅ DO(推荐做法):
- 用构造器注入 + final 字段
- 配合 Lombok 的
@RequiredArgsConstructor - 面向接口编程(Service 写接口 + 实现类)
- 同类型多 Bean 时优先用
@Qualifier或注入 Map/List - 使用
@Configuration而不是@Component来定义配置类 - 复杂初始化用
@PostConstruct
❌ DON'T(应避免):
- 在 Service 里用可变成员变量
- 在构造器里调用复杂的依赖方法
- 用字段注入(特别是 Service 层)
- 在普通方法里
new一个本应该是 Bean 的对象 - 滥用
@Lazy来"治"循环依赖 - 在静态方法里通过
SpringContextHolder取 Bean(除非真的没办法)
第二部分:AOP 深入
11. AOP 解决的根本问题
11.1 "横切关注点"是什么?
软件里有两类关注点:
- 核心关注点:业务逻辑(下单、支付、查询)
- 横切关注点:贯穿多个业务的通用功能(日志、事务、缓存、权限、监控)
问题:横切关注点会污染业务代码。
java
public Order createOrder(OrderRequest req) {
// 横切:权限校验
if (!SecurityContext.hasRole("USER")) throw new ForbiddenException();
// 横切:日志
log.info("创建订单开始: userId={}", req.getUserId());
long start = System.currentTimeMillis();
// 横切:事务
Transaction tx = txManager.begin();
try {
// ↓↓↓ 真正的业务逻辑(只占 3 行)↓↓↓
Order order = orderRepository.save(buildOrder(req));
inventoryService.decrease(req.getItems());
paymentService.charge(order.getId(), req.getAmount());
// ↑↑↑ 业务逻辑结束 ↑↑↑
tx.commit();
log.info("创建订单成功, 耗时: {}ms", System.currentTimeMillis() - start);
return order;
} catch (Exception e) {
tx.rollback();
log.error("创建订单失败", e);
throw e;
}
}
20 行代码里,业务逻辑只占 3 行,其余都是横切代码。每个方法都这样写,会变成噩梦。
11.2 AOP 的目标
把横切逻辑抽出来,让业务代码变成:
java
@Transactional
@LogExecutionTime
@PreAuthorize("hasRole('USER')")
public Order createOrder(OrderRequest req) {
Order order = orderRepository.save(buildOrder(req));
inventoryService.decrease(req.getItems());
paymentService.charge(order.getId(), req.getAmount());
return order;
}
核心思想:用注解声明意图,框架在背后帮你"织入"实际逻辑。
类比前端:类似 Vue 的指令(v-loading、v-permission)或 React 的高阶组件------把通用逻辑变成"装饰",业务代码保持简洁。
12. AOP 核心术语全解析
AOP 有自己的一套术语,初看晕,掌握后很清晰。
12.1 核心术语
| 术语 | 英文 | 通俗解释 |
|---|---|---|
| 切面 | Aspect | 横切关注点的"模块化",一个 @Aspect 类就是一个切面 |
| 连接点 | Join Point | 程序执行的某个点(在 Spring AOP 中就是方法调用) |
| 切入点 | Pointcut | 一组连接点的描述("匹配 service 包下所有方法") |
| 通知 | Advice | 在切入点执行的具体逻辑("方法执行前打日志") |
| 织入 | Weaving | 把通知应用到目标方法的过程 |
| 目标对象 | Target | 被代理的原始对象 |
| 代理对象 | Proxy | Spring 生成的、包含切面逻辑的代理 |
12.2 用图理解
markdown
目标方法(业务代码)
↑
┌─────┴─────┐
│ 织入 │
└─────┬─────┘
│
┌─────────┴─────────┐
│ Advice │ ← 在哪里执行什么逻辑
│ ↑ ↑ │
│ │ │ │
│ Pointcut │ │ ← 匹配哪些方法
│ │ │
└─────────────────┴─┘
Aspect(切面)
12.3 代码对应
java
@Aspect // 这个类是个切面
@Component
public class LoggingAspect {
// ↓ 切入点:匹配 service 包下所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
// ↓ 通知:在匹配的方法执行前打日志
@Before("serviceMethods()")
public void logBefore(JoinPoint jp) { // JoinPoint 就是当前连接点信息
System.out.println("调用方法: " + jp.getSignature().getName());
}
}
13. 五种通知(Advice)类型详解
13.1 @Before:方法执行前
java
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint jp) {
System.out.println("即将执行: " + jp.getSignature().getName());
Object[] args = jp.getArgs(); // 获取参数
}
应用:参数校验、权限检查、记录请求日志。
13.2 @After:方法执行后(无论成功失败)
java
@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint jp) {
System.out.println("执行完毕: " + jp.getSignature().getName());
}
应用:清理资源、记录方法结束。
13.3 @AfterReturning:方法成功返回后
java
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))",
returning = "result")
public void logResult(JoinPoint jp, Object result) {
System.out.println("方法返回: " + result);
}
应用:审计日志、缓存结果。
13.4 @AfterThrowing:方法抛异常时
java
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))",
throwing = "ex")
public void logException(JoinPoint jp, Exception ex) {
System.err.println("方法异常: " + ex.getMessage());
}
应用:异常告警、错误监控。
13.5 @Around:环绕通知(最强大)
可以同时控制方法的前后行为,甚至决定要不要执行原方法。
java
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 前置逻辑
long start = System.currentTimeMillis();
System.out.println("方法开始: " + pjp.getSignature().getName());
Object result;
try {
result = pjp.proceed(); // 执行原方法(可以修改参数:pjp.proceed(newArgs))
} catch (Throwable t) {
System.err.println("异常: " + t.getMessage());
throw t;
}
// 后置逻辑
long elapsed = System.currentTimeMillis() - start;
System.out.println("耗时: " + elapsed + "ms");
return result; // 可以修改返回值
}
应用:性能监控、缓存、重试、降级。
13.6 五种通知执行顺序
less
正常执行:
@Around(前置部分)
@Before
原方法
@After
@AfterReturning
@Around(后置部分)
异常时:
@Around(前置部分)
@Before
原方法(抛异常)
@After
@AfterThrowing
14. 切入点表达式(Pointcut Expression)
14.1 execution 表达式语法
css
execution([修饰符] 返回值类型 [包名.]类名.方法名(参数列表) [异常])
例子:
java
// 匹配 com.example.service 包下所有类的所有方法
execution(* com.example.service.*.*(..))
// 匹配 com.example.service 及其子包下所有类的所有方法
execution(* com.example.service..*.*(..))
// 匹配返回 User 类型、以 find 开头的方法
execution(User com.example.service.*.find*(..))
// 匹配带一个 Long 参数的方法
execution(* com.example.service.*.*(Long))
// 匹配 public 方法
execution(public * com.example.service.*.*(..))
通配符:
*:匹配任意一个内容(不含.)..:匹配任意多个内容+:匹配类及其子类
14.2 其他切入点指示符
java
// within:匹配类
@Pointcut("within(com.example.service.*)")
// @annotation:匹配带特定注解的方法
@Pointcut("@annotation(com.example.LogExecutionTime)")
// @within:匹配带特定注解的类的所有方法
@Pointcut("@within(org.springframework.stereotype.Service)")
// args:匹配参数类型
@Pointcut("args(Long, ..)") // 第一个参数是 Long
// bean:按 Bean 名匹配
@Pointcut("bean(*Service)") // 所有以 Service 结尾的 Bean
14.3 组合表达式
可以用 &&、||、! 组合:
java
@Pointcut("execution(* com.example.service.*.*(..)) && @annotation(com.example.Log)")
public void serviceWithLog() {}
@Pointcut("within(com.example.service.*) && !execution(* *.get*(..))")
public void serviceNonGetters() {}
14.4 复用切入点
java
@Aspect
@Component
public class MyAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
@Pointcut("@annotation(com.example.LogExecutionTime)")
public void logged() {}
@Before("serviceLayer() && logged()") // 复用上面定义的切入点
public void doSomething() { }
}
15. Spring AOP 的实现原理:动态代理
理解原理对于排查"AOP 怎么不生效了"这类问题至关重要。
15.1 核心思想:代理模式
AOP 的本质是 代理模式:
markdown
原本:调用方 ─→ 目标对象
代理:调用方 ─→ 代理对象 ─→ 目标对象
↑
插入切面逻辑
15.2 生活类比
你(调用方)想找律师(目标对象)咨询,但中间隔了一个律所前台(代理):
- 进门:前台帮你记录预约信息(前置通知)
- 咨询:前台带你见律师(执行目标方法)
- 离开:前台问你满意度(后置通知)
- 律师骂你:前台道歉(异常通知)
你以为直接跟律师交流,其实所有动作都经过前台。Spring 的代理就是这种"前台"。
15.3 一次方法调用的完整流程
java
@Service
public class UserService {
public User findById(Long id) {
return repo.findById(id);
}
}
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Before");
Object result = pjp.proceed();
System.out.println("After");
return result;
}
}
实际容器里注入的不是 UserService 本身,而是一个代理类:
java
// 伪代码:Spring 在运行时生成的代理类
public class UserService$$Proxy extends UserService {
private final UserService target;
private final LoggingAspect aspect;
@Override
public User findById(Long id) {
ProceedingJoinPoint pjp = new MethodInvocationProceedingJoinPoint(...) {
public Object proceed() {
return target.findById(id); // 调用原方法
}
};
return (User) aspect.around(pjp); // 调用切面逻辑
}
}
16. JDK 代理 vs CGLIB 代理
Spring AOP 有两种代理实现方式。
16.1 JDK 动态代理
前提:目标类必须实现接口。
java
public interface UserService {
User findById(Long id);
}
@Service
public class UserServiceImpl implements UserService { ... }
JDK 代理生成一个实现 UserService 接口的代理类,不能代理类自身。
16.2 CGLIB 代理
前提:通过继承生成子类来代理。
java
@Service
public class UserService { // 没有接口
public User findById(Long id) { ... }
}
CGLIB 生成一个 UserService 的子类作为代理。
限制:
- 目标类不能是
final(无法被继承) - 目标方法不能是
final、static、private(无法被重写)
16.3 Spring 怎么选
- 默认:目标类实现了接口 → 用 JDK 代理;没实现接口 → 用 CGLIB
- 强制用 CGLIB:
@EnableAspectJAutoProxy(proxyTargetClass = true)
Spring Boot 2.x 之后默认全部用 CGLIB(兼容性更好)。
16.4 两种代理对比
| 维度 | JDK 代理 | CGLIB 代理 |
|---|---|---|
| 要求 | 必须实现接口 | 不能是 final 类 |
| 实现方式 | 反射 | 字节码生成 |
| 创建速度 | 快 | 稍慢 |
| 调用速度 | 较慢(反射) | 较快(直接调用) |
| 适用场景 | 面向接口编程 | 通用 |
17. AOP 失效的常见场景
这是新手最容易踩的坑,必须搞清楚。
17.1 同类内部方法调用
java
@Service
public class UserService {
public void methodA() {
this.methodB(); // ⚠️ 直接调用,绕过代理!
}
@Transactional
public void methodB() {
// 事务不会生效!
}
}
原因 :this 是目标对象本身,不是代理对象。代理只在外部调用时起作用。
解决方案 1:注入自身
java
@Service
public class UserService {
@Lazy // 避免循环依赖
@Autowired
private UserService self;
public void methodA() {
self.methodB(); // 通过代理调用
}
@Transactional
public void methodB() { }
}
解决方案 2:通过 ApplicationContext 拿到代理
java
@Service
public class UserService implements ApplicationContextAware {
private UserService self;
@Override
public void setApplicationContext(ApplicationContext ctx) {
this.self = ctx.getBean(UserService.class);
}
public void methodA() {
self.methodB();
}
}
解决方案 3:使用 AopContext(需开启 exposeProxy)
java
@EnableAspectJAutoProxy(exposeProxy = true)
// ...
public void methodA() {
((UserService) AopContext.currentProxy()).methodB();
}
最佳方案:重新设计------把 methodB 放到另一个 Service 里。
17.2 方法不是 public
java
@Service
public class UserService {
@Transactional
private void doSomething() { } // ⚠️ private 无效
}
Spring AOP 默认只代理 public 方法。
17.3 final 类或 final 方法
java
@Service
public final class UserService { // ⚠️ CGLIB 无法继承
@Transactional
public void doSomething() { }
}
17.4 自己 new 出来的对象
java
@Service
public class UserService {
@Transactional
public void doSomething() { }
}
// 在某处
UserService service = new UserService();
service.doSomething(); // ⚠️ 这不是 Spring 管理的对象,没有代理,事务不生效
17.5 异步方法 + 事务
java
@Service
public class UserService {
@Async
@Transactional
public void doAsync() { } // 行为可能与预期不符
}
@Async 和 @Transactional 都是 AOP 代理,叠加使用要小心顺序问题。
17.6 静态方法
java
@Service
public class UserService {
@Transactional
public static void doSomething() { } // ⚠️ 静态方法无法被代理
}
17.7 一个总结性的判断流程
遇到 AOP 不生效,按这个顺序检查:
markdown
1. 对象是不是从 Spring 容器拿的?(不是自己 new 的)
↓ 是
2. 方法是不是 public?
↓ 是
3. 是不是从外部调用的?(不是同类内部 this 调用)
↓ 是
4. 类/方法是不是 final?(不是)
↓ 否
5. 切入点表达式写对了吗?
↓ 对
6. 应该能正常工作
18. @Transactional 的工作原理(AOP 实战)
@Transactional 是 Spring 中最常用的 AOP 应用,了解它的工作原理能加深对 AOP 的理解。
18.1 表面用法
java
@Service
public class OrderService {
@Transactional
public void createOrder(OrderRequest req) {
orderRepo.save(...);
inventoryRepo.decrease(...);
// 如果这里抛异常,前面的操作会回滚
}
}
18.2 背后发生了什么
Spring 启动时:
- 扫描到
@Transactional注解 - 为
OrderService生成代理对象 - 代理对象的
createOrder方法被增强为:
java
// 伪代码
public void createOrder(OrderRequest req) {
TransactionStatus tx = txManager.getTransaction(...); // 开启事务
try {
target.createOrder(req); // 调用原方法
txManager.commit(tx); // 提交事务
} catch (RuntimeException e) {
txManager.rollback(tx); // 回滚事务
throw e;
}
}
18.3 @Transactional 的常用属性
java
@Transactional(
propagation = Propagation.REQUIRED, // 传播行为
isolation = Isolation.READ_COMMITTED, // 隔离级别
timeout = 30, // 超时时间(秒)
readOnly = false, // 是否只读
rollbackFor = Exception.class // 哪些异常需要回滚
)
传播行为(最常考也最容易理解错):
| 值 | 含义 |
|---|---|
REQUIRED(默认) |
有事务就加入,没有就创建 |
REQUIRES_NEW |
总是创建新事务,挂起当前事务 |
NESTED |
嵌套事务(保存点) |
SUPPORTS |
有就加入,没有就以非事务执行 |
NOT_SUPPORTED |
以非事务执行,挂起当前事务 |
NEVER |
必须无事务,有就抛异常 |
MANDATORY |
必须有事务,没有就抛异常 |
18.4 @Transactional 不生效的原因(结合上一节)
- ✅ 没生效原因 1:同类方法内部调用
- ✅ 没生效原因 2:方法不是 public
- ✅ 没生效原因 3:异常被 catch 吞掉
- ✅ 没生效原因 4:默认只回滚 RuntimeException,Checked Exception 不回滚(需要显式
rollbackFor = Exception.class) - ✅ 没生效原因 5:数据库本身不支持事务(如 MySQL MyISAM 引擎)
19. 手写一个自定义注解 + AOP
完整实战:实现一个 @LogExecutionTime 注解,自动记录方法耗时。
19.1 定义注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
String value() default ""; // 可选:附加说明
}
19.2 编写切面
java
@Aspect
@Component
@Slf4j
public class ExecutionTimeAspect {
@Around("@annotation(logExecutionTime)")
public Object logTime(ProceedingJoinPoint pjp, LogExecutionTime logExecutionTime)
throws Throwable {
String methodName = pjp.getSignature().toShortString();
String tag = logExecutionTime.value();
long start = System.currentTimeMillis();
try {
Object result = pjp.proceed();
long elapsed = System.currentTimeMillis() - start;
log.info("[{}] {} 耗时: {}ms", tag, methodName, elapsed);
return result;
} catch (Throwable t) {
long elapsed = System.currentTimeMillis() - start;
log.error("[{}] {} 异常, 耗时: {}ms", tag, methodName, elapsed, t);
throw t;
}
}
}
19.3 启用 AOP(Spring Boot 默认已开启)
java
@SpringBootApplication
@EnableAspectJAutoProxy // Spring Boot 通常不用写,已自动启用
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
19.4 使用
java
@Service
public class UserService {
@LogExecutionTime("查询用户")
public User findById(Long id) {
return userRepository.findById(id).orElseThrow();
}
}
调用 findById 时,日志会自动输出:
scss
[查询用户] UserService.findById(..) 耗时: 23ms
业务代码一行没改。
19.5 进阶:基于注解参数做更复杂的事
比如做一个 @RateLimit 限流注解:
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int permitsPerSecond() default 10;
}
@Aspect
@Component
public class RateLimitAspect {
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
@Around("@annotation(rateLimit)")
public Object limit(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
String key = pjp.getSignature().toShortString();
RateLimiter limiter = limiters.computeIfAbsent(key,
k -> RateLimiter.create(rateLimit.permitsPerSecond()));
if (!limiter.tryAcquire()) {
throw new RuntimeException("访问太频繁,请稍后重试");
}
return pjp.proceed();
}
}
// 使用
@RateLimit(permitsPerSecond = 5)
@GetMapping("/api/data")
public Data getData() { ... }
20. AOP 性能与使用建议
20.1 AOP 有性能开销吗?
有,但通常可以忽略:
- 每次方法调用要经过代理(约几百纳秒到微秒级)
- 反射调用比直接调用慢,但 Spring 4.x 之后用了优化
- 切入点匹配在启动时完成,运行时不重复计算
结论:对绝大多数 Web 应用,AOP 的开销远小于一次数据库查询,完全不用担心。
20.2 什么时候用 AOP?
✅ 适合用 AOP:
- 日志、监控、链路追踪
- 事务管理
- 缓存
- 权限校验
- 限流、熔断
- 参数校验、防重复提交
❌ 不适合用 AOP:
- 核心业务逻辑(应该明确写出来)
- 性能极端敏感的路径(虽然影响很小,但内层循环要小心)
- 简单的、只用一次的功能(直接写代码更清晰)
20.3 编码建议
- 切面要"窄而专":一个切面只做一件事,不要在 LoggingAspect 里又做缓存
- 切入点要精确 :
execution(* com..*.*(..))这种太宽,会代理大量无关方法 - 避免在切面里抛非业务异常:切面应该是"透明的",不应该影响业务流程
- 小心通知执行顺序 :多个切面应用同一方法时,用
@Order(N)控制顺序(数字越小越外层) - 不要在切面里调用同类方法:切面也是 Bean,同样有 AOP 陷阱
20.4 调试 AOP 的小技巧
确认 Bean 是不是代理对象:
java
System.out.println(userService.getClass());
// 输出 com.example.UserService 表示没代理
// 输出 com.example.UserService$$EnhancerBySpringCGLIB$$xxx 表示已代理
查看代理类型:
java
boolean isProxy = AopUtils.isAopProxy(userService);
boolean isCglib = AopUtils.isCglibProxy(userService);
boolean isJdk = AopUtils.isJdkDynamicProxy(userService);
总结与思维升级
IoC 的核心思想
不要自己造对象,让容器管理对象的生与死。
掌握 IoC 的关键不是记住有多少注解,而是理解:
- 为什么要让容器创建对象(解耦、复用、可测试)
- Bean 的生命周期里有哪些扩展点(PostConstruct、BeanPostProcessor)
- 同类型多 Bean 怎么区分(Qualifier、Primary)
- 循环依赖背后的三级缓存机制
AOP 的核心思想
业务逻辑要纯粹,横切逻辑要抽离。
掌握 AOP 的关键是理解:
- 代理是 AOP 的物理基础
- 注解只是"标记",真正干活的是切面
- 失效场景的根源都是"绕过了代理"
- 它是 Spring 很多高级特性的底层(事务、缓存、安全)
升华一下
Spring 之所以强大,本质就是用 IoC 把"创建"统一管起来,用 AOP 把"增强"统一管起来。掌握这两个支柱后,看 Spring Security、Spring Data、Spring Cloud 都会发现:它们都是基于 IoC 和 AOP 的组合应用。
学到这一步,你已经不只是"用 Spring",而是开始理解"Spring 的设计哲学"了。🎯