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

  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 两个接口的意义

ImportSelector 和 ImportBeanDefinitionRegistrar 看起来用法繁琐,日常业务开发也很少直接写。但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 = true 为 Full 模式 ,代理保证 @Bean 方法无论调用多少次都返回容器中的同一个单例;false 为 Lite 模式 ,每次调用都新建对象,但省去代理开销、启动更快------有组件依赖必须 Full,没有依赖推荐 Lite;
  4. @Import 只有一个 Class 数组属性,却有四种用法:导入普通类、导入配置类、ImportSelector、ImportBeanDefinitionRegistrar;前两种导入的 bean 名称默认是类的全限定名;
  5. ImportSelector 动态决定"导入谁",ImportBeanDefinitionRegistrar 进一步掌控"怎么注册",二者是 SpringBoot 自动配置 与各类 @EnableXxx 注解的底层基石。
相关推荐
房开民7 小时前
通俗总结 Octree(八叉树)
java·服务器·数据库
豆角焖肉7 小时前
SpringBoot 集成腾讯 AI 人脸识别
人工智能·spring boot·后端·人脸识别·腾讯 ai·base64 图片
付威20237 小时前
如果你想拥有自己的 Agent,为什么值得从 rpi 开始
后端
木头科技7 小时前
【AI 工程化第十五篇】Spring AI 流式输出高并发性能优化实战:SSE、背压、限流、缓存与灰度发布
人工智能·spring·性能优化
2302_805771078 小时前
Day23:POM设计模式(封装)
java·开发语言·设计模式
SL_staff8 小时前
APS降本增效的关键:在JVS中实现‘人-机-料-法-环’全要素耦合调度
java·github·全栈
十年Java程序媛8 小时前
Java String、StringBuilder、StringBuffer 底层原理|字符串拼接性能与线程安全
java·开发语言·spring boot·安全·面试
可乐ea8 小时前
2000 个智能体把 Agent 重写成 Rust:编排流水线拆解
开发语言·后端·rust·多智能体协作·ai编程助手·智能体编排·rust重写
用户72757483196508 小时前
02-变量、常量和字符串:len("Hello, 世界") 为什么不是 9?
后端
SamDeepThinking8 小时前
CyclicBarrier的底层实现
java·后端·面试