Spring 核心概念深入:IoC 与 AOP

Spring 核心概念深入:IoC 与 AOP

本文是 Spring 学习文档的扩展篇,针对前端开发者深入讲解 IoC 和 AOP 这两个最核心的概念。理解了这两章,再看 Spring 的任何高级特性都会豁然开朗。


目录

第一部分:IoC 深入

  1. [从一个真实场景开始理解 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")
  2. [Bean:Spring 容器里的对象](#Bean:Spring 容器里的对象 "#2-beanspring-%E5%AE%B9%E5%99%A8%E9%87%8C%E7%9A%84%E5%AF%B9%E8%B1%A1")
  3. [Bean 的生命周期](#Bean 的生命周期 "#3-bean-%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F")
  4. [Bean 的作用域(Scope)](#Bean 的作用域(Scope) "#4-bean-%E7%9A%84%E4%BD%9C%E7%94%A8%E5%9F%9Fscope")
  5. 依赖注入的三种方式深入对比
  6. 循环依赖问题
  7. [@Bean、@Component、@Configuration 的本质区别](#@Bean、@Component、@Configuration 的本质区别 "#7-beancomponentconfiguration-%E7%9A%84%E6%9C%AC%E8%B4%A8%E5%8C%BA%E5%88%AB")
  8. [条件装配与 Profile](#条件装配与 Profile "#8-%E6%9D%A1%E4%BB%B6%E8%A3%85%E9%85%8D%E4%B8%8E-profile")
  9. [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")
  10. [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 深入

  1. [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")
  2. [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")
  3. 五种通知(Advice)类型详解
  4. [切入点表达式(Pointcut Expression)](#切入点表达式(Pointcut Expression) "#14-%E5%88%87%E5%85%A5%E7%82%B9%E8%A1%A8%E8%BE%BE%E5%BC%8Fpointcut-expression")
  5. [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")
  6. [JDK 代理 vs CGLIB 代理](#JDK 代理 vs CGLIB 代理 "#16-jdk-%E4%BB%A3%E7%90%86-vs-cglib-%E4%BB%A3%E7%90%86")
  7. [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")
  8. [@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")
  9. [手写一个自定义注解 + 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")
  10. [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")
        );
    }
}

问题暴露:

  1. 耦合死了OrderService 必须知道每个依赖怎么创建,包括它们各自的依赖(PaymentGatewaySmsProvider)。
  2. 配置硬编码:URL、API Key 写在代码里,改一个要重新编译。
  3. 难以测试 :单元测试时想 Mock PaymentService,但它是在构造器里硬 new 的,根本换不掉。
  4. 重复创建 :另一个 RefundService 也需要 PaymentService,又得 new 一份,浪费资源。
  5. 缺乏统一管理 :应用关闭时谁来释放 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 工厂

简化流程:

  1. 创建 A,A 实例化后(属性未填充),把 A 的工厂放入三级缓存
  2. 给 A 注入属性时发现需要 B,去创建 B
  3. B 实例化后,要注入 A,去三级缓存拿到 A 的"半成品"
  4. B 完成创建,放入一级缓存
  5. 回到 A 的注入流程,把完整的 B 注入给 A
  6. 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-loadingv-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(无法被继承)
  • 目标方法不能是 finalstaticprivate(无法被重写)

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 启动时:

  1. 扫描到 @Transactional 注解
  2. OrderService 生成代理对象
  3. 代理对象的 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 编码建议

  1. 切面要"窄而专":一个切面只做一件事,不要在 LoggingAspect 里又做缓存
  2. 切入点要精确execution(* com..*.*(..)) 这种太宽,会代理大量无关方法
  3. 避免在切面里抛非业务异常:切面应该是"透明的",不应该影响业务流程
  4. 小心通知执行顺序 :多个切面应用同一方法时,用 @Order(N) 控制顺序(数字越小越外层)
  5. 不要在切面里调用同类方法:切面也是 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 的设计哲学"了。🎯

相关推荐
百珏7 小时前
AI 应用技术演进串讲大纲
人工智能·后端·架构
敖正炀8 小时前
云原生架构核心理念与演进路径
架构
leon_teacher9 小时前
HarmonyOS 6 实战:基于 Ads Kit 的插屏广告(视频 + 图片)架构与实现全解析
架构·音视频·harmonyos
逆境不可逃9 小时前
Hello-Agents 第二部分-第六章:框架开发实践
java·人工智能·分布式·学习·架构·rabbitmq
Ailrid9 小时前
设计模式——创建型设计模式:阅读笔记与个人思考
架构·设计
用户65868180338409 小时前
业务系统集成 OpenClaw 多 Agent 方案:从架构到落地的完整指南
架构
小码哥06810 小时前
一套可复用的打车系统模板,微服务版网约车系统|类似滴滴的打车平台
微服务·云原生·架构·滴滴·打车
元智启10 小时前
企业AI如何开发:智能体时代的安全治理架构与合规管控实践
人工智能·安全·架构
老毛肚10 小时前
微服务网关整合授权中心实现单点登录
运维·微服务·架构