SpringBoot 向 IOC 容器注册组件的两种姿势:@Configuration 与 @Import

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 容器?

  1. @Configuration + @Bean ------ 最常用的声明式注册,以及它背后的 CGLIB 代理与 proxyBeanMethods 属性;
  2. @Import ------ 更灵活的批量注册

二、@Configuration + @Bean:声明式注册

2.1 基本用法:用 Java 配置类替代 XML

先准备两个普通的 POJO,UserCat:

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

usercat 出现。说明注册成功。

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 模式有两笔开销:

  1. 启动期:Spring 要为配置类生成 CGLIB 子类,配置类越多,生成字节码的开销越大,拖慢启动速度;
  2. 运行期 :每次调用 @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

观察结论:

  1. bean 名称同样是默认规则生成的;
  2. 类名里又出现了熟悉的 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 两个接口的意义

ImportSelectorImportBeanDefinitionRegistrar 看起来用法繁琐,日常业务开发也很少直接写。但SpringBoot 的自动配置,底层正是靠它们撑起来的

四、四种注册方式怎么选

方式 bean 名称默认值 特点 典型场景
@Configuration + @Bean 方法名 可逐个定制属性,配合 Full 模式保证单例 注册第三方库的类、需要复杂初始化逻辑的组件
@Import 普通类 / 配置类 类的全限定名 一行注解批量导入,简单直接 把分散的配置类聚合到一个入口
@Import + ImportSelector 类的全限定名 运行时动态计算导入哪些类 自动配置、按需批量装配
@Import + ImportBeanDefinitionRegistrar 自己指定 手工操作 BeanDefinition,自由度最高 自定义 beanName、注册前置检查、框架级开发

一句话版本:业务代码用 @Bean,聚合配置用 @Import 导类,写框架/starter 用 Selector 和 Registrar。

五、总结

  1. @Configuration 标注的类等同于一份 XML 配置文件,@Bean 方法等同于 <bean> 标签,方法名是 bean 的 id,返回值是容器中的实例,默认单例;
  2. 配置类本身也是组件(因为 @Configuration@Component 标注),且容器中管理的是它的 CGLIB 代理对象;
  3. proxyBeanMethods = trueFull 模式 ,代理保证 @Bean 方法无论调用多少次都返回容器中的同一个单例;falseLite 模式 ,每次调用都新建对象,但省去代理开销、启动更快------有组件依赖必须 Full,没有依赖推荐 Lite;
  4. @Import 只有一个 Class 数组属性,却有四种用法:导入普通类、导入配置类、ImportSelectorImportBeanDefinitionRegistrar;前两种导入的 bean 名称默认是类的全限定名;
  5. ImportSelector 动态决定"导入谁",ImportBeanDefinitionRegistrar 进一步掌控"怎么注册",二者是 SpringBoot 自动配置 与各类 @EnableXxx 注解的底层基石。
相关推荐
techdashen1 小时前
Cargo 1.94 开发周期全解析
开发语言·后端·rust
枕星而眠1 小时前
Linux守护进程完全指南:从原理到实战
linux·运维·服务器·c++·后端
北城以北88881 小时前
虚拟机安装JDK,Tomcat,部署项目
java·开发语言·tomcat
终将老去的穷苦程序员2 小时前
基于Android Studio开发的安卓图书借阅管理系统
java·sqlite·android studio·android-studio
接着奏乐接着舞2 小时前
springboot mp mybatis plaus
windows·spring boot·mybatis
金融支付架构实战指南2 小时前
Milvus 向量检索服务 + SpringBoot 实战:电商商品语义检索与相似商品推荐
spring boot·后端·milvus·向量检索
技术小结-李爽2 小时前
【工具】Maven的使用
java·maven
sou_time2 小时前
从 0 到 商用:AI Agent x SKILL x MCP 全栈实战教程:L2 高等篇:MCP 协议 + Spring AI + Agent 编排
java·人工智能·spring
冷小鱼2 小时前
高级研发编码习惯:从规范到艺术,再到AI+时代的人机协同
java·开发语言·python·编码习惯