【Spring】一文真正吃透 IoC 与 DI:从 new 的耦合问题到 Bean 创建全流程
本文基于 Java 21 与现代 Spring Boot 的常见写法,从一个普通博客业务出发,逐步解释 IoC、DI、
ApplicationContext、BeanDefinition、@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,现在交给 Springnew。
这句话适合入门,却解释不了真正的工程问题:
- 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 把依赖从外部传进来
我们先不使用 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 生命周期中创建代理对象,再把代理交给其他组件使用。
完整主线如下:
可以把容器理解成一座自动化装配工厂:
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:完整的应用运行环境
ApplicationContext 是 BeanFactory 的子接口。在 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);
}
这里 mainUser 和 user1 指向同一个 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;
}
可以把依赖解析过程简化为:
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 生命周期主线
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 启动时,可以按下面的过程理解:
- 组件扫描发现
BlogServiceImpl; - Spring将类信息转换成
BeanDefinition; - 容器准备创建非懒加载单例 Bean;
- 分析构造方法,发现需要
BlogMapper; - 从容器中取得或创建
BlogMapper; - 调用构造方法创建
BlogServiceImpl原始对象; - 完成属性填充、Aware 回调和初始化;
- Bean 后置处理器检查是否需要 AOP 增强;
- 如果需要事务等能力,创建代理对象;
- 容器保存最终可用的单例对象;
- 创建
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 项目的实践建议
- Controller、Service 等自定义组件使用语义明确的 stereotype 注解;
- 第三方对象和复杂对象使用
@Configuration与@Bean; - 必需依赖优先使用构造方法注入;
- 多个同类型 Bean 使用
@Qualifier或@Primary明确选择; - 单例 Controller 和 Service 尽量设计为无状态对象;
- 不要在普通业务代码中到处调用
ApplicationContext#getBean(); - 不要用字段注入或 Setter 注入掩盖循环依赖;
- 区分
@Lazy、作用域和线程安全三个问题; - 记住容器最终提供的 Bean 可能是代理,而不是原始对象;
- 短作用域 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 机制完成组合。
参考资料
- Spring Framework:Introduction to the Spring IoC Container and Beans
- Spring Framework:Bean Overview
- Spring Framework:Dependency Injection
- Spring Framework:Basic Concepts - @Bean and @Configuration
- Spring Framework:Using the @Bean Annotation
- Spring Framework:Using @Autowired
- Spring Framework:Bean Scopes
- Spring Framework:Customizing the Nature of a Bean
- Spring Framework:Lazy-initialized Beans

723

被折叠的 条评论
为什么被折叠?



