Spring系列03:Bean 的三种注册方式

你每天都在用,却未必说清「为什么是三种」

写 Spring 的人,几乎天天在「把一个对象交给容器」。@Service 标一下、配置类里 @Bean 写一个、或者 @Configuration 丢一堆方法------但它们本质不是一回事。

很多开发者能把三种写法都写出来,一被问「@Bean 放在 @Component 和 @Configuration 里到底差在哪」「为什么有的 @Bean 互相调用拿的是同一个实例、有的却 new 了好几个」,就卡壳了。这一篇把三种注册方式的身世、边界,以及那个最容易被忽略的 full/lite 玄机一次讲透。

三种方式,按「容器怎么知道这个 Bean 该被注册」来区分:

  • 方式一:组件扫描( Stereotype + @ComponentScan)------类上贴注解,容器扫包自动认。
  • 方式二:@Bean 方法 ------你在方法里 new,告诉容器「按这个方法造」。
  • 方式三:@Configuration 配置类------方式二的家,但它是「带了超能力的家」。

下面逐一拆,重点在第三种。

方式一:组件扫描,最省事的「贴标签」

@Component 是 Spring 对所有托管组件的通用原型(stereotype) 。而 @Repository、@Service、@Controller 只是它在持久层、服务层、表现层的特化(specializations) ------它们身上都元注解了 @Component ,所以容器看待它们的姿势完全一致,区别只在语义和额外能力(比如 @Repository 会自动做持久层异常翻译)。(出处:官方 · @Component and Further Stereotype Annotations)

光贴 @Service 还不够,容器得知道「去哪扫」。这就是 @ComponentScan 的活:

java 复制代码
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig { }

一个 @ComponentScan,容器就会去 com.example 下把贴了 @Component/@Service/@Repository/@Controller/@Configuration 的类自动探测并注册成 BeanDefinition。(出处:官方 · Automatically Detecting Classes and Registering Bean Definitions)

几个实战要点:

  • 默认 bean 名 :非限定类名首字母小写,比如 UserService → userService,也可用 @Component("xxx") 显式指定。
  • 默认作用域 :singleton,可用 @Scope 改。
  • 过滤 :@ComponentScan(includeFilters=..., excludeFilters=...) 能按注解 / 类型 / 正则 / AspectJ 自定义哪些扫、哪些不扫;useDefaultFilters=false 还能关掉默认那套注解探测。

一句话:组件扫描适合「自己写、自己管的业务类」------贴个注解就完事,零配置心智负担。

方式二:@Bean,把「怎么造」写进方法

@Bean 是方法级 注解,角色等价于 XML 里的 <bean/> 元素:你在一个方法里 new 出对象并配置好,方法返回类型就是 Bean 的类型,默认 bean 名就是方法名。(出处:官方 · Using the @Bean Annotation)

java 复制代码
@Configuration
public class AppConfig {
    @Bean
    public TransferService transferService(AccountRepository accountRepository) {
        return new TransferServiceImpl(accountRepository);
    }
}

这里有两个要知道的点:

  • 依赖通过方法参数表达 :transferService(AccountRepository ...) 的依赖解析,和构造器注入是同一套机制------容器先找好 AccountRepository,再调这方法。这就是为什么「@Bean 方法互相调用」其实不是表达依赖的正道,正道是写参数。
  • 生命周期回调 :@Bean(initMethod = "init", destroyMethod = "cleanup") 等价于 XML 的 init-method/destroy-method;而且容器会自动给有 public close/shutdown 方法的 Bean 挂上销毁回调 (太贴心也容易踩雷------比如 JNDI 拿的 DataSource 就不是你该关的,得 @Bean(destroyMethod = "") 显式关掉默认推断)。

注意:官方明确说 @Bean 既可以用在 @Configuration 里,也可以用在任何 @Component 里。这正是后面 full/lite 玄机的入口。

方式三:@Configuration,方式二真正的「家」

@Configuration 的首要目的就是当 bean 定义源 。它自己也被 @Component 元注解了------所以配置类本身也是一个 Bean,而且是组件扫描的默认候选之一 (和 @Component/@Service 平起平坐)。(出处:官方 · Using Filters to Customize Scanning)

也就是说,你在 @Configuration 类里写的每个 @Bean 方法,都是在向容器「申报」一个 Bean 定义。@Configuration 和 @Component 在这里的差别,不在「能不能写 @Bean」,而在下面的 full/lite 模式。

重点:full 模式为何要 CGLIB 代理(lite 的反面)

这是 @Bean 放在 @Configuration vs @Component 里唯一、也是最要命的区别。(出处:官方 · Basic Concepts: @Bean and @Configuration)

full 模式(默认) :@Configuration 类在启动时,Spring 会用 CGLIB 生成一个子类代理 ,覆写里面的 @Bean 方法。当你在配置类里写 service() 调 repository(),调用会被代理拦截 ------代理先去容器里查有没有名为 repository 的 Bean,有就直接返回那个单例,没有才真去执行方法体并注册 。于是无论你调多少次 repository(),拿到的永远是同一个容器托管实例。

官方给的理由很直白:这样能防止同一个 @Bean 方法被普通 Java 调用意外执行多次、造出多个实例这类难以排查的隐患。

lite 模式 :当 @Bean 方法写在非 @Configuration 的类 (比如 @Component)里,或显式标了 @Configuration(proxyBeanMethods = false) 时,就进入 lite 模式------没有 CGLIB 代理 ,方法调用就是普通 Java 调用 :service() 调 repository(),每次都是 new 一个新对象,而不是复用容器里的单例。

后果很明确:lite 模式下别用「方法互相调用」来表达 Bean 间依赖 ,因为那根本不会被容器托管。正确姿势是把依赖写成方法参数(前面方式二提过),让容器通过参数注入。lite 模式下的 @Bean 更像一把通用工厂方法,不享受跨方法引用重定向。

深度钩子:Spring Boot 的 @SpringBootApplication(经 @SpringBootConfiguration)默认就是 @Configuration(proxyBeanMethods = false),即 lite 模式 。所以 Boot 应用里你写的 @Bean 互相调用大概率不会复用单例------这是无数「我明明配了单例却有多个实例」惨案的源头。这条是 Spring Boot 的行为,不是 Framework 参考文档里的通用约定。

收益权衡:lite 模式省掉了运行期 CGLIB 子类化,启动开销和内存占用更小,这也是 Spring Boot 默认选它的原因。代价就是你得自己管住「别在配置类里用方法调用串依赖」。

看图就懂:full vs lite 的调用链路

左边 full 模式:调用链 service() → repository() 全程经过代理,最终都指向容器里那个唯一的 Repository 单例(图中只实例化一次)。

右边 lite 模式:service() 直接 new 出一个 Repository、repository() 又被 new 一次,两个互不相干的实例------依赖没被容器托管,埋下隐患。

到底怎么选三种方式

  • 业务类、自己写的 :@Component/@Service/@Repository + 组件扫描。零样板,最顺手。
  • 造第三方 / 复杂对象(连接池、客户端、需要 init/destroy 的) :@Bean。你能 100% 控制 new 和配置过程。
  • @Configuration 里的 @Bean :默认 full 模式,适合需要「@Bean 方法互相引用、保证单例复用」的场景;如果你确定不会在配置类里串方法调用依赖,显式 proxyBeanMethods = false 切 lite 模式省开销(Spring Boot 默认就这么做)。

一句话收束:组件扫描管「贴标签的业务类」,@Bean 管「怎么造难造的对象」,@Configuration 是 @Bean 的家,而 full/lite 模式决定了这个家里的方法调用到底走不走容器。 多数踩坑,都栽在后半句。


📌 版本与出处说明 :本文基于 Spring Framework 7.x 官方 Reference(查阅时文档版本 7.0.10,2026-10)撰写,核心概念均对照官方文档提炼,正文关键处标注了对应官方小节。官方文档若改版,以官网最新版为准。

相关推荐
余槐i19 分钟前
MySQL 8.0 联合索引走错:为什么加了索引查询反而从 200ms 变成 8s
java·spring boot·mysql·docker·性能优化
SL_staff20 分钟前
制造业订单交付总在最后一刻爆雷?用结构化计划模板打通跨系统协同断点
java·开源·全栈
做科研的周师兄39 分钟前
【R语言实战】MixSIAR 氢氧同位素水源解析全流程:从混合空间校验到贝叶斯 MCMC 收敛诊断(后附完整代码)
java·大数据·开发语言
有点小困9771 小时前
Spring Boot 异步任务 ClassLoader 踩坑实录:ClassNotFoundException 的诡异真相
java
aiks_yuan1 小时前
Java 正则表达式很慢怎么办:Pattern 预编译只快 2~5 倍,真正卡死线程的是回溯(JDK 8、11、17 实测)
java
用户3721574261351 小时前
C# 填写 PDF 交互式表单:文本框、复选框、单选按钮和下拉列表
java
IT枫斗者枫哥1 小时前
Java 退款请求超时,先查原单还是重试?
java·后端
特立独行的猫A1 小时前
我用仓颉写了个多平台发文工具:一份 Markdown,一条命令发六个平台
java·后端·python
ikgy1 小时前
JAVA EE:1.计算机是如何工作的
java·java-ee