Skip to content

IoC、DI 与 AOP

如果说 Spring 有哪几个概念最核心,那通常就是:

  1. IoC
  2. DI
  3. AOP

很多 Spring 能力最后都会回到这三条主线上。

这篇文章重点讲:

  1. IoC 到底是什么
  2. DIIoC 是什么关系
  3. Bean、容器、生命周期分别是什么意思
  4. AOP 到底在解决什么问题
  5. Spring 为什么能用代理机制把事务、日志这些能力织进去

1. IoC 到底是什么

IoCInversion of Control,即控制反转。

它可以看成 把对象的创建和管理控制权,从业务代码手里交给 Spring 容器

最直观的变化是:

  1. 以前自己 new
  2. 现在交给容器创建和维护

所以 IoC 解决的不是“少写几行代码”,而是:对象创建、依赖管理和生命周期控制不要散落在业务逻辑里。


2. DIIoC 到底是什么关系

DIDependency Injection,即依赖注入。

它可以理解成 由 Spring 容器把对象所依赖的其他对象自动注入进去

两者的关系可以直接记成:

  1. IoC 是思想
  2. DI 是最常见的落地方式

前者强调“控制权交给容器”,后者强调“依赖由容器注入”。


3. 为什么 IoC / DI 很重要

如果没有这套机制,代码经常会是这样:

  1. 一个类自己创建很多依赖
  2. 实现类被写死
  3. 测试时很难替换依赖
  4. 模块之间耦合很重

IoC / DI 带来的变化是:

  1. 对象创建交给容器
  2. 依赖关系由容器组织
  3. 替换实现更容易
  4. 模块更容易解耦
  5. 测试更方便

这就是为什么 Spring 在企业应用里很容易成为底座。


4. Spring 容器到底是什么

Spring 容器 可以理解成 负责创建、管理、装配和维护 Bean 的运行时环境

它解决的是 应用里那么多对象,谁来创建、谁来维护、谁依赖谁,这些事情不要让业务代码各自处理

更常见的两个名字是:

  1. BeanFactory
  2. ApplicationContext

4.1 BeanFactory

它更偏基础容器。

4.2 ApplicationContext

它是在 BeanFactory 基础上扩展出来的更完整容器。

它通常还提供:

  1. 事件机制
  2. 资源加载
  3. 国际化
  4. 更完整的应用上下文能力

所以实际开发里,更常接触的是 ApplicationContext


5. Bean 是什么

在 Spring 语境里,Bean 可以简单理解成:被 Spring 容器管理的对象。

要特别注意:不是所有 Java 对象都是 Bean。

只有那些:

  1. 被扫描到
  2. 被配置注册
  3. 或显式声明进入容器

的对象,才会变成 Bean。

常见方式包括:

  1. @Component
  2. @Service
  3. @Repository
  4. @Controller
  5. @Bean

5.1 常见 Bean 注册注解怎么理解

注解作用常见场景
@Component最通用的组件标记工具类、通用组件
@Service语义化表示业务服务层Service 层
@Repository语义化表示数据访问层DAO、Repository 层
@Controller语义化表示 MVC 控制层页面控制器
@RestController组合注解,表示接口控制层REST API
@Configuration表示配置类Bean 装配配置
@Bean把方法返回值注册为 Bean第三方对象、显式装配

@Service@Repository@Controller 本质上都还是组件扫描这条主线,只是语义更明确。

5.2 @Component@Bean 有什么区别

区别可以直接记成:

  1. @Component 是把“类”交给 Spring 扫描注册
  2. @Bean 是把“方法返回对象”交给 Spring 注册

例如:

java
@Service
public class OrderService {
}
java
@Configuration
public class AppConfig {

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper();
    }
}

6. 依赖注入有哪些常见方式

常见方式通常有:

  1. 构造器注入
  2. Setter 注入
  3. 字段注入

6.1 构造器注入

它更适合 把依赖显式表达出来,并且让对象在创建时就处于完整可用状态

工程上通常更推荐这种方式。

例如:

java
@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final PaymentService paymentService;

    public OrderService(OrderRepository orderRepository, PaymentService paymentService) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
    }
}

6.2 Setter 注入

它更适合 某些可选依赖或需要后续调整的依赖

例如:

java
@Component
public class ExportService {

    private MessageSender messageSender;

    @Autowired
    public void setMessageSender(MessageSender messageSender) {
        this.messageSender = messageSender;
    }
}

6.3 字段注入

它写起来最省事,但在依赖显式性、测试友好性方面通常没那么理想。

例如:

java
@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;
}

所以更稳妥的工程习惯通常是:优先构造器注入。

6.4 这 3 种注入方式到底有什么不同

这 3 种写法的核心区别,不只是“代码写在哪”,而是:依赖是在对象创建时注入,还是创建后再补进去;依赖关系是否显式;以及对象能不能从一开始就保持完整可用。

看一张总表:

方式注入时机依赖是否显式对象是否容易保持完整状态测试友好性
构造器注入创建对象时注入最显式最好
Setter 注入创建后再注入中等不一定较好
字段注入创建后由容器直接注入字段最弱不一定最差

把差别先拉开:

  1. 构造器注入强调“必要依赖必须在创建时给齐”
  2. Setter 注入强调“对象可以建出来,依赖之后再补”
  3. 字段注入强调“写法最短,但依赖关系最容易被隐藏”

6.5 它们各自的优缺点是什么

6.5.1 构造器注入

优点通常包括:

  1. 依赖最显式,代码一眼就能看出类依赖了什么
  2. 对象一创建就处于完整可用状态
  3. 适合把依赖声明为 final
  4. 最利于单元测试和重构

缺点通常包括:

  1. 当依赖很多时,构造器参数会变长
  2. 更容易暴露“类职责过多”这种设计问题

这里第二点从工程角度看不一定是坏事,因为它能更早提醒你这个类可能已经太重了。

6.5.2 Setter 注入

优点通常包括:

  1. 写法直观
  2. 对可选依赖更灵活
  3. 某些场景下方便后续替换依赖

缺点通常包括:

  1. 依赖不如构造器注入那么显式
  2. 对象在注入完成前,可能处于不完整状态
  3. 如果遗漏注入,问题更容易延迟到运行时暴露

所以它更适合 那些并不是对象创建所必需的依赖

6.5.3 字段注入

优点通常包括:

  1. 写法最短
  2. 初学时看起来最省事

缺点通常包括:

  1. 依赖最不显式
  2. 不利于单元测试
  3. 很难做不可变设计
  4. 更依赖 Spring 容器本身
  5. 容易隐藏一个类真实依赖过多的问题

所以它虽然方便,但工程上通常不是最推荐的选择。

6.6 工程上更常见的选择建议

如果把这三种方式压缩成最实用的结论,通常就是:

  1. 必要依赖优先使用构造器注入
  2. 可选依赖可以考虑 Setter 注入
  3. 字段注入尽量少用

可以用一句话记住:构造器注入最稳,Setter 注入更灵活,字段注入最省事但也最容易埋下维护问题。

6.7 常用依赖注入注解

注解作用要点
@Autowired按类型注入依赖Spring 最常见的注入注解
@Qualifier指定注入哪个 Bean多实现类场景很常见
@Primary指定默认优先 Bean解决同类型 Bean 冲突
@Resource按名称优先注入来自 Jakarta 规范

下面用同一组接口和实现,分别演示这几个注解最常见的使用方式。

看公共前置代码:

java
public interface MessageSender {
    void send(String message);
}

@Component("smsSender")
public class SmsSender implements MessageSender {
    @Override
    public void send(String message) {
    }
}

@Component("emailSender")
public class EmailSender implements MessageSender {
    @Override
    public void send(String message) {
    }
}

6.7.1 @Autowired

@Autowired 最常见的用法,是按类型注入依赖。

例如只有一个实现时,可以直接注入:

java
@Service
public class AuditService {

    private final MessageSender messageSender;

    public AuditService(@Autowired MessageSender messageSender) {
        this.messageSender = messageSender;
    }
}

这里真正的要点是:Spring 会先按类型找 Bean。

如果同一类型只有一个实现,这种写法最直接。

6.7.2 @Qualifier

当同一个接口有多个实现时,只按类型注入就不够了,这时通常要配合 @Qualifier 明确指定 Bean 名称。

java
@Service
public class NotifyService {

    private final MessageSender messageSender;

    public NotifyService(@Qualifier("smsSender") MessageSender messageSender) {
        this.messageSender = messageSender;
    }
}

这个例子表达的是:同类型 Bean 有多个时,用 @Qualifier 指定到底注入哪一个。

6.7.3 @Primary

如果你希望某个实现成为“默认首选实现”,可以在实现类上标记 @Primary

java
@Primary
@Component("smsSender")
public class SmsSender implements MessageSender {

    @Override
    public void send(String message) {
    }
}

@Component("emailSender")
public class EmailSender implements MessageSender {

    @Override
    public void send(String message) {
    }
}

@Service
public class DefaultNotifyService {

    private final MessageSender messageSender;

    public DefaultNotifyService(MessageSender messageSender) {
        this.messageSender = messageSender;
    }
}

这里没有写 @Qualifier,但仍然能注入成功,是因为:Spring 会优先选择被 @Primary 标记的那个实现。

6.7.4 @Resource

@Resource 更常见的理解是:按名称优先注入,名称找不到时再看类型。

java
@Service
public class ResourceNotifyService {

    @Resource(name = "emailSender")
    private MessageSender messageSender;
}

这个例子最适合说明:当你希望明确按 Bean 名称注入时,@Resource 会很直接。

6.8 Spring 是如何实现依赖注入的

前面讲的是:依赖注入有哪些写法、常见注解怎么用。

但如果再往里追一层,真正的问题其实是:Spring 容器内部到底是怎么把依赖“注进去”的。

可以把整个过程压缩成一条主线:

  1. 容器先收集并注册 BeanDefinition
  2. 容器刷新时,开始创建 Bean
  3. 创建 Bean 时先实例化对象
  4. 再解析依赖并完成属性填充
  5. 依赖注入完成后,再进入初始化和后置处理

也就是说:依赖注入不是一个孤立动作,而是 Bean 创建流程中的一个关键阶段。

一张总流程图,会更容易把后面的细节串起来:

mermaid
flowchart TD
    A[启动 Spring 应用] --> B[扫描配置类与组件]
    B --> C[注册 BeanDefinition]
    C --> D[刷新 ApplicationContext]
    D --> E[创建单例 Bean]
    E --> F{选择实例化方式}
    F -->|构造器注入| G[先解析构造参数]
    F -->|字段或 Setter 注入| H[先实例化对象]
    G --> I[创建 Bean 实例]
    H --> I
    I --> J[属性填充与依赖注入]
    J --> K[执行 Aware 与前置处理]
    K --> L[执行初始化方法]
    L --> M[执行 BeanPostProcessor 后置处理]
    M --> N[Bean 进入可用状态]
    N --> O[需要时可能被 AOP 代理包装]

6.8.1 第一步:把 Bean 定义收进容器

Spring 并不是看到 @Autowired 才临时决定“去哪里找对象”。更早之前,容器就在启动阶段把 Bean 的定义信息收集好了。

这些定义通常来自:

  1. @Component@Service@Repository 等组件扫描
  2. @Configuration + @Bean
  3. XML 或其他配置方式

这些信息最终会变成 BeanDefinition。它可以理解成“Bean 的说明书”,里面描述了:

  1. 这个 Bean 的类型是什么
  2. 它怎么创建
  3. 它的作用域是什么
  4. 它依赖哪些对象

所以容器先管理的是对象的定义信息,后面才按需创建实例。

6.8.2 第二步:容器刷新时,开始创建 Bean

ApplicationContext 为主线看,Spring 启动时会触发容器刷新。

在这个过程中,容器会把前面注册好的 Bean 定义交给 BeanFactory 管理,并逐步创建需要提前初始化的单例 Bean。

可以粗略理解成:容器开始按照 BeanDefinition,把“说明书”变成真正可用的对象。

6.8.3 第三步:先实例化,再注入依赖

这一步非常关键,因为不同注入方式,发生时机并不一样。

可以看最核心的区别:

  1. 构造器注入:创建对象之前,就要把构造参数解析出来
  2. Setter 注入 / 字段注入:对象先创建出来,再进行属性填充

例如下面这个类:

java
@Service
public class OrderService {

    private final OrderRepository orderRepository;

    @Autowired
    private PaymentService paymentService;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
}

Spring 大致会按这个思路处理:

  1. 先发现构造器需要 OrderRepository
  2. 先去容器里找 OrderRepository,找到后才能创建 OrderService
  3. OrderService 实例创建出来以后
  4. 再处理字段上的 @Autowired,把 PaymentService 注进去

这就是为什么:构造器注入更早发生,字段 / Setter 注入更偏向实例化之后的属性填充阶段。

6.8.4 @Autowired@Resource 到底是谁处理的

很多人会以为,@Autowired 是 Bean 自己“长出来的能力”。其实不是,它们是容器在 Bean 创建过程中,借助后置处理器完成的。

最常见的两个处理器是 AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor

  1. AutowiredAnnotationBeanPostProcessor 负责处理 @Autowired@Value 等注解
  2. CommonAnnotationBeanPostProcessor 负责处理 @Resource@PostConstruct@PreDestroy 等 Jakarta / JSR 规范相关注解

不是注解自己会注入依赖,而是 Spring 在创建 Bean 的过程中识别这些注解,再按规则完成注入。

6.8.5 Spring 按什么规则找到要注入的 Bean

依赖注入真正难的地方,不是“塞进去”,而是:同类型 Bean 有多个时,到底该选谁。

Spring 常见的解析顺序大致就是:

  1. 看类型能不能唯一匹配
  2. 如果同类型有多个,再看名称、@Qualifier@Primary
  3. 如果还是无法确定,就抛出注入歧义异常

例如:

java
public interface MessageSender {
    void send(String message);
}

@Primary
@Component("smsSender")
public class SmsSender implements MessageSender {
    @Override
    public void send(String message) {
    }
}

@Component("emailSender")
public class EmailSender implements MessageSender {
    @Override
    public void send(String message) {
    }
}

@Service
public class NotifyService {

    private final MessageSender messageSender;

    public NotifyService(@Qualifier("emailSender") MessageSender messageSender) {
        this.messageSender = messageSender;
    }
}

这个例子里:

  1. 如果没有 @Qualifier,Spring 会优先考虑 @Primary
  2. 写了 @Qualifier("emailSender") 之后,就明确指定注入 emailSender

@Resource(name = "emailSender") 更强调优先按名字去找 Bean。

6.8.6 为什么字段循环依赖有时能解,构造器循环依赖通常不行

这件事其实也和依赖注入的时机强相关。

字段注入 / Setter 注入的 Bean,往往可以把对象实例“提前暴露”出来,然后再补属性,所以 Spring 在一部分单例场景里还能继续推进。

但构造器注入不一样,因为构造器参数在对象创建前就必须准备好。

如果 A 构造器依赖 B,B 构造器又依赖 A,那么两个对象都还没创建出来,就已经卡住了。

  1. Spring 不是“天然能解决循环依赖”
  2. 它只是能在一部分单例 Bean 的属性注入场景里,通过提前暴露对象引用继续完成装配
  3. 构造器循环依赖通常没有这个回旋空间

6.8.7 把整个依赖注入流程串起来

把 Spring 依赖注入的实现压缩成一条完整链路,可以记成:

  1. 扫描和注册 Bean,生成 BeanDefinition
  2. 容器刷新,开始创建 Bean
  3. 先实例化对象
  4. 再由容器解析 @Autowired@Resource 等注解
  5. 按类型、名称、@Qualifier@Primary 等规则查找依赖
  6. 完成依赖注入后,进入初始化、后置处理和可能的代理增强

Spring 依赖注入真正解决的,不只是“自动帮你 new 对象”,而是把对象定义、创建时机、依赖解析、生命周期扩展和后续增强放进同一套容器机制里统一处理。


7. Bean 生命周期到底在说什么

Bean 生命周期不是简单的“new 一个对象”。

更完整一点的理解通常包括:

  1. 实例化
  2. 依赖注入
  3. 初始化前处理
  4. 初始化
  5. 初始化后处理
  6. Bean 可用
  7. 容器关闭时销毁

这里最关键的理解是:Spring 能在 Bean 创建前后插入很多扩展点,所以很多增强能力才有落点。

这也是为什么 BeanPostProcessor 这类机制很重要。

7.1 生命周期相关常见注解和接口

注解 / 接口作用
@PostConstruct依赖注入完成后执行初始化逻辑
@PreDestroyBean 销毁前执行清理逻辑
InitializingBean初始化回调接口
DisposableBean销毁回调接口

看更常见的注解方式:

java
@Component
public class CacheManager {

    @PostConstruct
    public void init() {
        System.out.println("load cache");
    }

    @PreDestroy
    public void destroy() {
        System.out.println("clear cache");
    }
}

如果你想用接口回调方式,也可以这样写:

java
@Component
public class CacheManager implements InitializingBean, DisposableBean {

    @Override
    public void afterPropertiesSet() {
        System.out.println("Bean 初始化完成,开始加载缓存");
    }

    @Override
    public void destroy() {
        System.out.println("Bean 即将销毁,开始释放资源");
    }
}

要注意的是:

  1. InitializingBean 对应的方法是 afterPropertiesSet()
  2. DisposableBean 对应的方法是 destroy()

它们最常见的用途通常包括:

  1. 初始化缓存
  2. 校验关键配置
  3. 预热连接或资源
  4. 容器关闭时释放资源

7.2 接口方式和注解方式怎么选

把这两种方式分开:

  1. InitializingBean / DisposableBean:接口方式
  2. @PostConstruct / @PreDestroy:注解方式

接口方式的特点是:

  1. 语义很直接
  2. 生命周期时机明确
  3. 但类会直接依赖 Spring 接口

注解方式的特点是:

  1. 写法更轻
  2. 侵入性更低
  3. 在大多数业务代码里更常见

所以更常见的工程习惯通常是:优先使用 @PostConstruct / @PreDestroy;只有在明确需要接口回调风格时,再考虑 InitializingBean / DisposableBean。


8. AOP 到底是什么

AOPAspect-Oriented Programming,即面向切面编程。

它可以看成 把分散在多个业务方法里的通用逻辑抽出来,不直接写死在业务主流程里

这些通用逻辑通常包括:

  1. 日志
  2. 事务
  3. 权限校验
  4. 性能统计
  5. 审计

如果没有 AOP,业务方法就容易变成:

  1. 先写主流程
  2. 再插日志
  3. 再插事务
  4. 再插鉴权

最后业务代码会越来越乱。

8.1 一个最直观的 AOP 示例

如果不用 AOP,日志可能会分散在很多业务方法里:

java
public void createOrder() {
    long start = System.currentTimeMillis();
    try {
        // 业务逻辑
    } finally {
        System.out.println("cost=" + (System.currentTimeMillis() - start));
    }
}

用了 AOP 之后,可以把通用逻辑抽出去:

java
@Aspect
@Component
public class LogAspect {

    @Around("execution(* com.example.service..*(..))")
    public Object logCost(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            System.out.println(joinPoint.getSignature() + " cost=" + (System.currentTimeMillis() - start));
        }
    }
}

9. AOP 具体解决什么问题

它解决的核心问题是:横切逻辑不该和主业务逻辑强耦合。

这里的“横切逻辑”,可以理解成 很多模块都要用,但它本身又不是主业务目标的那类逻辑

所以 AOP 的价值是:

  1. 让业务方法更聚焦主流程
  2. 让通用逻辑集中治理
  3. 让日志、事务、安全等能力更容易复用

10. Spring AOP 为什么能工作

Spring AOP 的核心不是“偷偷改源码”,而是:在运行时为目标对象创建代理对象,由代理对象在合适时机织入增强逻辑。

  1. 业务对象负责主流程
  2. 代理对象包在外面
  3. 请求先进入代理
  4. 代理决定是否在前后插入日志、事务、安全等增强逻辑

所以很多 Spring 能力背后都离不开:容器 + 代理

10.1 Spring AOP 常见注解

注解作用
@Aspect标记这是一个切面类
@Pointcut抽取切点表达式
@Before方法执行前增强
@After方法执行后增强
@AfterReturning方法正常返回后增强
@AfterThrowing方法抛异常后增强
@Around环绕增强,控制能力最强

例如把切点单独抽出来:

java
@Aspect
@Component
public class AuditAspect {

    @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
    public void transactionalMethod() {
    }

    @Before("transactionalMethod()")
    public void beforeTransactionalMethod() {
        System.out.println("before transactional method");
    }
}

10.2 Spring 里如何实现自定义注解

很多人第一次说“Spring 自定义注解”时,脑子里容易把两件事混在一起:

  1. 定义一个 Java 注解
  2. 让 Spring 识别这个注解,并在运行时做点事情

只有把这两步都做了,自定义注解才算真正“在 Spring 里跑起来”。

也就是说,注解本身只是元数据,Spring 还需要一套处理机制去识别它。

在工程里,最常见的实现方式通常是:自定义注解 + AOP 切面

例如,我们希望给某些业务方法加一个 @AuditLog,自动记录审计日志。

10.2.1 第一步:先定义注解

java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuditLog {

    String action();
}

这个定义里最关键的几件事是:

  1. @Target(ElementType.METHOD):表示这个注解放在方法上
  2. @Retention(RetentionPolicy.RUNTIME):表示运行时仍然保留,Spring 才有机会读取到它
  3. String action():给注解增加业务属性,表示当前操作的名称

如果没有 RUNTIME 级别保留策略,运行时拿不到注解信息,AOP 也就无从处理。

10.2.2 第二步:写切面去识别这个注解

java
@Aspect
@Component
public class AuditLogAspect {

    @Around("@annotation(auditLog)")
    public Object recordAuditLog(ProceedingJoinPoint joinPoint, AuditLog auditLog) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            String methodName = joinPoint.getSignature().toShortString();
            System.out.println("action=" + auditLog.action()
                    + ", method=" + methodName
                    + ", cost=" + (System.currentTimeMillis() - start));
        }
    }
}

这段代码里最关键的是:

  1. @Around("@annotation(auditLog)"):表示只拦截那些标了 @AuditLog 的方法
  2. AuditLog auditLog:Spring 会把当前方法上的注解对象直接传进来
  3. joinPoint.proceed():真正执行目标方法

这条链路直接看成:

  1. 业务方法被 Spring 代理包装
  2. 调用先进入切面
  3. 切面发现方法上有 @AuditLog
  4. 先执行增强逻辑,再放行目标方法
  5. 最后再做日志记录或收尾动作

10.2.3 第三步:在业务方法上使用

java
@Service
public class OrderService {

    @AuditLog(action = "createOrder")
    public void createOrder(Long orderId) {
        System.out.println("create order: " + orderId);
    }
}

这样一来,调用 createOrder() 时,除了业务本身执行,还会自动触发切面里的审计逻辑。

这就是 Spring 里最常见的“自定义注解落地”方式:注解负责表达业务语义,AOP 负责在运行时接住这个语义并织入对应逻辑。

10.2.4 为什么只定义注解还不够

很多人会写出这样的代码:

java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
}

然后期待加在方法上就自动生效,其实不会。注解本身只是在类或方法上贴了一个标记,真正让它“有功能”的,是 Spring 后续如何处理这个标记。

这个处理动作常见可能来自:

  1. AOP 切面
  2. BeanPostProcessor
  3. ImportSelector
  4. Spring MVC 参数解析或拦截器扩展

但在大多数业务项目里,最常见、最容易理解的仍然是:自定义注解 + AOP

10.2.5 再看一个组合注解示例

Spring 里除了“注解 + AOP”这种玩法,还有一种很常见的自定义方式,就是组合注解。

例如:

java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
@RequestMapping("/admin")
public @interface AdminRestController {
}

然后就可以这样用:

java
@AdminRestController
public class AdminUserController {

    @GetMapping("/users")
    public String listUsers() {
        return "ok";
    }
}

这个例子想表达的是:你可以把多个 Spring 注解打包成一个更符合业务语义的组合注解。

它的价值是:

  1. 减少重复配置
  2. 让注解表达更贴近业务语义
  3. 统一约束一类组件的使用方式

10.2.6 一句话总结 Spring 自定义注解

一句话总结就是:自定义注解本身只负责表达语义,真正让它生效的,是 Spring 在运行时通过 AOP、后置处理器或其他扩展机制去识别并处理它。


11. JDK 动态代理和 CGLIB 有什么区别

这是 Spring AOP 里最常见的问题之一。

11.1 JDK 动态代理

它更偏:基于接口生成代理。

适合目标对象有接口的场景。

11.2 CGLIB

它更偏:通过继承目标类并生成子类来做代理。

它不要求目标类一定有接口。

一句话压缩:

  1. JDK 动态代理偏接口
  2. CGLIB 偏子类继承

12. 为什么事务也和 AOP 有关

很多人会把事务只看成数据库问题,但在 Spring 里,声明式事务之所以能工作,常常也依赖 AOP 代理。

例如:

java
@Transactional
public void createOrder() {
}

这背后的关键不是注解本身“有魔法”,而是:Spring 通过代理对象在方法调用前后插入事务开启、提交、回滚这些逻辑。

所以事务、AOP、代理机制其实是连在一起的。


13. @Transactional 为什么有时候不生效

这是典型高频问题。

常见原因包括:

  1. 方法没有经过 Spring 代理对象调用
  2. 同类内部自调用
  3. 方法可见性不合适
  4. 异常被吞掉,没有触发预期回滚

这里最容易忽略的一点是:同类内部直接调用方法,往往不会经过代理对象,所以增强逻辑可能根本没机会执行。


14. 工程上最常见的几个误区

14.1 误区一:IoC 就是自动 new 对象

这太窄了。

IoC 的重点是控制权转移和统一管理,不只是少写 new

14.2 误区二:DIIoC 是同义词

它们密切相关,但不完全一样。

更稳妥的说法仍然是:

IoC 是思想,DI 是落地手段。

14.3 误区三:AOP 就是“高级注解”

注解只是表达形式。

真正让 AOP 工作的是代理和增强机制。

14.4 误区四:只要加了 @Transactional 就一定安全

事务是否生效,仍然要看:

  1. 调用路径
  2. 代理是否参与
  3. 异常是否正确传播

15. 一句话总结

IoC / DI 解决的是对象创建和依赖组织问题,AOP 解决的是横切逻辑抽离问题,而 Spring 真正强大的地方,正是在容器和代理机制之上把这两条主线统一了起来。

基于 VitePress 构建的个人技术笔记。