说实话,我刚入行那会儿,看到满屏的 @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 容器。这能帮你彻底理解什么是“依赖注入”。

假设我们要管理两个类:UserServiceUserDao

第一步:定义接口和实现

// 这是一个接口,代表“谁能查用户”
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 启动过程中会执行这样一个逻辑:

  1. 扫描路径:Spring 会去你指定的包(比如 com.example.demo)及其子包下转悠。

  2. 找类:它通过反射(Reflection)加载所有的 .class 文件。

  3. 看注解:它会检查这些类头上有没有它认识的“标签”:

    • @Component(通用组件)
    • @Service(服务层,本质也是 Component)
    • @Repository(数据访问层,本质也是 Component)
    • @Controller / @RestController(控制层,本质也是 Component)
    • @Configuration(配置类,特殊处理,后面细说)
  4. 注册图纸:一旦发现有这些标签,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;
    }
}

为什么推荐?

  1. 不可变性:字段可以加 final,防止被意外修改。
  2. 易于测试:单元测试时,你可以直接 new UserService(mockUserDao),不需要启动容器。
  3. 杜绝空指针:依赖在对象创建时就必须存在,不存在就报错,而不是等到方法调用时才爆雷。

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

原理链

  1. @SpringBootApplication 注解里有一个 @EnableAutoConfiguration
  2. @EnableAutoConfiguration 导入一个选择器 AutoConfigurationImportSelector
  3. 这个选择器在启动时,会去 classpath 下扫描所有的 spring.factories 文件
  4. 找到 EnableAutoConfiguration 对应的所有配置类(如 WebMvcAutoConfiguration)。
  5. 这些配置类上通常带有 @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 就绪

最后,我们把整个过程串起来,形成一个完整的认知链条。

  1. 入口SpringApplication.run()
  2. 准备环境prepareEnvironment()。加载配置文件(application.properties/yml),构建 Environment 对象。
  3. 创建上下文createApplicationContext()。根据是否是 Web 应用,创建 AnnotationConfigServletWebServerApplicationContext 或普通上下文。
  4. 准备 Bean 工厂prepareBeanFactory()。设置 ClassLoader,注册一些必需的 Bean(如 EnvironmentPropertySources)。
  5. 执行后置处理器invokeBeanFactoryPostProcessors()
    • 这里会执行 ConfigurationClassPostProcessor
    • 关键点:解析 @Configuration 类,处理 @ComponentScan,扫描组件,注册 BeanDefinition
    • 关键点:处理 @PropertySource,加载配置属性。
  6. 注册 Bean 后置处理器registerBeanPostProcessors()。注册 AutowiredAnnotationBeanPostProcessor(处理 @Autowired)等。
  7. 初始化上下文refreshContext()
    • 触发 BeanFactoryPostProcessor
    • 实例化所有单例 Bean(finishBeanFactoryInitialization())。
    • 在这里,@Autowired@Value 等注解被真正处理,依赖被注入。
  8. 发布事件finishRefresh()。启动内嵌 Tomcat,发布 ApplicationReadyEvent

理解这个流程的价值: 如果你遇到“Bean 注入为 null”,你知道这通常发生在步骤 7,可能是你的 Bean 没有被扫描到(步骤 5 出问题),或者你的 @Autowired 位置不对。 如果你遇到“配置文件没生效”,你知道这可能在步骤 2 或 4。


七、 总结:给新手的一些真心话

写这篇文章,我是真心想帮大家在面试和工作中少踩坑。总结一下几个核心点:

  1. IoC 是外包:别自己 new 依赖,让容器给你。
  2. BeanDefinition 是图纸:容器先画图纸,再盖房子。
  3. @Component 家族是标记:告诉容器“扫描我”。
  4. @Configuration + @Bean 是手动装配:用于第三方库或复杂逻辑。
  5. 自动装配是条件判断spring.factories 列出候选,