SpringBoot 向 IOC 容器注册组件的两种姿势:@Configuration 与 @Import
目录
- 一、引言
- [二、@Configuration + @Bean:声明式注册](#二、@Configuration + @Bean:声明式注册)
- [2.1 基本用法:用 Java 配置类替代 XML](#2.1 基本用法:用 Java 配置类替代 XML)
- [2.2 在启动类里把容器中的bean打印出来](#2.2 在启动类里把容器中的bean打印出来)
- [2.3 从容器中获取的 bean 是单例的](#2.3 从容器中获取的 bean 是单例的)
- [2.4 配置类本身也是组件,而且是个 CGLIB 代理对象](#2.4 配置类本身也是组件,而且是个 CGLIB 代理对象)
- [2.5 proxyBeanMethods:Full 模式与 Lite 模式](#2.5 proxyBeanMethods:Full 模式与 Lite 模式)
- [2.6 组件依赖:为什么必须用 Full 模式](#2.6 组件依赖:为什么必须用 Full 模式)
- [2.7 既然 Full 这么稳,Lite 模式存在的意义是什么](#2.7 既然 Full 这么稳,Lite 模式存在的意义是什么)
- 三、@Import:更灵活的批量注册
- [3.1 源码:value 属性](#3.1 源码:value 属性)
- [3.2 用法一:导入普通类](#3.2 用法一:导入普通类)
- [3.3 用法二:导入配置类](#3.3 用法二:导入配置类)
- [3.4 用法三:实现 ImportSelector 接口](#3.4 用法三:实现 ImportSelector 接口)
- [3.5 用法四:实现 ImportBeanDefinitionRegistrar 接口](#3.5 用法四:实现 ImportBeanDefinitionRegistrar 接口)
- [3.6 两个接口的意义](#3.6 两个接口的意义)
- 四、四种注册方式怎么选
- 五、总结
一、引言
Spring想让一个对象交给 Spring 管理,最早的做法是在 XML 里写 <bean> 标签。到了 SpringBoot 时代,XML 基本退出了历史舞台,取而代之的是注解驱动的配置方式。
那在 SpringBoot 里,我们到底有哪几种方式可以把组件注册进 IOC 容器?
@Configuration+@Bean------ 最常用的声明式注册,以及它背后的 CGLIB 代理与proxyBeanMethods属性;@Import------ 更灵活的批量注册
二、@Configuration + @Bean:声明式注册
2.1 基本用法:用 Java 配置类替代 XML
先准备两个普通的 POJO,User 和 Cat:
java
public class User {
private String username;
private Integer age;
// 省略 getter / setter
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", age=" + age +
'}';
}
}
java
public class Cat {
private String name;
private String color;
// 省略 getter / setter
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
'}';
}
}
然后编写一个 Java 配置类,把它们注册进容器:
java
@Configuration
public class MyConfig {
@Bean
public User user(){
User user = new User();
user.setUsername("eric");
user.setAge(18);
return user;
}
@Bean
public Cat cat(){
Cat cat = new Cat();
cat.setName("大橘");
cat.setColor("橘色");
return cat;
}
}
两个注解的释义:
@Configuration:告诉 SpringBoot 这是一个配置类,等同于原生 Spring 项目中配置文件(XML)的作用;@Bean:向 Spring 容器中添加组件,类似原生 Spring 中 XML 里的<bean>标签。方法名相当于 bean 的 id,方法返回值就是组件在容器中的实例。
2.2 在启动类里把容器中的bean打印出来
java
@SpringBootApplication
public class App {
public static void main(String[] args) {
// 获得 ioc 容器
ConfigurableApplicationContext context =
SpringApplication.run(App.class, args);
// 获取所有 bean 的名称的集合
String[] beanDefinitionNames =
context.getBeanDefinitionNames();
// 循环打印 bean 的名称
for (String name : beanDefinitionNames) {
System.out.println(name);
}
}
}
观察控制台输出(节选):
text
app
org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory
myConfig
helloController
user ← 这两个 bean 已经交给 spring 容器管理了
cat ←
org.springframework.boot.autoconfigure.AutoConfigurationPackages
user 和 cat 出现。说明注册成功。
2.3 从容器中获取的 bean 是单例的
继续测试,在启动类中追加如下代码:
java
User user01 = context.getBean("user", User.class);
User user02 = context.getBean("user", User.class);
System.out.println("组件:" + (user01 == user02));
运行结果:
text
组件:true
两次 getBean 拿到的是同一个对象。由此可知:我们获取的组件就是从 Spring 容器中获取的,并且默认是单实例(singleton)的 bean。
2.4 配置类本身也是组件,而且是个 CGLIB 代理对象
前面控制台里出现了 myConfig,说明配置类自己也是一个组件,同样交给了 Spring 容器管理 。这是为什么?点开 @Configuration 的源码就明白了:
java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // ← 关键:@Configuration 本身就被 @Component 标注
public @interface Configuration {
@AliasFor(annotation = Component.class)
String value() default "";
boolean proxyBeanMethods() default true;
}
@Configuration 是被 @Component 元注解标注的,所以配置类天然就是一个组件。
那容器里管理的这个"配置类组件"长什么样?我们把它取出来打印一下:
java
MyConfig myConfig = context.getBean(MyConfig.class);
System.out.println(myConfig);
输出结果:
text
com.xq.config.MyConfig$$EnhancerBySpringCGLIB$$6064970@3e681bc
注意类名里的 EnhancerBySpringCGLIB ------ 这是一个基于 CGLIB 字节码增强的代理对象! 也就是说,Spring 容器里管理的并不是 MyConfig 的原始实例,而是它的代理对象(增强后的对象)。
为什么要代理?
2.5 proxyBeanMethods:Full 模式与 Lite 模式
@Configuration 有一个非常重要的属性:
java
@Configuration(proxyBeanMethods = true)
这个值默认为 true。我们来实测它的作用。在启动类中添加:
java
User user1 = myConfig.user();
User user2 = myConfig.user();
System.out.println(user1 == user2);
注意,这次我们不是通过 context.getBean(),而是直接调用配置类的 user() 方法。运行结果:
text
true
为什么手动调用方法两次,拿到的还是同一个对象?因为我们调用的并不是原始的 MyConfig.user(),而是通过它在容器中的 CGLIB 代理对象在调方法。代理逻辑会拦截方法调用,SpringBoot 总会先检查这个组件是否已在容器中存在:存在就直接返回容器里的那个,不存在才真正执行方法体创建。
现在把属性改成 false:
java
@Configuration(proxyBeanMethods = false)
再次运行同样的代码,结果变成:
text
false
也就是说,关闭代理之后,每次调用 @Bean 方法都会执行方法体,调用多少次就新创建多少个对象。
这两种行为有专门的名字:
- Full 模式 (
proxyBeanMethods = true):保证每个@Bean方法无论被调用多少次,返回的组件都是单实例的; - Lite 模式 (
proxyBeanMethods = false):每个@Bean方法每被调用一次,返回的组件都是新创建的。
2.6 组件依赖:为什么必须用 Full 模式
proxyBeanMethods 这个属性,本质上是为了解决一个问题:组件依赖。
我们改造一下 User,让它持有一只猫:
java
public class User {
private String username;
private Integer age;
private Cat cat;
// 省略 getter / setter
}
配置类中,user() 方法内部直接调用 cat() 来完成依赖装配:
java
@Configuration(proxyBeanMethods = true)
public class MyConfig {
@Bean
public User user(){
User user = new User();
user.setUsername("eric");
user.setAge(18);
user.setCat(cat()); // ← 组件依赖:user 依赖 cat
return user;
}
@Bean
public Cat cat(){
Cat cat = new Cat();
cat.setName("大橘");
cat.setColor("橘色");
return cat;
}
}
测试:user 里的猫,和容器里的猫,是同一只吗?
java
User user = context.getBean("user", User.class);
Cat cat = context.getBean("cat", Cat.class);
System.out.println(user.getCat() == cat);
在 proxyBeanMethods = true(Full 模式)下,结果为:
text
true
改成 proxyBeanMethods = false(Lite 模式)再测,结果为:
text
false
Lite 模式下,user() 里那句 cat() 是一次普通的方法调用,会 new 出一只全新的猫------它和容器里的 cat bean 是两个对象,单例语义被破坏了。
结论:只要 @Bean 方法之间存在相互调用(组件依赖),就必须使用 Full 模式(默认值);没有依赖关系时,可以考虑 Lite 模式。
2.7 既然 Full 这么稳,Lite 模式存在的意义是什么
性能。Full 模式有两笔开销:
- 启动期:Spring 要为配置类生成 CGLIB 子类,配置类越多,生成字节码的开销越大,拖慢启动速度;
- 运行期 :每次调用
@Bean方法都要走代理拦截逻辑,去容器里检查 bean 是否存在。
而 Lite 模式跳过了整个代理环节,配置类就是个普普通通的类,启动更快、调用更轻。所以实践中的选择逻辑是:
- 配置类中的
@Bean方法互不调用 → 放心标proxyBeanMethods = false,白赚一份启动性能; @Bean方法之间有调用关系 → 保持默认的 Full 模式。
查看SpringBoot 自带的自动配置类源码,你会发现大量的 @Configuration(proxyBeanMethods = false) ------ 官方为了优化启动速度,能用 Lite 就用 Lite。
三、@Import:更灵活的批量注册
@Configuration + @Bean 适合注册我们自己写的、需要逐个定制属性的组件。但有些场景下,我们想成批地、甚至按条件动态地 往容器里导入类,这时就轮到 @Import 出场了。
3.1 源码:value 属性
java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
Class<?>[] value();
}
@Import 的定义非常简单,只有一个 value 属性,类型是 Class 数组。结合官方注释,可以提炼出这些要点:
- 可以同时导入多个
@Configuration配置类、ImportSelector实现、ImportBeanDefinitionRegistrar实现,以及普通类(4.2 版本开始支持); @Import的功能与 XML 中的<import/>标签等效;- 可以在类级别声明,也可以作为元注解使用;
- 如果需要导入 XML 或其他非 bean 定义资源,请改用
@ImportResource注解。
也就是说,小小一个 value,根据传入的 Class 类型不同,衍生出四种用法。下面逐一演示。
3.2 用法一:导入普通类
先定义一个普通类(注意,它身上没有任何 Spring 注解):
java
public class Animal {
private String name;
// 省略 getter / setter
@Override
public String toString() {
return "Animal{" +
"name='" + name + '\'' +
'}';
}
}
用 @Import 把它导入:
java
@Import(value = {Animal.class})
public class MyConfig {
}
启动后打印容器中所有 bean 名称,控制台输出(节选):
text
myConfig
helloController
com.xq.pojo.Animal ← 类的全限定名
user
cat
可以发现一个有趣的细节:使用 @Import 导入的 bean,名称默认是类的全限定名 ,而不是 @Bean 方式下的方法名、也不是 @Component 方式下的类名首字母小写。
3.3 用法二:导入配置类
定义一个配置类:
java
@Configuration
public class Student {
}
通过 @Import 导入它:
java
@Configuration(proxyBeanMethods = true)
@Import(value = {Animal.class, Student.class})
public class MyConfig {
}
测试:
java
@SpringBootApplication
public class App {
public static void main(String[] args) {
ConfigurableApplicationContext context =
SpringApplication.run(App.class, args);
String[] beanDefinitionNames =
context.getBeanDefinitionNames();
for (String name : beanDefinitionNames) {
System.out.println(name);
}
Student student = context.getBean(Student.class);
System.out.println(student);
}
}
输出:
text
com.xq.pojo.Student$$EnhancerBySpringCGLIB$$231b5261@5f574cc2
观察结论:
- bean 名称同样是默认规则生成的;
- 类名里又出现了熟悉的
EnhancerBySpringCGLIB------ 被@Configuration标注的类,即使是通过 @Import 导入的,依然会被 CGLIB 代理
3.4 用法三:实现 ImportSelector 接口
前两种用法都是"写死"要导入哪些类。如果想在运行时动态决定 导入什么,就需要 ImportSelector。
第一步,定义一个需要被导入的 bean:
java
public class ObjectA {
public void objectA(){
System.out.println("objectA");
}
}
第二步,创建一个类实现 ImportSelector 接口,重写 selectImports 方法。方法返回值就是需要导入的类的全限定名数组:
java
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 返回的是需要导入类的全限定名的数组
return new String[]{ObjectA.class.getName()};
}
}
第三步,导入这个 Selector:
java
@Import(value = {MyImportSelector.class})
public class MyConfig {
}
测试:
java
@SpringBootApplication
public class App {
public static void main(String[] args) {
ConfigurableApplicationContext context =
SpringApplication.run(App.class, args);
ObjectA obj = context.getBean(ObjectA.class);
System.out.println(obj);
}
}
输出:
text
com.xq.pojo.ObjectA@55a8dc49
ObjectA 成功进入容器。注意MyImportSelector 自己并不会被注册成 bean,它只是一个"中间人",真正进容器的是它返回的那批类。由于 selectImports 的返回值是我们写代码算出来的,这里就有了无限的想象空间------读配置文件、扫描注解、做条件判断,都可以。
3.5 用法四:实现 ImportBeanDefinitionRegistrar 接口
ImportSelector 只能决定"导入谁",如果还想控制怎么注册 ------比如自定义 bean 的名称、设置作用域、注册前先做存在性检查------就要用更底层的 ImportBeanDefinitionRegistrar,直接操作 BeanDefinitionRegistry。
第一步,创建一个需要导入的 bean:
java
public class ObjectB {
public void objectB(){
System.out.println("objectB");
}
}
第二步,实现 ImportBeanDefinitionRegistrar 接口:
java
public class ImportConfig implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
// containsBeanDefinition:判断容器中是否存在指定的 bean 定义,true 为存在
boolean flag = registry.containsBeanDefinition(ObjectB.class.getName());
if (!flag) {
RootBeanDefinition beanDefinition = new RootBeanDefinition(ObjectB.class);
// 注册 BeanDefinition,并指定 beanName
registry.registerBeanDefinition("objectB", beanDefinition);
}
}
}
第三步,导入:
java
@Import(value = {ImportConfig.class})
public class MyConfig {
}
测试:
java
@SpringBootApplication
public class App {
public static void main(String[] args) {
ConfigurableApplicationContext context =
SpringApplication.run(App.class, args);
ObjectB obj = context.getBean(ObjectB.class);
System.out.println(obj);
}
}
输出:
text
com.xq.pojo.ObjectB@38d5b107
和 ImportSelector 相比,这里我们亲手 new 了 RootBeanDefinition、亲手指定了 beanName(objectB,不再是全限定名)、还在注册前做了一次"容器里有没有"的判断------注册过程的每一个环节都在我们掌控之中,这是四种用法里自由度最高的一种。
3.6 两个接口的意义
ImportSelector 和 ImportBeanDefinitionRegistrar 看起来用法繁琐,日常业务开发也很少直接写。但SpringBoot 的自动配置,底层正是靠它们撑起来的
四、四种注册方式怎么选
| 方式 | bean 名称默认值 | 特点 | 典型场景 |
|---|---|---|---|
@Configuration + @Bean |
方法名 | 可逐个定制属性,配合 Full 模式保证单例 | 注册第三方库的类、需要复杂初始化逻辑的组件 |
@Import 普通类 / 配置类 |
类的全限定名 | 一行注解批量导入,简单直接 | 把分散的配置类聚合到一个入口 |
@Import + ImportSelector |
类的全限定名 | 运行时动态计算导入哪些类 | 自动配置、按需批量装配 |
@Import + ImportBeanDefinitionRegistrar |
自己指定 | 手工操作 BeanDefinition,自由度最高 | 自定义 beanName、注册前置检查、框架级开发 |
一句话版本:业务代码用 @Bean,聚合配置用 @Import 导类,写框架/starter 用 Selector 和 Registrar。
五、总结
@Configuration标注的类等同于一份 XML 配置文件,@Bean方法等同于<bean>标签,方法名是 bean 的 id,返回值是容器中的实例,默认单例;- 配置类本身也是组件(因为
@Configuration被@Component标注),且容器中管理的是它的 CGLIB 代理对象; proxyBeanMethods = true为 Full 模式 ,代理保证@Bean方法无论调用多少次都返回容器中的同一个单例;false为 Lite 模式 ,每次调用都新建对象,但省去代理开销、启动更快------有组件依赖必须 Full,没有依赖推荐 Lite;@Import只有一个 Class 数组属性,却有四种用法:导入普通类、导入配置类、ImportSelector、ImportBeanDefinitionRegistrar;前两种导入的 bean 名称默认是类的全限定名;ImportSelector动态决定"导入谁",ImportBeanDefinitionRegistrar进一步掌控"怎么注册",二者是 SpringBoot 自动配置 与各类@EnableXxx注解的底层基石。