作者:明明如月学长, CSDN 博客专家,蚂蚁集团高级 Java 工程师,《性能优化方法论》作者、《解锁大厂思维:剖析《阿里巴巴Java开发手册》》、《再学经典:《EffectiveJava》独家解析》专栏作者。
热门文章推荐:
- (1)《人工智能时代,软件工程师们将会被取代?》
- (2)《如何写出高质量的文章:从战略到战术》
- (3)《我的技术学习方法论》
- (4)《什么? 你还没用过 Cursor? 智能 AI 代码生成工具 Cursor 安装和使用介绍》
- (5)《我的性能方法论》
- (6)《New Bing 编程提效实践 - 语言识别功能》
一、背景
今天一个偶然的机会,发现某个同事在使用 Spring 的时候,有一个 Bean 在类上既加上了 @Service 注解,又在 Spring 的 XML 配置文件中也加了 的定义。
那么,如果两处都进行了配置,如果两处配置不一致会怎样?
我们今天简单分析下。
二、场景复现
2.1 直接复现
2.1.1 复现
添加 Spring 依赖:
<!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.23</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.23</version>
</dependency>
模拟 Bean 的定义:
package org.example.third.spring;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Data
@Component("student")
public class Student {
@Value("李四")
private String name;
@Value("18")
private Integer age;
}
Spring 的 XML 配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.example.third.spring"/>
<bean id="student" class="org.example.third.spring.Student">
<property name="name" value="张三"/>
<property name="age" value="22"/>
</bean>
</beans>
加载执行:
package org.example.third.spring;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class InjectValueXmlApplication {
public static void main(String[] args) throws Exception {
ApplicationContext ctx = new ClassPathXmlApplicationContext("inject-value.xml");
Student student = ctx.getBean(Student.class);
System.out.println("student : " + student);
}
}
打印的结果:
student : Student(name=张三, age=22)
通过观察可以看到,同时采用 @Component
注解和 xml 定义 Bean 时, xml 优先级更高。
2.1.2 分析
通过调试我们发现,解析 Bean 定义时,优先执行执行 component-scan 扫描注解:
然后再执行 176 行处理 xml 中 bean 定义
上述两个入口都会走到这里: DefaultListableBeanFactory#registerBeanDefinition
@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
Assert.hasText(beanName, "Bean name must not be empty");
Assert.notNull(beanDefinition, "BeanDefinition must not be null");
if (beanDefinition instanceof AbstractBeanDefinition) {
try {
((AbstractBeanDefinition) beanDefinition).validate();
}
catch (BeanDefinitionValidationException ex) {
throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
"Validation of bean definition failed", ex);
}
}
BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
if (existingDefinition != null) {
if (!isAllowBeanDefinitionOverriding()) {
throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
}
else if (existingDefinition.getRole() < beanDefinition.getRole()) {
// e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
if (logger.isInfoEnabled()) {
logger.info("Overriding user-defined bean definition for bean '" + beanName +
"' with a framework-generated bean definition: replacing [" +
existingDefinition + "] with [" + beanDefinition + "]");
}
}
else if (!beanDefinition.equals(existingDefinition)) {
if (logger.isDebugEnabled()) {
logger.debug("Overriding bean definition for bean '" + beanName +
"' with a different definition: replacing [" + existingDefinition +
"] with [" + beanDefinition + "]");
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("Overriding bean definition for bean '" + beanName +
"' with an equivalent definition: replacing [" + existingDefinition +
"] with [" + beanDefinition + "]");
}
}
this.beanDefinitionMap.put(beanName, beanDefinition);
}
else {
if (hasBeanCreationStarted()) {
// Cannot modify startup-time collection elements anymore (for stable iteration)
synchronized (this.beanDefinitionMap) {
this.beanDefinitionMap.put(beanName, beanDefinition);
List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
updatedDefinitions.addAll(this.beanDefinitionNames);
updatedDefinitions.add(beanName);
this.beanDefinitionNames = updatedDefinitions;
removeManualSingletonName(beanName);
}
}
else {
// Still in startup registration phase
this.beanDefinitionMap.put(beanName, beanDefinition);
this.beanDefinitionNames.add(beanName);
removeManualSingletonName(beanName);
}
this.frozenBeanDefinitionNames = null;
}
if (existingDefinition != null || containsSingleton(beanName)) {
resetBeanDefinition(beanName);
}
else if (isConfigurationFrozen()) {
clearByTypeCache();
}
}
通过注解解析时,通过类文件中读取 Bean 的定义:
BeanDefinition 被放在 beanDefinitionMap
, bean 名称被放在 beanDefinitionNames
中。
然后从 xml 中加载重名的 bean 时,从xml 中读取 Bean 的定义。由于注解中已经定义,这里会走到1005 行:
在 org.springframework.beans.factory.support.DefaultListableBeanFactory
默认允许 BeanDefinition 重写。
/**
* Return whether it should be allowed to override bean definitions by registering
* a different definition with the same name, automatically replacing the former.
* @since 4.1.2
*/
public boolean isAllowBeanDefinitionOverriding() {
return this.allowBeanDefinitionOverriding;
}
/** Whether to allow re-registration of a different definition with the same name. */
private boolean allowBeanDefinitionOverriding = true;
因此, XML 中的 Bean 定义覆盖了注解中的配置。
2.2 在 xml 中重复定义
2.2.1 模拟
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.example.third.spring"/>
<bean id="student" class="org.example.third.spring.Student">
<property name="name" value="张三"/>
<property name="age" value="22"/>
</bean>
<bean id="student" class="org.example.third.spring.Student">
<property name="name" value="李四"/>
<property name="age" value="18"/>
</bean>
</beans>
Exception in thread "main" org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Bean name 'student' is already used in this <beans> element
Offending resource: class path resource [inject-value.xml]
at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:72)
at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:119)
at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:111)
at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.error(BeanDefinitionParserDelegate.java:281)
at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.checkNameUniqueness(BeanDefinitionParserDelegate.java:488)
at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseBeanDefinitionElement(BeanDefinitionParserDelegate.java:434)
at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseBeanDefinitionElement(BeanDefinitionParserDelegate.java:405)
at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.processBeanDefinition(DefaultBeanDefinitionDocumentReader.java:306)
at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseDefaultElement(DefaultBeanDefinitionDocumentReader.java:197)
at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:176)
at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:149)
at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:96)
关键信息: BeanDefinitionParsingException: Configuration problem: Bean name 'student' is already used in this <beans> element
Offending resource: class path resource [inject-value.xml]
2.2.2 分析
Spring 的 Bean 加载离不开AbstractApplicationContext#refresh()
。
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
// 准备刷新此上下文。
prepareRefresh();
// 告诉子类刷新内部bean工厂。
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// 为在此上下文中使用的bean工厂做准备。
prepareBeanFactory(beanFactory);
try {
// 允许在上下文子类中后处理bean工厂。
postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// 调用在上下文中注册为bean的工厂处理器。
invokeBeanFactoryPostProcessors(beanFactory);
// 注册拦截bean创建的bean处理器。
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
// 为此上下文初始化消息源。
initMessageSource();
// 为此上下文初始化事件多路广播器。
initApplicationEventMulticaster();
// 在特定上下文子类中初始化其他特殊bean。
onRefresh();
// 检查监听器bean并注册它们。
registerListeners();
// 实例化所有剩余的(非延迟初始化)单例。
finishBeanFactoryInitialization(beanFactory);
// 最后一步:发布相应的事件。
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// 销毁已创建的单例以避免悬空资源。
destroyBeans();
// 重置“活动”标志。
cancelRefresh(ex);
// 将异常传播给调用者。
throw ex;
}
finally {
// 重置Spring核心中的常见内省缓存,因为我们可能不再需要单例bean的元数据...
resetCommonCaches();
contextRefresh.end();
}
}
}
这个问题涉及到关键方法: AbstractRefreshableApplicationContext#refreshBeanFactory
@Override
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
customizeBeanFactory(beanFactory);
// 加载 bean 定义
loadBeanDefinitions(beanFactory);
this.beanFactory = beanFactory;
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}
再往底层:AbstractBeanDefinitionReader#loadBeanDefinitions(java.lang.String)
@Override
public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException {
return loadBeanDefinitions(location, null);
}
再往底层: BeanDefinitionParserDelegate#checkNameUniqueness
/**
* Validate that the specified bean name and aliases have not been used already
* within the current level of beans element nesting.
*/
protected void checkNameUniqueness(String beanName, List<String> aliases, Element beanElement) {
String foundName = null;
if (StringUtils.hasText(beanName) && this.usedNames.contains(beanName)) {
foundName = beanName;
}
if (foundName == null) {
foundName = CollectionUtils.findFirstMatch(this.usedNames, aliases);
}
if (foundName != null) {
error("Bean name '" + foundName + "' is already used in this <beans> element", beanElement);
}
this.usedNames.add(beanName);
this.usedNames.addAll(aliases);
}
第一次会将 beanName 放在 usedNames 中。
读取第一个 bean 的名称时, usedNames 集合里面已经有了,就会报这个错误
通过阅读源码和调试,我们可以发现在 xml 中重复定义会有 bean 名称的重复检查。
三、启示
3.1 注解和 XML 哪种更好?
Spring 使用注解和使用 xml 的方式定义 bean 都有各自的优缺点,没有绝对的好坏,具体要根据实际情况和需求来选择。
适合使用注解的情况:
简化配置:使用注解可以减少XML配置文件的冗长,使代码更加简洁易读。
代码可读性:使用注解可以更加清晰地表达代码的意图,使代码更加易于理解。
依赖注入:注解可以很方便地进行依赖注入,省去了手动配置的麻烦。
自动装配:使用注解可以轻松实现自动装配,提高开发效率。
适合使用XML配置的情况:
统一管理:XML配置文件可以集中管理所有的配置信息,包括数据库连接、事务管理等。
灵活性:XML配置文件可以根据需要进行修改,而不需要修改代码。
依赖关系:XML配置文件可以清晰地表达Bean之间的依赖关系,使代码更加易于维护。
兼容性:XML配置文件具有很好的兼容性,可以在不同的环境中使用。
一般来说,注解方式更简洁、方便、灵活,但也可能造成代码和配置的耦合,而 xml 方式更清晰、规范、可扩展,但也可能造成配置文件的冗长和复杂。
3.2 如何选择
一般来说,如果需要使用一些第三方的库或者类,或者需要配置一些通用的或者复杂的 bean,可以使用 xml 配置,这样可以更好地管理和扩展。
如果需要使用自己开发的类或者简单的 bean,可以使用注解配置,这样可以更简洁和方便。
如果需要更好的类型安全和开发效率,也可以考虑使用注解;如果需要更好的灵活性和可读性,也可以考虑使用 xml。
最终还是要根据具体的项目需求和团队开发习惯来选择合适的方式。
3.3 注意事项
注解和 xml 的方式定义 bean 也可以同时使用,但要注意避免命名冲突的问题。如果出现两个相同名称的实例,Spring 会覆盖其中一个,xml 优先级高于注解;xml 中同时配置两个相同 id 的 bean,直接校验不通过报错。
四、总结
大家在日常开发中,尤其是工作一两年的同学,写代码一定不要止步于模仿,而要真正搞清楚为什么要这么做,避免一些“粗心” 带来不必要的问题。
大家不要止步于写出能跑的代码,而是要写出“对”的代码。
创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。