说实话,我刚入行那会儿,看到满屏的 @Autowired、@Bean、@Component,脑子是一团浆糊。网上那些教程上来就丢一张巨大的注解关系图,然后告诉你“这个管依赖注入,那个管组件扫描”,看得我怀疑人生。结果呢?代码能跑,但一旦报错,连排查方向都找不到,因为根本不知道容器内部到底发生了什么。
今天咱们不聊那些干巴巴的概念,我把这层窗户纸捅破。咱们从头开始,像搭房子一样,看看 Spring 这栋“大厦”到底是怎么从地基(IoC 容器)一层层盖到精装修(Spring Boot 自动装配)的。看完这篇,你以后再见到这些注解,脑子里浮现的不再是符号,而是一个个生动的“办事员”和“仓库管理员”。
一、 破除迷信:IoC 容器到底是个啥?
首先,把“控制反转”这个词忘掉,它听起来太像哲学课了。我通常跟我的学生说,IoC 就是“外包”。
想象一下,你以前写 Java 代码,想用一个对象,你得自己去 new:
public class UserService {
private UserDao userDao = new UserDaoImpl(); // 你自己去创造,自己负责
}
这样写有啥毛病?毛病大了。哪天 UserDaoImpl 要换成 UserDaoMyBatisImpl,你不得把 UserService 里的代码全改一遍?业务逻辑和底层实现死死绑在一起,牵一发而动全身。
IoC(Inverse of Control,控制反转) 的核心思想就一句话:你把“创建对象”和“管理对象生命周期”的权利,交出去。 交给谁?交给容器。
1.1 手工搭建一个最小的 IoC 容器
别嫌麻烦,我们用最原生的 Java 代码,模拟一个最简单的 Spring 容器。这能帮你彻底理解什么是“依赖注入”。
假设我们要管理两个类:UserService 和 UserDao。
第一步:定义接口和实现
// 这是一个接口,代表“谁能查用户”
public interface UserDao {
void save(String user);
}
// 这是具体的实现,比如存数据库
public class JdbcUserDao implements UserDao {
@Override
public void save(String user) {
System.out.println("使用 JDBC 保存用户: " + user);
}
}
// 业务层,它需要一个 UserDao
public class UserService {
private UserDao userDao;
// 关键来了:我不直接 new,而是通过 setter 注入
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void add(String user) {
if (userDao != null) {
userDao.save(user);
}
}
}
第二步:编写一个简单的容器
这个容器其实就是一个 Map<String, Object>。Key 是 Bean 的名字,Value 是对象的实例。
import java.util.HashMap;
import java.util.Map;
public class SimpleIoCContainer {
// 这个 Map 就是我们的“仓库”,存放所有创建好的对象
private Map<String, Object> beans = new HashMap<>();
/**
* 注册 Bean:相当于告诉容器“我有这个对象,请帮我管理”
*/
public void registerBean(String name, Object bean) {
beans.put(name, bean);
}
/**
* 获取 Bean:相当于向容器“借”对象
*/
public Object getBean(String name) {
return beans.get(name);
}
}
第三步:组装
public class Main {
public static void main(String[] args) {
SimpleIoCContainer container = new SimpleIoCContainer();
// 1. 创建具体的实现类
UserDao userDao = new JdbcUserDao();
// 2. 创建业务类
UserService userService = new UserService();
// 3. 【关键步骤】依赖注入
// 以前是你自己 new 出来塞进去,现在是容器帮你“注入”
userService.setUserDao(userDao);
// 4. 把对象注册进容器(模拟 Spring 的做法)
container.registerBean("userDao", userDao);
container.registerBean("userService", userService);
// 5. 使用
UserService service = (UserService) container.getBean("userService");
service.add("张三"); // 输出:使用 JDBC 保存用户: 张三
}
}
看懂了吗?IoC 容器本质上就是一个巨大的工厂 + 仓库。 它帮你 new 对象,帮你把依赖关系配好,最后把配好关系的对象给你。
新手必坑提醒 1:很多新手觉得“依赖注入”很高深。其实只要记住一点:谁需要谁,就把这个“谁”的引用通过构造器或 setter 传进去,而不是自己在里面
new。
二、 Spring 如何“扫描”并“创建”Bean?
刚才那个 SimpleIoCContainer 是手写的,太累了。Spring 厉害的地方在于,它能自动把 Java 类变成容器里的 Bean。这靠的是两步:ClassPathScanningCandidateComponentProvider(扫描)和 BeanDefinition(定义)。
2.1 什么是 BeanDefinition?
这是 Spring 里最重要的概念之一,没有之一。
你可以把 BeanDefinition 想象成“建筑图纸”。
在容器真正去盖房子(创建对象)之前,它需要先有一张图纸。这张图纸上写着:
- 这个房子叫什么名字(Bean Name)
- 用什么材料盖(Class 类型)
- 是单家独院还是多人合租(Scope:Singleton 还是 Prototype)
- 需要什么家具(Dependencies)
在 Spring 源码里,它对应的是 org.springframework.beans.factory.support.BeanDefinition 接口。
2.2 组件扫描(Component Scanning)
当你写下 @SpringBootApplication 或者配置 <context:component-scan> 时,Spring 启动过程中会执行这样一个逻辑:
扫描路径:Spring 会去你指定的包(比如
com.example.demo)及其子包下转悠。找类:它通过反射(Reflection)加载所有的
.class文件。看注解:它会检查这些类头上有没有它认识的“标签”:
@Component(通用组件)@Service(服务层,本质也是 Component)@Repository(数据访问层,本质也是 Component)@Controller/@RestController(控制层,本质也是 Component)@Configuration(配置类,特殊处理,后面细说)
注册图纸:一旦发现有这些标签,Spring 就会创建一个对应的
BeanDefinition,注册到BeanDefinitionRegistry(注册表)里。
这时候,对象还没创建! 只是有了“计划”。
2.3 实例化 Bean(Instantiation)
等到容器启动完毕,或者你第一次 getBean 时(默认是单例,所以启动时就会创建好),Spring 才会根据 BeanDefinition 里的图纸,调用构造函数、处理属性注入,真正把对象 new 出来。
新手必坑提醒 2:
@Service、@Repository和@Component有什么区别? 在大多数情况下,没区别。它们都是@Component的衍生注解,作用完全一样:告诉 Spring “把我当成 Bean 管起来”。 那为什么还要分三个?为了语义清晰,让读代码的人一眼就知道这个类在架构里的角色(是控制器、服务还是 DAO)。如果你在 DAO 层用@Service,代码能跑,但不专业。
三、 依赖注入(DI)的三种姿势
理解了 Bean 是怎么来的,接下来就是怎么把依赖塞进去。Spring 支持三种方式,我按推荐程度排序:
3.1 构造器注入(Constructor Injection)—— 强烈推荐
@Service
public class UserService {
private final UserDao userDao;
// Spring 会自动调用这个构造器,把 userDao 传进来
@Autowired // 如果是单一构造器,甚至可以省略 @Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}
}
为什么推荐?
- 不可变性:字段可以加
final,防止被意外修改。 - 易于测试:单元测试时,你可以直接
new UserService(mockUserDao),不需要启动容器。 - 杜绝空指针:依赖在对象创建时就必须存在,不存在就报错,而不是等到方法调用时才爆雷。
3.2 Setter 注入
@Service
public class UserService {
private UserDao userDao;
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
场景:依赖可选时(比如某个插件是可选的,没有也能跑),或者需要循环依赖时(虽然循环依赖本身是个坏设计,但 Spring 能处理)。
3.3 字段注入(Field Injection)—— 最不推荐,但最常见
@Service
public class UserService {
@Autowired
private UserDao userDao; // 直接在字段上加注解
}
为什么我不推荐?
- 隐藏了依赖关系。看这个类的代码,你不知道它依赖什么,除非去翻代码。
- 难以测试。你必须通过反射或者启动整个 Spring 容器才能测试这个类。
- 容易写错:很多新手在这里写错,导致注入为 null,却不知道为什么。
新手必坑提醒 3:
@Autowired默认是按类型(byType)注入的。如果容器里有两个UserDao类型的 Bean,Spring 会报错(NoUniqueBeanDefinitionException)。这时候你需要配合@Qualifier("beanName")来指定具体用哪个。
四、 进阶:@Configuration 与 @Bean 的秘密
前面说了 @Component 系列,它们适合管理你自己写的类。但如果你要用第三方库的类(比如 Druid 数据源、Redis 客户端),你不能改人家的源码加注解,这时候怎么办?
答案是:@Configuration 和 @Bean。
4.1 配置类的工作原理
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
// 这里手动创建第三方对象
DruidDataSource ds = new DruidDataSource();
ds.setUrl("jdbc:mysql://localhost:3306/db");
return ds;
}
}
这里有一个非常重要的概念:代理机制。
当你使用 @Configuration 时,Spring 会创建一个CGLIB 代理(默认情况下)。这意味着什么?
假设你在 AppConfig 里写了这样的代码:
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
return new DruidDataSource();
}
@Bean
public DataSourceTransactionManager transactionManager() {
// 这里调用了同一个类里的 dataSource()
return new DataSourceTransactionManager(dataSource());
}
}
如果 @Configuration 没有代理,dataSource() 调用 dataSource() 会重新创建一个新的 DataSource 对象。
但因为有了 CGLIB 代理,Spring 会拦截这个调用,直接返回容器中已经创建好的那个单例 Bean。
结论:@Bean 方法之间的调用,默认是单例共享的。这是 Spring 保证单例完整性的关键机制。
新手必坑提醒 4:不要在
@Bean方法里手动new一个依赖它的 Bean,除非你确定你要的是新实例。正确做法是让 Spring 注入依赖,或者调用配置类里的其他@Bean方法(会被代理拦截)。
五、 终极揭秘:Spring Boot 自动装配原理
这是很多中级开发者的盲区。你写了一个 @SpringBootApplication,没配任何东西,Tomcat 起来了,数据库连上了,日志正常了。这是怎么做到的?
这就是自动装配(Auto-Configuration)。
5.1 核心魔法:spring.factories
Spring Boot 的自动装配并不是魔法,而是一个约定优于配置的工程实践。
当你引入一个 starter,比如 spring-boot-starter-web,它里面并没有写死如何配置 Tomcat。而是引入了 spring-boot-autoconfigure 依赖。
在这个 autoconfigure 的 jar 包里,有一个关键文件:
META-INF/spring.factories
打开这个文件,你会看到一行行配置:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration
原理链:
@SpringBootApplication注解里有一个@EnableAutoConfiguration。@EnableAutoConfiguration导入一个选择器AutoConfigurationImportSelector。- 这个选择器在启动时,会去 classpath 下扫描所有的
spring.factories文件。 - 找到
EnableAutoConfiguration对应的所有配置类(如WebMvcAutoConfiguration)。 - 这些配置类上通常带有
@ConditionalOnClass、@ConditionalOnMissingBean等条件注解。
5.2 条件注解:智能装配的关键
为什么引入 spring-boot-starter-data-jpa 才会自动配 JPA,而不引入时不会报错?
因为 DataSourceAutoConfiguration 类上有这样的注解:
@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDataSource.class }) // 只有当类路径上有这些类时才生效
@ConditionalOnBean(DataSource.class) // 只有当容器里有 DataSource 时才生效(可选)
public class DataSourceAutoConfiguration { ... }
这就是“自动”的真谛:Spring Boot 不是盲目地注入所有 Bean,而是根据你引入了什么依赖(classpath 里有什么类)和你配置了什么,智能地决定是否启用某个配置类。
5.3 实战:自己写一个自动装配
为了彻底搞懂,我们来手写一个简单的自动装配。
第一步:定义 Starter 依赖的项目(模拟)
public class MyAutoConfiguration {
@Bean
@ConditionalOnProperty(name = "my.app.enabled", havingValue = "true") // 只有配置文件中 my.app.enabled=true 时才装配
public MyService myService() {
return new MyService();
}
}
第二步:创建 spring.factories 文件
路径:src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyAutoConfiguration
第三步:在主项目中引用
当你把这个 starter jar 引入主项目,并在 application.properties 中写下 my.app.enabled=true,Spring Boot 启动时就会自动发现并注册 MyService Bean。
新手必坑提醒 5:自动装配生效的前提是类路径(classpath)上有对应的类。如果你引入的 starter 依赖没有传递过来,或者你引入了错误的版本,
@ConditionalOnClass检查不通过,自动装配就不会执行。排查自动装配不生效时,先检查mvn dependency:tree,看依赖是否真的拉取下来了。
六、 启动流程全景图:从 main 到 Bean 就绪
最后,我们把整个过程串起来,形成一个完整的认知链条。
- 入口:
SpringApplication.run()。 - 准备环境:
prepareEnvironment()。加载配置文件(application.properties/yml),构建 Environment 对象。 - 创建上下文:
createApplicationContext()。根据是否是 Web 应用,创建AnnotationConfigServletWebServerApplicationContext或普通上下文。 - 准备 Bean 工厂:
prepareBeanFactory()。设置 ClassLoader,注册一些必需的 Bean(如Environment、PropertySources)。 - 执行后置处理器:
invokeBeanFactoryPostProcessors()。- 这里会执行
ConfigurationClassPostProcessor。 - 关键点:解析
@Configuration类,处理@ComponentScan,扫描组件,注册BeanDefinition。 - 关键点:处理
@PropertySource,加载配置属性。
- 这里会执行
- 注册 Bean 后置处理器:
registerBeanPostProcessors()。注册AutowiredAnnotationBeanPostProcessor(处理@Autowired)等。 - 初始化上下文:
refreshContext()。- 触发
BeanFactoryPostProcessor。 - 实例化所有单例 Bean(
finishBeanFactoryInitialization())。 - 在这里,
@Autowired、@Value等注解被真正处理,依赖被注入。
- 触发
- 发布事件:
finishRefresh()。启动内嵌 Tomcat,发布ApplicationReadyEvent。
理解这个流程的价值:
如果你遇到“Bean 注入为 null”,你知道这通常发生在步骤 7,可能是你的 Bean 没有被扫描到(步骤 5 出问题),或者你的 @Autowired 位置不对。
如果你遇到“配置文件没生效”,你知道这可能在步骤 2 或 4。
七、 总结:给新手的一些真心话
写这篇文章,我是真心想帮大家在面试和工作中少踩坑。总结一下几个核心点:
- IoC 是外包:别自己
new依赖,让容器给你。 - BeanDefinition 是图纸:容器先画图纸,再盖房子。
@Component家族是标记:告诉容器“扫描我”。@Configuration+@Bean是手动装配:用于第三方库或复杂逻辑。- 自动装配是条件判断:
spring.factories列出候选,
