spring-ioc-di-tutorial-v3

【Spring】一文真正吃透 IoC 与 DI:从 new 的耦合问题到 Bean 创建全流程

本文基于 Java 21 与现代 Spring Boot 的常见写法,从一个普通博客业务出发,逐步解释 IoC、DI、ApplicationContextBeanDefinition@Bean、依赖注入、作用域和 Bean 生命周期。重点不在背注解,而在理解 Spring 为什么这样设计,以及代码究竟在什么时候生效。


前言:我们每天都在用 Spring,但 Spring 到底帮我们做了什么?

写过 Spring Boot 项目后,下面这段代码应该非常熟悉:

@RestController
@RequestMapping("/blogs")
public class BlogController {

    private final BlogService blogService;

    public BlogController(BlogService blogService) {
        this.blogService = blogService;
    }
}

我们没有手动创建 BlogService,它却能直接使用。于是很多教程把 IoC 总结成一句话:

以前对象由我们自己 new,现在交给 Spring new

这句话适合入门,却解释不了真正的工程问题:

  • Spring 如何找到 BlogService 的实现?
  • 同一个接口存在两个实现时,Spring 为什么不知道注入谁?
  • @Component@Bean 都能注册对象,它们有什么本质区别?
  • 为什么 Spring 默认提前创建单例 Bean?
  • 为什么单例 Bean 不一定线程安全?
  • @Transactional 为什么依赖代理对象?
  • Bean 的生命周期和 AOP 有什么联系?

因此,理解 IoC 不能只盯着 new。我们要完整地观察一次:

一个普通 Java 类,如何被 Spring 发现、描述、创建、注入、初始化、增强,最终成为业务代码真正拿到的 Bean。


一、先不用 Spring:new 到底带来了什么问题?

假设我们要实现一个博客查询接口,最初代码如下。

1.1 传统写法

public interface BlogMapper {
    String findTitleById(Long id);
}
public class MemoryBlogMapper implements BlogMapper {

    @Override
    public String findTitleById(Long id) {
        return "深入理解 Spring IoC";
    }
}
public class BlogService {

    private final BlogMapper blogMapper;

    public BlogService() {
        this.blogMapper = new MemoryBlogMapper();
    }

    public String getTitle(Long id) {
        return blogMapper.findTitleById(id);
    }
}

代码能运行,但 BlogService 已经和 MemoryBlogMapper 绑定在了一起。

后来我们接入 MyBatis,希望使用数据库实现:

public class MyBatisBlogMapper implements BlogMapper {
    // 查询 MySQL
}

此时不仅要增加新实现,还必须修改 BlogService

public BlogService() {
    this.blogMapper = new MyBatisBlogMapper();
}

问题并不在于多写了一行 new,而在于 BlogService 同时承担了两种职责:

  1. 处理博客业务;
  2. 决定持久层对象如何创建。

业务逻辑和对象构建逻辑混在了一起。

1.2 把依赖从外部传进来

我们先不使用 Spring,只做一次普通 Java 重构:

public class BlogService {

    private final BlogMapper blogMapper;

    public BlogService(BlogMapper blogMapper) {
        this.blogMapper = blogMapper;
    }

    public String getTitle(Long id) {
        return blogMapper.findTitleById(id);
    }
}

创建对象时,由外部决定传入哪个实现:

BlogMapper mapper = new MemoryBlogMapper();
BlogService service = new BlogService(mapper);

切换实现时,BlogService 不再修改:

BlogMapper mapper = new MyBatisBlogMapper();
BlogService service = new BlogService(mapper);

这一步已经出现了依赖注入的基本思想:

BlogService 不负责创建依赖,只通过构造方法声明“我需要一个 BlogMapper”。

但是,随着项目扩大,谁来统一创建 Controller、Service、Mapper、DataSource、事务管理器和各种客户端?谁来决定创建顺序?谁来处理单例、初始化和销毁?

这就是 IoC 容器存在的原因。


二、IoC 与 DI:一个负责交权,一个负责交对象

2.1 IoC:反转的是对象创建和依赖查找的控制权

IoC 全称 Inversion of Control,即控制反转。

传统代码中,业务对象主动创建或者查找依赖:

this.blogMapper = new MemoryBlogMapper();

使用 IoC 后,对象只描述自身依赖,创建与组装工作交给容器:

public BlogService(BlogMapper blogMapper) {
    this.blogMapper = blogMapper;
}

所以,IoC 更准确的定义是:

对象创建、依赖查找和对象装配的控制权,从业务对象自身转移给外部容器。

IoC 的目的不是消灭 new。DTO、实体类、局部数据对象照样可以 new。真正适合交给容器的,是那些需要长期存在、相互协作、统一配置或者被框架增强的组件。

2.2 DI:容器把依赖传给对象

DI 全称 Dependency Injection,即依赖注入。

Spring 官方对 DI 的描述可以压缩成一句话:对象通过构造方法、工厂方法参数或者属性声明依赖,容器创建 Bean 时负责注入。

因此二者关系是:

IoC:设计思想——依赖管理权交给容器
DI:实现手段——容器把依赖注入对象

2.3 IoC、DI 与 DIP 不要混淆

依赖倒置原则 DIP(Dependency Inversion Principle)强调:高层模块和低层模块都应该依赖抽象。

private final BlogMapper blogMapper;

这里依赖 BlogMapper 接口,而不是写死 MemoryBlogMapper

三者可以这样记:

概念回答的问题
DIP依赖关系应该如何设计?
IoC依赖创建和管理权交给谁?
DI依赖对象怎样进入业务对象?

DI 能帮助我们实践 IoC 和 DIP,但三者不是同一个概念。


三、Spring 容器不是一个“大号 Map”

我们可以从启动方法获得 Spring 上下文:

ConfigurableApplicationContext context =
        SpringApplication.run(BlogApplication.class, args);

然后获取 Bean:

BlogService blogService = context.getBean(BlogService.class);

它看起来很像:

map.get(BlogService.class);

Spring内部确实需要类似映射表的结构保存 Bean 定义和单例对象,也会使用反射分析构造方法、注解和字段。但 Spring 容器不是简单的“反射 + Map”,它至少承担三类工作。

3.1 保存 Bean 的创建说明

Spring需要知道:

  • 创建哪个类;
  • Bean 叫什么;
  • 使用单例还是多例;
  • 构造方法需要哪些参数;
  • 依赖哪些其他 Bean;
  • 是否懒加载;
  • 如何初始化和销毁。

3.2 根据依赖图创建和组装对象

假设依赖关系如下:

BlogController
    └── BlogService
            └── BlogMapper
                    └── DataSource

创建 Controller 前,Spring需要先准备 Service;创建 Service 前,又要先准备 Mapper。

3.3 在生命周期中增强对象

事务、缓存和 AOP 通常不是修改原始业务类,而是在 Bean 生命周期中创建代理对象,再把代理交给其他组件使用。

完整主线如下:

扫描注解、读取 @Bean 和自动配置

注册 BeanDefinition

分析依赖关系

实例化原始对象

依赖注入

初始化回调

BeanPostProcessor 处理

必要时生成 AOP 代理

按作用域提供给业务代码

可以把容器理解成一座自动化装配工厂:

BeanDefinition:生产图纸
BeanFactory:按照图纸生产和装配对象
ApplicationContext:承载完整工厂运行环境
BeanPostProcessor:装配线上的质量检测与功能增强工位

四、BeanFactory、ApplicationContext 与 BeanDefinition

4.1 BeanFactory:IoC 容器的基础能力

BeanFactory 是 Spring Bean 管理体系中的基础接口,提供创建、查找和依赖管理等能力。

常见的 getBean() 就属于这套体系:

Object getBean(String name);

<T> T getBean(Class<T> requiredType);

4.2 ApplicationContext:完整的应用运行环境

ApplicationContextBeanFactory 的子接口。在 Bean 管理之外,还增加了:

  • AOP 集成;
  • 事件发布;
  • 国际化消息;
  • 资源读取;
  • 环境和配置访问;
  • Web 应用上下文。

普通 Spring Boot 业务开发基本围绕 ApplicationContext 运行,但不应该在业务类中到处主动调用 getBean()。否则业务代码会直接依赖容器,重新走向 Service Locator 风格。

下面这种写法只适合调试、框架扩展或特殊动态场景:

BlogService service = context.getBean(BlogService.class);

正常业务协作仍然应该使用构造方法注入。

4.3 BeanDefinition:Spring 为什么需要“对象说明书”

Spring扫描到 @Service 时,并不会简单地马上执行 new,而是先将相关信息转换成 BeanDefinition

BeanDefinition 通常描述:

  • Bean 类型和名称;
  • 作用域;
  • 构造参数与属性;
  • 依赖关系;
  • 懒加载标记;
  • 初始化与销毁方法。

为什么要先建立定义,再创建对象?因为 Spring需要先掌握完整配置,才能统一处理:

  • 条件注册;
  • 依赖排序;
  • 同类型候选选择;
  • 作用域;
  • BeanFactory 后置处理;
  • Bean 生命周期;
  • AOP 代理。

最终流程是:

配置元数据
    ↓
BeanDefinition
    ↓
创建和组装对象
    ↓
Bean 实例或代理对象

五、Spring 如何知道哪些对象要成为 Bean?

5.1 组件扫描:自己编写的类交给 Spring

@RestController
public class BlogController {
}

@Service
public class BlogServiceImpl implements BlogService {
}

@Repository
public class MemoryBlogRepository {
}

@Controller@Service@Repository 都属于组件 stereotype 注解。它们都能让类被扫描为 Bean,同时表达不同层次的职责。

如果没有明确分层语义,也可以使用:

@Component
public class JwtUtils {
}

@SpringBootApplication 默认从启动类所在包开始向下扫描,所以启动类通常放在项目根包中。

例如:

com.example.blog
├── BlogApplication.java
├── controller
├── service
├── mapper
└── config

如果把配置类放在 com.other.config,又没有额外配置扫描或导入,它不会因为写了 @Configuration 就自动生效。

实战提示:看到 NoSuchBeanDefinitionException 时,不要第一时间怀疑注入注解,先检查目标类是否真的处于组件扫描范围内。

5.2 @Bean:无法直接扫描的对象怎么办?

假设我们要使用 JDK 提供的 HttpClient。我们不能修改它的源码,也不能给它补上 @Component

这时可以使用 @Bean

@Configuration
public class HttpClientConfig {

    @Bean
    public HttpClient httpClient() {
        return HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(3))
                .build();
    }
}

@Bean 的语义是:

这是一个 Bean 工厂方法,请将它形成 BeanDefinition,并在需要时调用该方法,把返回对象纳入 Spring 管理。


六、把 @Bean 真正拆开看

6.1 @Bean 方法中的四类信息

@Bean
public HttpClient httpClient() {
    return HttpClient.newHttpClient();
}

这段代码不仅仅是“返回一个对象”,它同时表达:

代码位置对 Spring 的含义
HttpClient 返回类型Bean 的可识别类型
httpClient 方法名默认 Bean 名称
方法参数当前 Bean 需要的依赖
方法返回值交给容器管理的实际对象

6.2 @Bean 的典型使用场景

场景一:第三方类无法添加注解

@Bean
public HttpClient httpClient() {
    return HttpClient.newHttpClient();
}

场景二:对象创建过程复杂

@Bean
public ApiClient apiClient(ApiProperties properties,
                           HttpClient httpClient) {
    return new ApiClient(
            properties.getBaseUrl(),
            properties.getToken(),
            httpClient
    );
}

场景三:同一个类型需要多个配置实例

@Bean
public ApiClient internalApiClient() {
    return new ApiClient("http://internal.example.com");
}

@Bean
public ApiClient publicApiClient() {
    return new ApiClient("https://api.example.com");
}

此时不是“一个 Bean 两个名字”,而是两份 BeanDefinition,通常对应两个对象实例。

6.3 Bean 默认名称就是方法名

@Bean
public User user1() {
    return new User("zhangsan", 18);
}

默认 Bean 名称是 user1

可以自定义名称:

@Bean("mainUser")
public User user1() {
    return new User("zhangsan", 18);
}

也可以设置别名:

@Bean({"mainUser", "user1"})
public User user1() {
    return new User("zhangsan", 18);
}

这里 mainUseruser1 指向同一个 Bean,不会创建两个对象。

我们可以用下面的代码验证:

User a = context.getBean("mainUser", User.class);
User b = context.getBean("user1", User.class);

System.out.println(a == b);

输出:

true

6.4 @Bean 方法参数会自动注入

@Bean
public BlogService blogService(BlogMapper blogMapper) {
    return new BlogServiceImpl(blogMapper);
}

Spring 调用 blogService() 前,会先从容器中寻找 BlogMapper。这和构造方法注入的思想完全一致:方法参数就是依赖声明。

如果存在多个 BlogMapper 候选,可以在方法参数上使用限定:

@Bean
public BlogService blogService(
        @Qualifier("mysqlBlogMapper") BlogMapper blogMapper) {
    return new BlogServiceImpl(blogMapper);
}

6.5 为什么不建议直接调用另一个 @Bean 方法?

有些代码会这样写:

@Bean
public BlogService blogService() {
    return new BlogServiceImpl(blogMapper());
}

在默认完整代理模式的 @Configuration 中,Spring会增强配置类,把 blogMapper() 调用重定向到容器,以维护单例语义。

但是,如果 @Bean 位于普通 @Component 中,或者配置类关闭了方法代理,blogMapper() 就可能退化为普通 Java 方法调用,直接创建新对象。

更稳定、更易读的写法仍然是参数注入:

@Bean
public BlogService blogService(BlogMapper blogMapper) {
    return new BlogServiceImpl(blogMapper);
}

这种写法不依赖“配置类方法调用是否被代理”这一隐含条件。

6.6 @Bean 返回的对象仍会经历 Spring 生命周期

虽然对象是在我们的方法里 new 出来的:

return new ApiClient();

但方法返回后,它会进入容器,继续接受依赖填充、初始化回调、Bean 后置处理和销毁管理。

@Bean(initMethod = "init", destroyMethod = "close")
public ApiClient apiClient() {
    return new ApiClient();
}

这和在普通业务方法中临时 new ApiClient() 完全不同。后者不会自动进入 Spring 生命周期。


七、依赖注入的三种方式,为什么构造方法更合适?

7.1 字段注入:写得快,但隐藏依赖

@Service
public class BlogServiceImpl implements BlogService {

    @Autowired
    private BlogMapper blogMapper;
}

优点是代码短,缺点则更加关键:

  • 依赖没有体现在对象构造规则中;
  • 字段不能自然声明为 final
  • 脱离 Spring 直接创建对象时,字段是 null
  • 单元测试需要容器或反射填充字段;
  • 类依赖过多时不容易第一眼发现。

7.2 Setter 注入:适合真正可选的依赖

@Service
public class BlogServiceImpl implements BlogService {

    private AuditService auditService;

    @Autowired
    public void setAuditService(AuditService auditService) {
        this.auditService = auditService;
    }
}

Setter 注入允许对象先创建,再配置属性,适合可选或允许重新配置的依赖。

但如果 BlogMapper 是 Service 正常工作不可缺少的依赖,就不应该让对象在缺少它的情况下先被创建。

7.3 构造方法注入:维护对象成立的基本条件

@Service
public class BlogServiceImpl implements BlogService {

    private final BlogMapper blogMapper;

    public BlogServiceImpl(BlogMapper blogMapper) {
        this.blogMapper = blogMapper;
    }
}

构造方法表达了一个明确约束:

没有 BlogMapper,就不存在一个可正常使用的 BlogServiceImpl。

它的优点来自设计本身:

  • 对象创建完成时必需依赖已经齐全;
  • 可以使用 final 字段;
  • 测试时可以直接传入 Fake 或 Mock;
  • 构造参数过多会暴露类职责过重;
  • 构造器循环依赖会立即暴露。

如果类只有一个构造方法,通常不需要写 @Autowired

public BlogServiceImpl(BlogMapper blogMapper) {
    this.blogMapper = blogMapper;
}

7.4 用一个单元测试感受构造注入的价值

class BlogServiceTest {

    @Test
    void shouldReturnTitle() {
        BlogMapper fakeMapper = id -> "测试标题";
        BlogService service = new BlogServiceImpl(fakeMapper);

        assertEquals("测试标题", service.getTitle(1L));
    }
}

这个测试不需要启动 Spring,也不需要连接数据库。因为对象依赖是显式的,我们可以自由替换。


八、@Autowired、@Qualifier、@Primary 与 @Resource

8.1 只有一个候选对象时

@Service
public class BlogServiceImpl implements BlogService {
}
public BlogController(BlogService blogService) {
    this.blogService = blogService;
}

Spring按类型找到唯一 BlogService Bean,直接完成注入。

8.2 同一个接口有两个实现时

@Service
public class DatabaseBlogService implements BlogService {
}

@Service
public class CacheBlogService implements BlogService {
}

此时仅按照 BlogService 类型无法确定唯一候选,Spring会报告依赖不唯一。

方案一:使用 @Primary 指定默认实现

@Primary
@Service
public class DatabaseBlogService implements BlogService {
}

方案二:使用 @Qualifier 精确限定

public BlogController(
        @Qualifier("cacheBlogService") BlogService blogService) {
    this.blogService = blogService;
}

可以把依赖解析过程简化为:

根据注入点类型寻找候选 Bean

候选是否唯一?

完成注入

检查 Qualifier、Primary 等限定信息

能否确定唯一 Bean?

抛出依赖不唯一异常

8.3 @Resource 的理解

@Resource(name = "cacheBlogService")
private BlogService blogService;

@Resource 是 Jakarta 标准注解,在 Spring 中更强调按名称装配的语义。

面试中可以先这样概括:

@Autowired:Spring 注解,主要按照类型解析
@Qualifier:在多个类型候选中继续限定
@Primary:指定默认优先候选
@Resource:Jakarta 标准注解,主要表达按名称装配

不过,现代项目的重点仍然是优先使用构造方法,使依赖关系显式化。


九、Bean 的作用域:不是这个类全局只能有一个对象

9.1 常见作用域

作用域含义
singleton每个容器、每份 BeanDefinition 共享一个实例,默认值
prototype每次向容器请求时创建新实例
request每个 HTTP 请求一个实例
session每个 HTTP Session 一个实例
application每个 ServletContext 一个实例
websocket每个 WebSocket 会话一个实例

9.2 Spring singleton 与经典单例模式不同

下面两个方法都返回 User,但它们是两份 BeanDefinition:

@Bean
public User user1() {
    return new User();
}

@Bean
public User user2() {
    return new User();
}

即使二者都是 singleton,容器中仍然可以存在两个 User 对象。

所以 Spring singleton 更准确的描述是:

per-container、per-bean-definition,而不是这个 Java 类在整个 JVM 中绝对只有一个对象。

9.3 如何验证 singleton 和 prototype

@Component
@Scope("prototype")
public class PrototypeTask {
}
@Component
public class SingletonTask {
}
@Bean
CommandLineRunner scopeRunner(ApplicationContext context) {
    return args -> {
        SingletonTask s1 = context.getBean(SingletonTask.class);
        SingletonTask s2 = context.getBean(SingletonTask.class);

        PrototypeTask p1 = context.getBean(PrototypeTask.class);
        PrototypeTask p2 = context.getBean(PrototypeTask.class);

        System.out.println("singleton: " + (s1 == s2));
        System.out.println("prototype: " + (p1 == p2));
    };
}

典型输出:

singleton: true
prototype: false

9.4 单例 Bean 为什么不一定线程安全

Controller 和 Service 默认通常都是单例 Bean。多个 HTTP 请求线程可能同时调用同一个 Service。

@Service
public class BlogService {

    private Long currentUserId;
}

如果每个请求都修改 currentUserId,请求之间就可能覆盖彼此的数据。

Spring只保证容器管理的是同一个单例实例,不保证这个对象中的可变成员变量自动线程安全。

因此,Web 项目中的 Controller 和 Service 通常应该保持无状态:

  • 请求数据使用方法参数;
  • 临时状态使用局部变量;
  • 不把当前用户、当前请求等信息保存在单例字段中。

这正好和 Java 并发中的共享变量问题联系起来:局部变量通常属于线程自己的调用栈,而单例对象的成员变量会被多个线程共享。

9.5 prototype 注入 singleton 的经典陷阱

@Scope("prototype")
@Component
public class TaskContext {
}
@Service
public class TaskService {

    private final TaskContext taskContext;

    public TaskService(TaskContext taskContext) {
        this.taskContext = taskContext;
    }
}

很多人会以为每次调用 TaskService 都能获得新的 TaskContext。实际上,TaskService 是单例,只创建和注入一次,因此它会一直持有创建当时注入的那个 prototype 对象。

需要每次获取新对象时,可以使用 ObjectProvider

@Service
public class TaskService {

    private final ObjectProvider<TaskContext> provider;

    public TaskService(ObjectProvider<TaskContext> provider) {
        this.provider = provider;
    }

    public void execute() {
        TaskContext context = provider.getObject();
        System.out.println(context);
    }
}

这道题考查的本质是:

依赖注入发生在宿主 Bean 创建时,不会在每次业务方法调用时自动重新注入。


十、为什么 Spring 默认提前创建单例 Bean?

默认情况下,ApplicationContext 会在初始化阶段提前创建非懒加载的 singleton Bean。

为什么不全部等到第一次使用时再创建?

因为提前创建可以在启动阶段发现:

  • 依赖缺失;
  • 同类型 Bean 冲突;
  • 配置属性非法;
  • 构造方法执行失败;
  • 循环依赖;
  • 外部环境不完整。

这相当于应用启动时先进行一次“全局装配检查”。如果启动成功,主要单例组件已经完成验证。

可以使用 @Lazy 延迟创建:

@Lazy
@Service
public class ExpensiveReportService {

    public ExpensiveReportService() {
        System.out.println("创建 ExpensiveReportService");
    }
}

它通常会在第一次被请求时创建。

但是,如果它是某个非懒加载单例的直接依赖,Spring为了完成那个单例的注入,仍然可能在启动阶段创建它。

因此:

@Lazy 改变创建时机
@Scope 改变实例提供策略
二者都不会自动解决线程安全问题

十一、Bean 生命周期:AOP 和事务是怎样接进来的?

11.1 实例化和初始化不是一回事

实例化主要是调用构造方法得到原始 Java 对象;初始化则发生在依赖注入之后,包括生命周期回调和 Bean 后置处理。

这解释了一个常见问题:为什么字段注入的属性不能在构造方法里使用?

@Component
public class DemoBean {

    @Autowired
    private BlogService blogService;

    public DemoBean() {
        System.out.println(blogService); // 此时通常还是 null
    }
}

构造方法执行时,对象才刚被实例化,字段注入还没有发生。

11.2 生命周期主线

读取 BeanDefinition

调用构造方法实例化

属性填充与依赖注入

Aware 接口回调

BeanPostProcessor 初始化前处理

执行初始化回调

BeanPostProcessor 初始化后处理

必要时返回代理对象

Bean 对外提供服务

容器关闭时执行销毁回调

11.3 用代码观察初始化与销毁

@Component
public class LifecycleBean {

    public LifecycleBean() {
        System.out.println("1. 构造方法");
    }

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

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

注意导包来自 Jakarta:

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

11.4 多种生命周期回调的典型顺序

如果同一个 Bean 同时使用不同的初始化机制,典型顺序是:

@PostConstruct
    ↓
InitializingBean.afterPropertiesSet()
    ↓
自定义 initMethod

销毁时典型顺序是:

@PreDestroy
    ↓
DisposableBean.destroy()
    ↓
自定义 destroyMethod

现代业务代码通常优先使用 @PostConstruct@PreDestroy,因为不需要实现 Spring 专用接口。

11.5 BeanPostProcessor 为什么重要

BeanPostProcessor 可以在 Bean 初始化前后处理对象。它不仅能修改属性,还可以返回另一个对象。

当某个 Bean 需要 AOP 增强时,后置处理器可以创建代理对象:

原始 BlogServiceImpl
        ↓
检查是否需要事务、缓存或切面增强
        ↓
创建代理对象
        ↓
容器最终保存并注入代理对象

因此,其他组件拿到的 BlogService 不一定是最初通过构造方法创建的原始对象,而可能是代理。

这为后续理解 @Transactional 打下基础:

  • 事务通常由代理在方法调用前后开启和提交;
  • 类内部自调用可能绕开代理;
  • 被注入的对象类型判断需要考虑代理;
  • AOP 能力与 Bean 生命周期密切相关。

11.6 prototype Bean 的销毁

Spring主要负责 prototype Bean 的创建、注入和初始化。对象交给调用方后,容器通常不会继续追踪它,也不会在容器关闭时统一执行其完整销毁流程。

如果 prototype Bean 持有文件、连接等资源,调用方需要负责清理。


十二、循环依赖:能解决,不等于应该保留

假设:

class A {
    A(B b) {}
}

class B {
    B(A a) {}
}

创建 A 必须先得到 B,创建 B 又必须先得到 A,两个对象都无法率先完成构造。这就是无法解析的构造器循环依赖。

Setter 或字段循环依赖在部分条件下可能通过提前暴露引用等机制处理,但这不代表设计合理。涉及 AOP 代理、不同作用域和复杂初始化后,循环关系会进一步增加不一致风险。

真正应该检查的是:

  • A 和 B 是否职责混乱;
  • 能否抽取第三个协调组件;
  • 能否通过事件发布解除双向调用;
  • 依赖方向是否违反分层架构。

构造方法注入让循环依赖直接失败,反而能更早暴露架构问题。


十三、把一次 Bean 创建过程完整串起来

假设项目中存在:

@Service
public class BlogServiceImpl implements BlogService {

    private final BlogMapper blogMapper;

    public BlogServiceImpl(BlogMapper blogMapper) {
        this.blogMapper = blogMapper;
    }
}

Spring 启动时,可以按下面的过程理解:

  1. 组件扫描发现 BlogServiceImpl
  2. Spring将类信息转换成 BeanDefinition
  3. 容器准备创建非懒加载单例 Bean;
  4. 分析构造方法,发现需要 BlogMapper
  5. 从容器中取得或创建 BlogMapper
  6. 调用构造方法创建 BlogServiceImpl 原始对象;
  7. 完成属性填充、Aware 回调和初始化;
  8. Bean 后置处理器检查是否需要 AOP 增强;
  9. 如果需要事务等能力,创建代理对象;
  10. 容器保存最终可用的单例对象;
  11. 创建 BlogController 时,将最终对象注入构造方法。

所以,“Spring 自动注入 Service”背后不是简单从 Map 取对象,而是一套配置解析、依赖图构建、对象创建、生命周期管理和代理增强流程。


十四、经典面试问法与回答

14.1 什么是 IoC?

IoC 是控制反转,即将对象创建、依赖查找和对象装配的控制权从业务对象转移给容器,使对象构建和业务使用解耦。

14.2 什么是 DI?

DI 是依赖注入,是 IoC 的主要实现方式。对象通过构造方法、工厂方法参数或属性声明依赖,容器创建 Bean 时完成解析和注入。

14.3 BeanFactory 和 ApplicationContext 有什么区别?

BeanFactory 提供基础 Bean 管理能力;ApplicationContext 在其基础上增加 AOP、事件、资源、国际化、环境和 Web 上下文等应用级能力。Spring Boot 日常开发主要围绕 ApplicationContext

14.4 @Component 和 @Bean 有什么区别?

@Component 标在类上,通过组件扫描注册,适合自己编写的组件;@Bean 标在工厂方法上,适合第三方类、复杂创建逻辑和同类型多实例。二者最终都会向容器贡献 BeanDefinition。

14.5 为什么推荐构造方法注入?

它能保证必需依赖在对象创建时完整,支持 final 字段,便于单元测试,还能尽早暴露依赖过多和循环依赖问题。

14.6 @Autowired 和 @Resource 有什么区别?

@Autowired 是 Spring 注解,主要按照类型解析;@Resource 是 Jakarta 标准注解,更强调按名称装配。多个候选对象可结合 @Qualifier@Primary 处理。

14.7 Spring singleton 和单例模式一样吗?

不完全一样。Spring singleton 是每个容器、每份 BeanDefinition 共享一个实例;经典单例模式通常将实例唯一性写进类本身。

14.8 单例 Bean 线程安全吗?

不一定。Spring只管理实例数量,不保证对象内部可变成员变量线程安全。Controller 和 Service 通常应该保持无状态。

14.9 prototype 注入 singleton 后,每次调用都是新对象吗?

不是。依赖注入通常只在 singleton 创建时发生一次,它会一直持有当时注入的 prototype 对象。需要每次获取新对象时,可以使用 ObjectProvider 等方式。

14.10 为什么 Spring 默认提前创建单例 Bean?

为了在应用启动阶段尽早发现依赖缺失、配置错误、候选冲突和环境异常,避免错误延迟到第一次业务访问时才暴露。

14.11 Bean 生命周期的核心步骤是什么?

BeanDefinition 解析、实例化、依赖注入、Aware 回调、初始化前处理、初始化回调、初始化后处理、对外使用和销毁。AOP 代理通常在 Bean 后置处理阶段生成。

14.12 Spring 能解决循环依赖吗?

构造方法循环依赖无法正常解析;Setter 或字段循环依赖在部分条件下可能处理,但不应该依赖该机制掩盖架构设计问题。


十五、现代 Spring Boot 项目的实践建议

  1. Controller、Service 等自定义组件使用语义明确的 stereotype 注解;
  2. 第三方对象和复杂对象使用 @Configuration@Bean
  3. 必需依赖优先使用构造方法注入;
  4. 多个同类型 Bean 使用 @Qualifier@Primary 明确选择;
  5. 单例 Controller 和 Service 尽量设计为无状态对象;
  6. 不要在普通业务代码中到处调用 ApplicationContext#getBean()
  7. 不要用字段注入或 Setter 注入掩盖循环依赖;
  8. 区分 @Lazy、作用域和线程安全三个问题;
  9. 记住容器最终提供的 Bean 可能是代理,而不是原始对象;
  10. 短作用域 Bean 进入长作用域 Bean 时,要考虑代理或延迟获取。

十六、总结

IoC 的价值从来不只是“少写几个 new”,而是把对象构建和业务逻辑分开。

业务类只负责表达:

我是谁
我需要谁
我负责什么业务

Spring 容器负责:

读取配置元数据
生成 BeanDefinition
分析依赖关系
创建和组装对象
执行初始化回调
生成必要的代理
按照作用域管理对象
在容器关闭时清理资源

最终可以用六句话收束:

IoC:把对象创建和依赖管理权交给容器
DI:由容器把对象需要的依赖传进来
BeanDefinition:描述对象如何创建和管理
BeanFactory:根据定义创建和获取 Bean
ApplicationContext:承载完整 Spring 应用运行环境
BeanPostProcessor:在生命周期中继续处理甚至替换 Bean

理解到这里,后面学习 AOP、事务、Spring Boot 自动配置乃至 Spring AI 时,很多概念都会自然接上:模型客户端、向量数据库、Advisor、Tool 和 MCP 组件,最终同样要以 Bean 的形式进入容器,并通过同一套 IoC 与 DI 机制完成组合。


参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值