Spring Bean交给容器管理的三种方式:XML、注解扫描与Java Config全解析
发布时间:2026/10/1 4:11:28 作者:尧图编辑部 阅读量:1,286

一说到把Bean交给Spring容器管理很多同学第一反应是不就是加个Service注解吗对业务代码里绝大多数情况确实就这么一下但真正等你接老项目、看框架源码或者面试被深挖的时候就会发现有好多平时没细想的问题——XML里那一堆bean标签是从哪来的配置类里一个返回对象的方法为什么也能被Spring接管Component和Bean到底有什么区别这篇文章就是把“把Bean交给Spring容器管理”这件事彻底捋清楚讲透三种主流方式XML配置、注解扫描、Java Config以及它们背后的原理和选型思路。内容适合刚接触Spring想系统理清思路的同学也适合准备面试时查漏补缺的老手看完你能直接回答“你的Bean是怎么进容器的”这个问题。1. 先搞清楚“交给Spring容器管理”到底是什么意思1.1 容器接管的是什么创建、装配、销毁全流程在写代码之前得先把概念对齐。所谓“交给Spring容器管理”本质是把你代码里一个普通的class实例从“自己new、自己维护生命周期”变成“容器负责创建、容器负责注入依赖、容器负责在合适的时机销毁”。举个生活化的例子以前你想吃顿饭得自己去买菜、洗菜、开火、炒菜、洗碗交给容器管之后你只管在菜单上点菜后厨团队会按固定流程把菜做好端到你面前。Spring就是这个后厨团队Bean就是菜单上的一道菜而“点菜”的方式就是我们今天要讲的三种方式。被容器管理的Bean能享受什么待遇依赖注入DI、AOP代理、生命周期回调初始化、销毁、条件装配、延迟加载、作用域控制单例还是原型这些都是容器托管带来的能力。换句话说你告诉容器“有这个Bean”容器就会在背后帮你做很多事情。1.2 不是所有对象都适合交给Spring管理这也是一个容易走极端的点。刚学Spring的时候觉得所有类都加个注解很爽但实际项目里真的不是所有对象都需要进容器。适合交给Spring管理的对象有状态的服务类、DAO/Repository、数据源、消息客户端、缓存客户端、各种外部API的封装类——这些类通常有复杂的依赖关系需要复用需要被AOP处理。不适合交给Spring管理的对象纯DTO/VO值对象、无状态的工具类方法静态方法为主、只在某个方法内部临时使用的一次性对象。这类对象交进容器反而增加无谓的开销还会污染ApplicationContext里的Bean清单。判断标准很简单这个对象有没有“被多个地方依赖”“需要统一配置”“需要生命周期管理”的需求如果没有就别硬塞进容器。2. 三种主流方式全景对比三种方式本质上都是“把Bean的定义告诉容器”但声明的形式和时机不同。我先给一个总览后面逐节展开。方式声明位置适用场景典型代码侵入性XML配置外部XML文件老项目维护、无注解环境、配置频繁变更的第三方集成bean iduserService class...低代码无感知注解扫描类本身日常业务开发的主力Component、Service、Repository中类上需加注解Java Config独立配置类第三方Bean装配、条件装配、复杂初始化逻辑ConfigurationBean低业务类无需感知2.1 XML配置方式最原始但依然存在的方案XML方式是Spring早期版本的标配。直到今天一些老项目、部分中间件集成代码里还能看到它的影子。使用步骤很固定第一步先定义一个普通的Java类比如UserService和它的依赖UserDao。public class UserDao { public void query() { System.out.println(query user from db); } } public class UserService { private UserDao userDao; // 构造器注入 public UserService(UserDao userDao) { this.userDao userDao; } public void execute() { userDao.query(); } }第二步在applicationContext.xml里声明两个Bean?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd bean iduserDao classcom.example.xml.UserDao/ bean iduserService classcom.example.xml.UserService constructor-arg refuserDao/ /bean /beans第三步启动容器取BeanClassPathXmlApplicationContext context new ClassPathXmlApplicationContext(applicationContext.xml); UserService userService context.getBean(userService, UserService.class); userService.execute(); context.close();XML里的bean可以配置很多属性scope单例/原型、lazy-init是否延迟加载、init-method和destroy-method生命周期回调、factory-method工厂方法创建Bean等。注入方式支持构造器注入constructor-arg和setter注入property。为什么现在新代码不推荐XML类型安全性差字符串写错了要运行时才能发现配置冗长每加一个依赖就要改XMLIDE重构时类改名XML里的class路径不会自动跟着变。但XML有一个独特优势不需要改代码就能调整Bean的装配关系所以一些框架的扩展点、规则引擎、对运维友好的配置项至今仍倾向于用XML或类似的外部化配置。2.2 注解扫描方式日常业务开发的主力注解方式是现在业务代码里最常用的。核心就是把注册信息写到类上容器启动时自动扫描发现。Component public class UserDao { public void query() { System.out.println(query user from db); } } Service public class UserService { private final UserDao userDao; public UserService(UserDao userDao) { this.userDao userDao; } public void execute() { userDao.query(); } }然后在配置类或启动类上开启组件扫描。Spring Boot项目里启动类上的SpringBootApplication已经包含了ComponentScan默认扫描启动类所在的包和子包。SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }如果你是非Boot的Spring项目需要手动加扫描Configuration ComponentScan(basePackages com.example.annotated) public class AppConfig { }为什么Service能被扫描到因为Service被Component元注解修饰了ClassPathBeanDefinitionScanner在扫描时会根据默认的includeFilters把带有Component及其元注解的类注册为BeanDefinition。所以Repository、Controller、Service在“被容器发现”这件事上和Component是等效的区别只是语义分层Repository负责持久层且会被PersistenceExceptionTranslationPostProcessor识别并转换异常Controller会额外被Spring MVC的处理器扫描识别Service纯粹是业务层标记。用注解方式的优点非常明显类改名重构时无需额外维护映射声明和类定义在一起代码即文档类型安全。缺点也明显Bean装配逻辑分散在各个类里查整体依赖关系需要工具辅助同一类型多个实例时还需要配合Qualifier、Primary区分。2.3 Java Config方式显式声明的高级装配Java Config是Spring 3.0之后官方大力推荐的配置方式本质是把XML里的bean标签换成Configuration类里的Bean方法。Configuration public class AppConfig { Bean public UserDao userDao() { return new UserDao(); } Bean public UserService userService(UserDao userDao) { return new UserService(userDao); } }容器调用userService(UserDao userDao)方法时会自动把userDao这个Bean传进来。默认情况下Bean方法的名称就是Bean的名称也可以通过Bean(customName)指定。这里有一个非常关键的底层机制Configuration类会被Spring用CGLIB代理Bean方法之间的调用会先查容器里有没有现成的单例Bean有就直接返回而不是重新执行方法体。这就是为什么上面userService方法虽然手动new了一个UserService但它内部的userDao参数是容器注入的同一个单例。只要方法声明在Configuration类里返回对象就归容器管容器会负责后续的所有生命周期。Java Config特别适合以下几类场景装配第三方库的Bean比如RedisTemplate、RestTemplate、DataSource、KafkaTemplate因为这些类没法改源码去加Component注解。条件装配结合ConditionalOnProperty、ConditionalOnMissingBean等注解按环境或配置决定创建哪个Bean。复杂初始化逻辑比如创建对象后还要做一堆设置、读取配置文件、构建连接池。需要精细控制Bean名称、作用域、初始化顺序的场合。有个细节值得注意如果Bean方法写在Component类里而不是Configuration类里Spring会以“lite模式”处理不会给类加CGLIB代理Bean方法之间直接调用会生成不同实例。这个坑稍后细讲。3. 原理细节三种方式如何殊途同归3.1 一切都归于BeanDefinition三种方式外观差别很大但Spring内部最终都会把它们转换成同一种东西BeanDefinition。你可以把BeanDefinition理解成“生产Bean的图纸”里面记录了Bean的类名、作用域、是否懒加载、构造器参数、属性值、初始化方法、销毁方法等所有必要信息。XML方式XmlBeanDefinitionReader读取bean标签解析成BeanDefinition。注解方式ClassPathBeanDefinitionScanner扫描类路径发现加了Component的类构建BeanDefinition。Java Config方式ConfigurationClassPostProcessor处理Configuration类把Bean方法解析成BeanDefinition。这些BeanDefinition最终都会注册到BeanDefinitionRegistry也就是容器内部那张“登记表”。搞懂这张“登记表”对排查问题特别有帮助当你说“为什么这个Bean没注进来”时第一反应应该是去查登记表里到底有没有这个BeanDefinition而不是盯着Autowired字段发呆。3.2 从BeanDefinition到单例Bean的完整创建流程有了BeanDefinition之后Spring会按一套固定流程创建Bean合并BeanDefinition子定义覆盖父定义得到最终蓝图。推断构造方法如果只有一个构造器且参数能解析直接用有多个构造器时按规则选择最合适的。实例化通过反射调用构造器这里只得到一个“半成品”对象属性还没填充。属性填充populateBean阶段处理Autowired、Resource、Value等注解把依赖对象注入进去。初始化阶段依次执行Aware接口回调、BeanPostProcessor前置处理、PostConstruct、InitializingBean、init-method、BeanPostProcessor后置处理。完成单例Bean存入一级缓存singletonObjects等待被使用。这个流程里最让开发者头疼的是第4步遇到循环依赖的情况。比如A依赖BB又依赖A如果两个都是用构造器注入Spring直接抛BeanCurrentlyInCreationException如果是setter或字段注入Spring靠三级缓存解决singletonFactories提前暴露A的早期引用半成品代理让B能先引用到A等B创建完再回来完善A。这个机制对日常开发的影响是想用构造器注入时如果遇到循环依赖要么改设计要么给其中一个加Lazy。4. 三种方式的对比与选型建议4.1 全面对比该看哪些维度现在把三种方式放到同一张表里方便你按项目情况做决策。对比维度XML注解扫描Java Config配置位置独立XML文件类定义上独立配置类可读性依赖关系集中但代码跳转繁琐分散IDE插件辅助集中且类型安全逻辑清晰类型安全弱字符串指定类名强编译期可查强编译期可查重构友好度差类名改动需同步XML好类和注解一起动好配置方法返回值可改不可变Bean支持构造器注入支持构造器注入支持构造器注入支持第三方库Bean支持不支持无法改源码最合适条件装配麻烦较弱支持丰富新项目推荐度不推荐新增业务Bean主力基础设施/集成Bean主力4.2 实际项目里我推荐的搭配组合结合我这些年踩坑的经验最省心的方案是业务Bean用注解方式跨层集成、第三方库、条件装配用Java ConfigXML只用于老项目维护新代码尽量不新增XML配置。具体来讲Controller、Service、Repository这些有明确分层语义的类直接用对应的Controller、Service、Repository语义清晰团队协作时一目了然。数据源、Redis客户端、MQ客户端、RestTemplate、WebClient这类基础设施Bean全部放进Configuration类用Bean声明。因为这类Bean往往需要读取配置、设置连接池参数、做重试策略在配置类里能写完整的初始化逻辑。如果一个Bean需要根据配置项决定创建不创建用Java Config加ConditionalOnProperty。同一接口有多个实现需要精确指定注入时配合Primary或Qualifier而不是去XML里反复改ref。另外还有一个实战中的建议不要让包扫描范围过大。Spring Boot默认扫启动类所在包及子包很多人图省事把启动类放在顶层包结果把整个项目所有类都扫一遍。如果项目里有些类不需要进容器放在扫描范围外或者用excludeFilters排除掉能明显缩短启动时间。5. 常见问题与排查技巧实录5.1 注入为null为什么我的Autowired失效了这是问得最多的问题。Autowired注入为null绝大多数原因不是注解本身的问题而是这个类压根没有被Spring管理。常见场景在Util类里写了Autowired字段但Util类本身没有加Component也没有在配置类里注册或者用new关键字手动创建了对象导致Spring根本不知道这个对象的存在。还有一个容易被忽略的场景包扫描范围没覆盖到。比如启动类在com.example.app而需要扫描的类在com.example.common如果不改扫描路径common包下的类不会被发现。排查方法很直接启动时查看日志里有没有这个Bean的注册信息用ApplicationContext.getBeanDefinitionNames()打印所有Bean名称或者直接在代码里调applicationContext.getBean(Xxx.class)验证。IDEA装了Spring插件的话被Spring管理的类旁边会有小叶子图标没有叶子就说明没进容器。5.2 同一个类里方法调用导致代理失效有一个非常经典的问题Transactional方法在同一个类里被另一个方法调用时为什么事务不生效Service public class OrderService { public void createOrder() { // 这里的事务注解不生效 this.updateStock(); } Transactional public void updateStock() { // 数据库操作 } }原因是Spring的AOP代理机制。OrderService实际被注入的是代理对象但this.updateStock()是直接调用目标类的方法没有通过代理所以Transactional注解没有被代理拦截。解决办法有几种把updateStock拆到另一个Service类里注入自身代理用AopContext.currentProxy()获取当前代理对象。这个问题的根源就是“Bean已经被代理但你绕过了代理”。5.3 循环依赖导致启动失败怎么办启动时报BeanCurrentlyInCreationException时先看是不是构造器注入的循环依赖。Spring的三级缓存解决不了构造器循环依赖因为它没法在构造完成前提前暴露引用。这时候方案优先级优先重构代码拆出循环依赖如果实在改不了把其中一个Autowired字段改成setter或字段注入还可以给其中一个依赖加Lazy让Spring先注入一个懒加载代理真正调用时才去解析。这里想多说一句循环依赖往往是设计信号。好的设计里依赖关系应该尽量是单向的A依赖B、B依赖A这种结构通常意味着职责边界没划清重构优先级比绕开它更高。5.4 容器里有多个同类型Bean导致注入报错Autowired默认按类型注入当接口有多个实现类时Spring会抛出NoUniqueBeanDefinitionException。这不算Bug反而是在提醒你明确选择。处理方式按优先级排序业务上确实有默认实现给其中一个加Primary不同场景要选不同实现用Qualifier(beanName)指定名称或者直接用JSR 250的Resource(name beanName)按名称注入。顺便说一个比较新的写法如果这些实现类本身就想作为一个整体注入可以改用List接口来注入Spring会把容器里所有该类型的Bean按顺序组装成一个列表这个特性在设计策略模式时很好用。5.5 配置类加上proxyBeanMethods false导致Bean方法重复执行Spring Boot 2.2之后自动配置类普遍使用Configuration(proxyBeanMethods false)来提升启动速度这个写法叫“lite模式”。在这个模式下Configuration类不会被CGLIB代理Bean方法之间互相调用时每次都会重建一个对象不再是从容器里拿单例。所以你自己写的配置类如果内部存在Bean方法调用另一个Bean方法的情况不要随便加proxyBeanMethods false否则可能出现意外创建多个实例的问题。什么时候可以加配置类里所有Bean方法都不互相依赖且每个方法都通过new独立创建对象时加这个参数能省掉代理创建的开销。5.6 调试Bean装配的几个实用手段排查Bean装配问题时我常用的工具和手段有这么几个启动时打印ctx.getBeanDefinitionNames()快速确认Bean是否注册。用Spring Boot Actuator的beans端点能查看BeanDefinition的详细信息以及依赖关系。IDE里使用Spring插件自带的Bean层级图可视化查看依赖链。在Bean方法上打条件断点能看到容器是在什么时机、被谁触发创建的。这些手段都能帮你快速定位是“没注册”还是“注册了但注入路径不对”的问题。6. 踩过几次坑之后的个人体会说实话搞懂“三种方式”只是第一步真正值钱的是明白“为什么要有这么多方式”。XML是地基建得早注解是业务代码的舒适区Java Config是面对复杂集成时的利器。它们不是互相替代的关系而是Spring在不同历史阶段、不同应用场景下给出的答案。每次在项目里看到混用三种方式的代码我都会先去想作者当初为什么选这种方式是历史遗留还是确实有适配的场景这个思考习惯帮我避免了很多“看着不舒服但不敢动”的尴尬局面。还有一个特别想分享的小技巧能写构造器注入就尽量别写字段注入。字段注入写起来最快但隐藏了依赖关系类一多就变成一团乱麻。构造器注入让每个Bean的依赖在创建那一刻就固定下来配合不可变类不管是写单测还是排查问题都省心很多。这比纠结用哪种方式注册Bean更值得落实到日常编码习惯里。