Spring Bean、IOC/DI 与 Bean 注册机制指南

本文聚焦 Spring 容器中的 Bean、IOC、DI、作用域与注册机制,帮助你建立“对象如何进入容器、如何被注入、为什么能够被自动装配”的基础认知。

第一章 IOC、DI 与三层协作

1.1 三层协作中的依赖关系

为了代码清晰,后端一般会分成三层:

  • Controller(控制层):接收请求,响应数据,调用 Service。
  • Service(业务逻辑层):处理具体业务。
  • Dao / Mapper(数据访问层):直接操作数据库。

在 Spring 项目中,三层协作通常依赖容器来统一管理对象。

1.2 什么是 IOC

  • IOC(控制反转):对象的创建和管理权交给 Spring 容器。
  • 过去是业务代码自己 new 对象,现在是容器负责准备对象,再提供给业务代码使用。

1.3 什么是 DI

  • DI(依赖注入):容器自动把目标对象所需的依赖注入进去。
  • 常见方式包括字段注入、构造器注入和 Setter 注入。

示例:

Java
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;
}

@RestController
public class UserController {
    @Autowired
    private UserService userService;
}

理解这段代码时要抓住两点:

  • UserServiceImplUserController 自己不负责依赖对象的创建。
  • Spring 容器会先管理 Bean,再完成注入关系。

第二章 Bean 的声明与作用域

Bean 是 Spring 容器管理对象的基本单位。理解 Bean 的声明方式、作用域和初始化时机,是理解后续扩展机制的基础。

2.1 Bean 的五种作用域

作用域说明快捷注解
singleton默认值,容器内只有一个实例@Scope("singleton")
prototype每次获取时创建新的实例@Scope("prototype")
request每个 HTTP 请求创建一个实例@RequestScope
session每个 HTTP 会话创建一个实例@SessionScope
application每个 Web 应用创建一个实例@ApplicationScope

示例:

Java
@Scope("prototype")
@RestController
@RequestMapping("/depts")
public class DeptController {
}

2.2 @RequestScope 的典型用法

Java
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;

@Component
@RequestScope
public class UserContext {
    private String traceId;
    private Long currentUserId;

    public void setTraceId(String traceId) {
        this.traceId = traceId;
    }

    public String getTraceId() {
        return traceId;
    }

    public void setCurrentUserId(Long currentUserId) {
        this.currentUserId = currentUserId;
    }

    public Long getCurrentUserId() {
        return currentUserId;
    }
}

它适合保存一次请求中的上下文信息,例如当前用户、追踪 ID、租户编号等。

2.3 延迟初始化 @Lazy

默认情况下,单例 Bean 会在容器启动时创建。如果你希望第一次真正使用它时再初始化,可以使用 @Lazy

Java
@Lazy
@RestController
public class DeptController {
    public DeptController() {
        System.out.println("创建 DeptController 对象...");
    }
}

2.4 第三方 Bean 的声明

如果对象来自第三方库,通常不能直接在源码上加 @Component,这时可以通过 @Bean 把它交给容器管理。

Java
@Configuration
public class OSSConfig {

    @Bean("customOSS")
    public AliyunOSSOperator aliyunOSSOperator(AliyunOSSProperties ossProperties) {
        return new AliyunOSSOperator(ossProperties);
    }
}

第三章 Bean 注册机制

3.1 为什么第三方 Bean 有时不会生效

有些第三方工具包已经写了 @Component@Service,但项目引入依赖后仍然注入失败,核心原因通常不是注解失效,而是没有被扫描到

3.2 扩展组件扫描:@ComponentScan

如果你明确知道目标类所在的包路径,并且对方已经写好了组件注解,可以通过 @ComponentScan 扩展扫描范围。

Java
@SpringBootApplication
@ComponentScan(basePackages = {"com.demo", "com.example.token"})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

这种方式的特点是:

  • 适合项目内部模块较多、按包组织清晰的场景
  • 对包路径有耦合
  • 扫描范围越大,启动阶段需要处理的类也越多

3.3 主动导入:@Import

相比扫描,@Import 更像是显式告诉 Spring“请把这些类注册进来”。它适合导入明确的配置类、普通类或扩展类。

导入普通类

Java
@Import(TokenParser.class)
public class AppConfig {
}

导入配置类

Java
@Configuration
public class HeaderConfig {

    @Bean
    public HeaderParser headerParser() {
        return new HeaderParser();
    }

    @Bean
    public HeaderGenerator headerGenerator() {
        return new HeaderGenerator();
    }
}
Java
@SpringBootApplication
@Import(HeaderConfig.class)
public class Application {
}

导入 ImportSelector

Java
public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] {
            "com.example.token.TokenParser",
            "com.example.header.HeaderConfig"
        };
    }
}

导入 ImportBeanDefinitionRegistrar

Java
public class MyRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata,
            BeanDefinitionRegistry registry) {
        RootBeanDefinition beanDefinition = new RootBeanDefinition(TokenParser.class);
        registry.registerBeanDefinition("tokenParser", beanDefinition);
    }
}

3.4 @EnableXxx 的本质

很多注解看起来像是在“开启某项功能”,例如:

  • @EnableScheduling
  • @EnableAsync
  • @EnableCaching
  • @EnableAspectJAutoProxy

它们的核心思路通常是:通过 @Import 导入一组配置类或扩展类

Java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(HeaderConfig.class)
public @interface EnableHeaderConfig {
}

3.5 @ComponentScan@Import 如何选择

方式适合场景
@ComponentScan项目内部模块较多,希望按包统一扫描
@Import明确导入少量类、配置类、扩展类,或者封装第三方能力

实践中通常可以这样理解:

  • 业务代码内部更常用组件扫描
  • 第三方能力接入更常用 @Import
  • 做框架封装、starter 封装时更常用 @Import

本文小结

  • IOC 负责把对象创建与管理权交给容器,DI 负责把依赖注入到目标对象中。
  • Bean 是容器管理对象的基本单位,需要理解作用域、延迟初始化与第三方 Bean 注册。
  • @ComponentScan 适合按包扫描,@Import 适合显式导入。
  • @EnableXxx 的底层思想通常仍然建立在 @Import 之上。