Spring 容器的本质是一个管理对象(Bean)的装配工厂。要让容器接管某个类的实例化与生命周期,首先得告诉容器“哪些类需要被你管”。这就是组件注册。Spring 提供了一套层次分明的注解来完成这件事:从类级别的模式注解,到方法级别的 @Bean,再到按条件精细控制的 @Conditional,共同构成了一张灵活的注册网。
8.2.1 模式注解:把类直接交给容器
最常用的注册方式是在类上标注 @Component 及其衍生注解,让容器通过类路径扫描自动发现并加载。
| 注解 | 语义 | 适用场景 |
|------|------|----------|
| @Component | 通用组件 | 无法归类到更具体注解的工具类 |
| @Controller | 控制器组件 | Spring MVC 的控制器层 |
| @Service | 业务逻辑组件 | Service 层,无额外行为但增强可读性 |
| @Repository | 数据访问组件 | DAO 层,额外提供持久层异常翻译 |
从技术角度看,@Service、@Controller、@Repository 在源码层面都被 @Component 元注解标注,本质上是完全等价的。那为什么还要区分?为了语义清晰和框架增强。例如:
@Repository会被 Spring 自动挂接持久层异常转换器,将数据库相关异常封装为DataAccessException。@Controller会被 Spring MVC 识别为处理 HTTP 请求的处理器,支持请求映射等功能。- 在 AOP 切点表达式中,也可以按注解类型进行精确拦截,比如
@within(org.springframework.stereotype.Service)拦截所有 Service 层方法。
使用示例
@Service
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
// ...
}
@Repository
public class OrderRepository {
// ...
}
默认情况下,这些注解不会自动生效,必须配合 @ComponentScan 或在 Spring Boot 应用中通过 @SpringBootApplication 间接启用扫描。扫描范围默认是该注解所在类的包及其子包。如果需要精确控制,可以在 @ComponentScan 中指定 basePackages 或使用 includeFilters、excludeFilters 过滤特定类。
8.2.2 @Configuration 与 @Bean:方法级别的组件声明
当需要将第三方库中的类注册为 Bean,或一个 Bean 的创建过程包含复杂逻辑(如参数设置、条件判断)时,类级别的注解就显得力不从心。这时需要用到 Java 配置:在 @Configuration 标注的类中,通过 @Bean 方法显式创建对象。
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/business");
config.setUsername("app");
config.setPassword("secret");
return new HikariDataSource(config);
}
}
这里有一个容易被忽视却非常重要的细节:@Configuration 类默认使用 CGLIB 代理(proxyBeanMethods = true)。这意味着容器内的 @Bean 方法调用会被拦截,即便在同一个配置类中多次调用 dataSource() 方法,始终返回的是同一个单例 Bean。如果需要每次调用都创建一个新实例(轻量模式),可以设置 proxyBeanMethods = false,此时配置类不会被代理,多个 @Bean 方法之间直接调用会执行原生 Java 语义,每次 new 一个新对象。
对比两种注册方式:
@Component+ 组件扫描:被动发现,类自己声明自己是一个组件。@Configuration+@Bean:主动声明,由开发者精确控制对象的创建步骤。
实际开发中二者常常配合使用:自己写的业务类使用 @Service、@Repository 等模式注解;引入的外部组件或需要定制化初始化的对象则用 @Bean 注册。
8.2.3 @Import:快速导入配置或组件
当需要将另一个配置类或普通组件引入当前容器,而不想让它依赖于组件扫描时,可以使用 @Import。它就像一个“快速通道”,直接告诉容器“也把那个类加载进来”。
常见用法包括:
- 引入普通配置类:
@Import(DataSourceConfig.class),让该类中的@Bean定义生效。 - 引入
ImportSelector实现:常用于框架内部的自动配置模块,根据条件动态返回要导入的类名数组。Spring Boot 的自动配置大量依赖此机制。 - 引入
ImportBeanDefinitionRegistrar实现:可在运行时通过编程方式注册 Bean,比ImportSelector拥有更高的灵活性。
一个直观的例子,当需要启用某个模块(如定时任务)时,通常只需一个注解:
@EnableScheduling
@Configuration
public class AppConfig {
}
而 @EnableScheduling 底层正是通过 @Import 导入了 SchedulingConfiguration 类,该配置类里又定义了一个 @Bean 方法注册了后置处理器。这种“注解驱动”的模式是 Spring 模块化集成的精妙设计。
8.2.4 条件化注册:按环境动态决定
真实项目中,并不是所有组件都要无条件创建。测试环境需要内存数据源,生产环境需要连接池——条件化注册让容器拥有了“判断力”。
Spring 提供了 @Conditional 注解,配合实现 Condition 接口的类,可在 Bean 注册前执行自定义匹配逻辑。Spring Boot 在此基础上,封装了一系列更易用的组合注解:
@ConditionalOnClass:类路径中存在指定类时才注册。@ConditionalOnMissingBean:容器中不存在指定类型的 Bean 时才注册。@ConditionalOnProperty:当配置文件中的某个属性满足指定条件时注册。@ConditionalOnExpression:基于 SpEL 表达式的结果决定。
实际案例——只在开发环境启用一个用于调试的 Bean:
@Bean
@Profile("dev")
public DataSource devDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
@Profile 本身就是一个 @Conditional 的扩展,它让同一套代码能够适配开发、测试、生产等不同环境,避免编写多份配置文件。
8.2.5 影响 Bean 定义的其他注解
除了注册本身,还有一些注解直接影响 Bean 的行为和生命周期,它们也属于组件注册体系的一部分:
@Lazy:延迟初始化,容器启动时不会立即创建该 Bean,而是等到第一次被注入或获取时才实例化,适合启动耗时长或不一定会用到的组件。@Scope:指定 Bean 的作用域,如singleton(单例,默认)、prototype(每次获取都创建新实例)、request、session等。在 Web 开发中,@Scope("request")可以将 Bean 的生命周期绑定到 HTTP 请求。@Primary:当有多个同类型的候选 Bean 时,标记为@Primary的 Bean 会优先被注入,避免因歧义导致启动失败。@DependsOn:强制指定当前 Bean 依赖于另一个 Bean 先初始化,尽管它们之间可能没有直接的字段注入关系。
这些注解往往配合 @Component 或 @Bean 使用,让容器对 Bean 的控制更加精细。
8.2.6 注册方式的选择策略
面对多种注册手段,实际开发中可以遵循一个简单的决策路径:
- 自己编写的业务类(Controller、Service、Repository)→ 使用模式注解
@Controller、@Service、@Repository,让组件扫描自动发现。 - 第三方类或需要复杂构造逻辑的对象(数据源、RestTemplate、线程池)→ 在
@Configuration类中用@Bean方法注册。 - 需要根据环境开关的组件→ 组合使用
@Profile、@ConditionalOnProperty等条件注解。 - 框架集成或启用模块→ 查找是否已有现成的
@EnableXxx注解,它内部已经封装了@Import逻辑。
遵循这些原则,可以在不牺牲灵活性的前提下,保持组件注册的清晰与可控。容器拥有了明确的“势力范围”,后续的依赖注入、AOP 增强、事务管理等才能准确作用到每一个目标 Bean 上。