ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Bean交给容器的三种方式:XML、注解与Java配置类

Spring Bean交给容器的三种方式:XML、注解与Java配置类 1. 为什么同一个交给容器的动作会有三种不同的姿势先聊一个很多Spring初学者都会困惑的问题既然Spring容器能帮我们管理对象那把一个Bean交给Spring容器管理到底是什么意思直接new一个对象出来和把它交给容器区别在哪里区别其实就一句话对象创建之后谁说了算。直接new对象的生命周期由你自己掌控用完就扔彼此之间毫无关联交给容器对象的一生——创建、属性填充、初始化、依赖注入、销毁——全部由Spring接管你只需要声明我需要这样一个Bean剩下的容器来搞定。但是声明这个动作在Spring的发展史上出现过三种完全不同的写法。这正好也是面试里特别经典的一个问题把一个Bean对象交给Spring容器管理有哪几种方式先说结论分别是XML配置方式、注解方式组件扫描、Java配置类方式ConfigurationBean。这三种方式都能实现同一个目标——让Spring容器创建并管理一个对象但它们的底层机制、适用场景、代码组织方式差别非常大。本文会把三种方式逐一拆开来讲同时会补充很多实际开发中才会遇到的细节比如Bean命名规则、扫描路径、配置类代理机制、同名Bean覆盖的坑以及它们最终是如何殊途同归走到BeanDefinition这一步的。先看一张总览表后续每一步都会对应展开方式核心写法适用场景需要额外配置吗XMLbean id class/老项目、遗留系统、对第三方类做精细配置需要ApplicationContext加载XML注解ComponentComponentScan自己项目内部的业务类团队协作开发需要配置扫描路径Java配置类ConfigurationBean第三方库集成、多条件创建、批量装配基本不需要天然友好三种方式不是谁取代谁的关系而是各自有各自的生存空间。下面逐个拆解。2. XML配置老资格的开发方式至今仍不可彻底抛弃2.1 从bean标签说起XML方式是最古老的方式。在Spring 2.x到3.x那个年代没有注解所有对象都写在applicationContext.xml里。基本的写法长这样beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd bean iduserService classcom.example.service.UserService/ /beans然后在Java代码里加载XML配置文件就能拿到容器ApplicationContext context new ClassPathXmlApplicationContext(applicationContext.xml); UserService userService context.getBean(userService, UserService.class);这里的核心逻辑是Spring通过反射根据class属性去实例化UserService放进容器并以id作为Bean在容器中的唯一标识。注意id不是必须的如果省略Spring会使用com.example.service.UserService#0这样的自动生成名。2.2 对象之间的依赖如何表达XML方式不只是创建单个Bean更重要的是表达Bean之间的依赖关系。在实际开发里一个类几乎总会依赖别的类。比如UserService依赖UserMapperbean iduserMapper classcom.example.mapper.UserMapper/ bean iduserService classcom.example.service.UserService property nameuserMapper refuserMapper/ /bean用property标签做setter注入用ref引用另一个Bean。还有构造器注入bean iduserService classcom.example.service.UserService constructor-arg refuserMapper/ /bean2.3 三种实例化方式这个知识点在面试里经常被叫Bean实例化方式。XML里配置一个Bean可以细分为三种实例化方法构造器实例化最常见Spring调用类的无参构造器或带参构造器创建对象上面的例子就是。静态工厂实例化类里有一个static方法负责返回对象比如public class UserFactory { public static UserService createUserService() { return new UserService(); } }bean iduserService classcom.example.factory.UserFactory factory-methodcreateUserService/实例工厂实例化先创建一个工厂Bean再通过工厂的普通方法创建目标Beanpublic class UserFactory { public UserService createUserService() { return new UserService(); } }bean iduserFactory classcom.example.factory.UserFactory/ bean iduserService factory-beanuserFactory factory-methodcreateUserService/第二种和第三种在真实项目中用得极少但它们是理解Spring底层为什么能创建对象的关键Spring其实并不在乎对象是怎么来的它只在乎容器里最终有没有一个可用的Bean。这个思想一直延续到了Bean方法——你可以在方法内部用任何诡异的逻辑制造对象Spring只认方法返回值。2.4 XML方式实际使用中的感受说实话纯XML配置写多了真的挺痛苦。一个项目几十个Bean每个都要写标签还要维护ref引用关系项目一大人很容易眼花改一个Bean名字可能导致一堆引用断裂。但XML有个不可替代的优势不需要改代码就可以调整装配关系。在还不流行DevOps的年代运维可以直接改XML配置来切换环境、调整Bean实现不用重新编译打包。所以现在XML方式通常只出现在老项目维护、需要极其细粒度控制Bean定义参数比如lazy-init、init-method的场合。真正从零开发新项目几乎没人会用XML来装配业务对象了。3. 注解方式最贴近业务的效率之选3.1Component家族注解方式是目前使用率最高的一种。它的核心思想是不再由开发者显式声明我要这个Bean而是Spring自己扫描某个包路径下的所有类凡是标记了特定注解的类统统自动注册为Bean。最基础的注解是ComponentComponent public class UserService { // 业务代码 }Spring看到Component会默认以类名首字母小写作为Bean名称也就是userService。想自定义名称就写Component(myUserService)。Component下面还有几个专门的分支用于表达语义注解语义典型位置Repository数据仓储层DAO、MapperService业务服务层Service实现类Controller控制层Spring MVC的ControllerRestController控制层REST风格接口层注意这几种注解的本质就是Component。Service、Repository的源码上都标着Component它们是别名式的存在功能上没有区别主要价值是语义化——看一眼就知道类扮演什么角色。3.2 依赖注入怎么处理光把类注册成Bean还不够对象之间还是要互相引用的这时候用AutowiredService public class UserService { Autowired private UserMapper userMapper; // 业务方法 }Autowired的注入逻辑是先按类型查找再按名称查找。也就是说Spring先去容器里找一个UserMapper类型的Bean找到就直接注入如果同类型有多个再根据字段名去匹配。这里有个容易踩坑的点如果UserMapper是个接口你有两个实现类UserMapperA和UserMapperB字段名userMapper谁都不匹配启动就会报NoUniqueBeanDefinitionException。解决思路有两个一个是配合Qualifier(userMapperA)明确指定另一个是改字段名。实际开发里我更推荐加Qualifier语义清楚不容易埋雷。3.3 扫描路径最容易忽略的配置注解方式不是放到项目任意位置都行的。Spring必须知道去哪里扫描。Spring Boot项目默认扫描主启动类所在的包及子包这是因为SpringBootApplication里封装了ComponentScan默认扫描自身所在包。如果你手写Spring MVC项目就得自己配Configuration ComponentScan(basePackages com.example) public class AppConfig { }或者用XMLcontext:component-scan base-packagecom.example/扫描路径这件事看着简单但它是实际项目里Bean被莫名其妙找不到的头号原因。我见过好几个项目把Component加了、类也写了启动就报NoSuchBeanDefinitionException最后定位就是某个Bean放在了启动类所在包的兄弟包下面根本没被扫到。所以约定所有要交给Spring管理的类必须放在主扫描路径覆盖的范围内。3.4 注解方式的一个常见需求网上搜索热词里能看到bean实例化方式、spring boot mybatis 的 java 开源多商户跨境商城源码下载这类需求背后对应的其实是当你要把一个第三方库的对象交给Spring管理或者一个对象的创建逻辑比较复杂比如需要从配置中心读取参数来构造Component就无能为力了——因为你没法在别人的类上打注解。这时候就要用到第三种方式Java配置类。4. Java配置类方式用代码写装配兼顾灵活与类型安全4.1Configuration和Bean的基本用法Java配置类方式本质上是XML方式的一次代码化升级。它用Configuration标注一个类再用Bean标注类里面的方法方法的返回值就会交给Spring容器管理Configuration public class AppConfig { Bean public UserService userService() { return new UserService(userMapper()); } }这种方式的灵活之处在于Bean方法的执行逻辑完全由你掌控。你可以先读配置文件、根据环境变量判断、甚至循环批量注册BeanConfiguration public class DataSourceConfig { Bean public DataSource dataSource() { DruidDataSource dataSource new DruidDataSource(); dataSource.setUrl(jdbc:mysql://localhost:3306/mall); dataSource.setUsername(root); dataSource.setPassword(123456); return dataSource; } }像DruidDataSource这种第三方类你根本没法在它源码上加ComponentBean就成了最优雅的方案。4.2 配置类的全档模式和轻量模式这一步是很多人都没搞懂、但面试里高级问题常考的细节。Configuration类里的Bean方法和我们普通类里标注Bean的方式在底层机制上是有区别的标注了Configuration的配置类Spring会通过CGLIB生成一个代理子类保证Bean方法在整个容器生命周期内只被调用一次从而实现单例。如果某个类没标Configuration只写了Bean方法比如在Component类中Spring会走轻量模式Bean方法不会做代理每次调用方法都会新建对象。网上热词里提到proxy factory其实就跟这个机制相关。Spring对Configuration类做增强依赖的就是类似ProxyFactory的代理技术底层。你可以做个实验验证在一个Configuration类里Bean方法内部调用另一个Bean方法得到的对象是同一个换成Component里写Bean方法得到的对象就不同了。第一种情况就是代理生效的效果。那么哪种好常规项目配置类务必加Configuration这样Bean之间的依赖关系、单例语义才能正确处理。轻量模式一般是Spring Boot自动配置内部在用日常开发不需要主动追求。4.3Bean方法的参数注入Bean方法不仅能自己new对象还能通过方法参数接收容器中已有的BeanConfiguration public class ServiceConfig { Bean public UserService userService(UserMapper userMapper) { return new UserService(userMapper); } }此时Spring看到Bean方法有参数会自动去容器里查找对应的Bean注入进来。这个做法避免了一个很丑的写法——在方法体内context.getBean()。我见过不少老项目在Bean方法里直接注入ApplicationContext再手动getBean虽然也能跑但完全没必要。用参数注入语义清晰、可测试性也好。5. 三种方式背后的统一真相一切都会汇合到BeanDefinition5.1 殊途同归的注册流程三种方式看着风格迥异但如果你去抠Spring源码会发现它们在容器内部的走向是完全一致的。Spring容器管理Bean的前提是先拿到这个Bean的元信息——这个Bean叫什么、是什么类型、是单例还是原型、依赖哪些属性。这个元信息在Spring里就是BeanDefinition。三种方式只是生产BeanDefinition的不同入口XML方式XmlBeanDefinitionReader读取XML文件逐个解析bean标签生成BeanDefinition。注解方式ClassPathBeanDefinitionScanner扫描包路径下的Component类通过AnnotationBeanDefinitionReader生成BeanDefinition。Java配置类方式ConfigurationClassPostProcessor解析Configuration类里的Bean方法生成BeanDefinition。生成之后全部都要注册到DefaultListableBeanFactory的beanDefinitionMap里然后走同一套实例化流程——构造器推理、属性填充、初始化回调、AOP代理增强。理解了这一点你就能看懂为什么网上一堆手写Spring教程的核心都在讲BeanDefinition、BeanFactoryPostProcessor、BeanPostProcessor这几个概念。它们的本质就是围绕Bean元信息和Bean生命周期拦截器做的设计。5.2 三种方式的优先级规则三种方式可以混用那出现同名Bean时听谁的Spring处理优先级是显式注册优先于隐式扫描。也就是说在Spring Boot里如果XML显式定义了userService而Component扫描也扫到了同名的userServiceXML的配置会覆盖注解扫描的结果。如果都是注解方式同名的两个Bean先注册者胜出但这其实更多是悄悄覆盖而不是报错很容易造成排查困难。实际项目中我几乎不混用XML和注解来管理同一个业务Bean这会带来不可控的覆盖行为。最稳妥的策略是新代码统一走注解/Java配置类旧代码保留XML中间用ImportResource桥接尽量不重叠管理同一类对象。5.3 容器通过名字或类型获取Bean容器始终对外提供两套查找Bean的方式getBean(beanName)和getBean(ClassT)。如果你从容器里拿到的对象在类型转换时报错多半是没分清这两者。比如容器里既有UserService接口的实现A又有实现B你用getBean(UserService.class)就会抛NoUniqueBeanDefinitionException必须同时指定名字UserService userService context.getBean(userServiceA, UserService.class);6. 深水区经验把Bean交给容器之后还有哪些绕不开的坑6.1 循环依赖与三级缓存spring三级缓存原理这个搜索词几乎是每个Spring面试者必背的。它跟把Bean交给容器有什么关系关系很大——当一个Bean的创建过程依赖另一个Bean、而另一个又依赖回去时Spring没法简单地先创建A再创建B于是设计了三级缓存一级缓存存放成品Bean二级缓存存放早期暴露的原始对象三级缓存存放对象的工厂。靠Lazy、构造器注入和三级缓存Spring大部分情况下能解决循环依赖。但我要提醒一句能解决不代表推荐制造循环依赖。三级缓存解决的是setter注入和字段注入的循环依赖构造器注入的循环依赖依然会直接报错。你在新项目里应该通过接口拆分、Lazy等方式尽量避免循环依赖而不是把三级缓存当成百试百灵的解药。6.2PostConstruct和生命周期回调Bean交给容器后生命周期被Spring接管这带来一个很常见的操作在Bean初始化完成后执行一些代码。最常用的写法是Component public class DataInitializer { PostConstruct public void init() { System.out.println(容器启动后执行初始化逻辑); } }PostConstruct的执行时机是对象创建完成、依赖注入完毕、BeanPostProcessor的前置方法执行之后。注意它和InitializingBean接口、XML里的init-method、Bean里的initMethod属性最终都会汇聚到同一个执行链中。优先级是PostConstruct先于afterPropertiesSet()afterPropertiesSet()先于自定义initMethod。知道这个顺序能帮你预判日志输出排查初始化异常的时候很有用。6.3 什么时候选哪种方式讲了这么多最后给一条经验法则自己写的业务类默认用Service、Repository、Component任何框架类自动装配都用Autowired。第三方库的对象、创建过程需要根据配置动态判断的对象用Bean方法。系统已经依赖XML的老项目新模块也尽量随大流继续用XML减少维护心智负担但要尽量把新代码往Java配置类迁移。你在写一个供他人复用的starter或SDK优先用Bean方法 条件注解ConditionalOnClass、ConditionalOnMissingBean这样对使用方最友好。我个人在这些年里的实际体会是注解方式是日常主力Java配置类是兜底神器XML是历史包袱但也是理解Spring设计思想的钥匙。三种方式都熟练了你对Spring容器如何管理Bean对象才算真正通了而不是停留在只会加个Component就往容器里扔对象的阶段。最后再分享一个小技巧写代码时可以借助IDE的Bean结构图功能IDEA里View → Tool Windows → Spring直观看出哪些类被容器接管、哪些Bean之间有依赖关系。排查为什么这个Bean是null这类问题时这个功能比在代码里翻来找去快得多。
返回列表