本文基于SpringBoot 2.6.3分析
前言
在看SpringBoot源码时发现,SpringBoot通过WebMvcAutoConfiguration.EnableWebMvcConfiguration
重写了spring-webmvc.jar
中的WebMvcConfigurationSupport#requestMappingHandlerMapping
方法,但奇怪的是重写的方法中并没有做特殊处理,只是调用的父类的方法。父类中的方法同样是有@Bean
注解的,区别在于子类多了一个@Primary
注解。
实际上这是SpringBoot为了解决Spring上下文中出现多个RequestMappingHandlerMapping
Bean时,MvcUriComponentsBuilder
会抛出异常的问题. 推荐看另一篇文章:Spring中相同类型Bean存在多个时抛出异常分析及解决方案
然而在我阅读的这个版本中MvcUriComponentsBuilder
已经支持多个RequestMappingHandlerMapping
Bean的情况。SpringBoot再做这样的处理是属于冗余代码。因此我也向SpringBoot提出了相关issue:SpringBoot中冗余的RequestMappingHandlerMapping配置 https://github.com/spring-projects/spring-boot/issues/29682
WebMvcAutoConfiguration.EnableWebMvcConfiguration#requestMappingHandlerMapping
源码:
WebMvcConfigurationSupport#requestMappingHandlerMapping
源码:
回到本文主题,看了WebMvcAutoConfiguration.EnableWebMvcConfiguration
中的源码后,突然好奇:
Spring是如何处理这种配置类之间继承,父类中存在@Bean注解方法情况的 ?**
源码分析
- Spring在类扫描时会将扫描到的class封装到
BeanDefinitionHolder
中,然后会遍历Set集合中的BeanDefinitionHolder
,拿到Bean Class,将其封装成ConfigurationClass
,调用ConfigurationClassParser#processConfigurationClass
进行解析。
- 解析
configClass
时,主要处理类上如下几个注解@Component
、@PropertySources
、@ComponentScans
、@ImportResource
和 带有@Bean
注解的Method。 - 类上的注解处理完后,会获取Bean Class中所有带有
@Bean
注解的Method,调用ConfigurationClass#addBeanMethod(BeanMethod method)
保存到成员变量ConfigurationClass#beanMethods
集合中(Set<BeanMethod> beanMethods = new LinkedHashSet<>()
)。 - 最后会判断
configClass
是否有父类,如果有,会将父类class封装成SourceClass
返回,#processConfigurationClass
中判断#doProcessConfigurationClass
方法的返回值不为空会继续解析父类,父类中的@Bean
方法同样被保存在configClass#beanMethods
集合中。 - 如果没有父类,则当前
configClass
解析完成,保存ConfigurationClassParser#configurationClasses
Map集合中。 - 继续解析下一个扫描到的Bean Class
ConfigurationClassParser#doProcessConfigurationClass
源码:
7. 所有的Bean Class被解析后,会调用ConfigurationClassBeanDefinitionReader#loadBeanDefinitions(Set<ConfigurationClass> configClasses)
方法,从ConfigurationClass
加载Bean Class中定义的Bean,封装成BeanDefinition
ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod)
源码:
如果这个BeanMethod是被已经注册的BeanMethod重写的,则不再注册到Spring,直接返回。
这个就是Spring对配置类之间继承, @Bean方法在子类被重写并标记@Bean注解的处理,防止重复注册抛出异常。
但如果这个BeanMethod没有被子类重写的,但存在重名的Bean,后续的注册则会抛出异常。
示例代码及运行结果:
public class BeanObject {
}
public class BeanConfigParent {
@Bean
public BeanObject bean1() {
System.out.println("BeanConfigParent bean1 创建");
return new BeanObject();
}
}
1. 父类存在标有@Bean
的方法,子类配置成Bean后,Spring是否会调用父类的@Bean
方法创建Bean ?
@Configuration
public class BeanConfigChild1 extends BeanConfigParent {
}
输出: BeanConfigParent bean1 创建
答案:子类配置成Bean后,Spring会调用父类的@Bean
方法创建Bean
2. 子类重写了父类标有@Bean
的方法,子类中也使用@Bean
标记方法,那父类中的是否也会被创建 ?
@Configuration
public class BeanConfigChild1 extends BeanConfigParent {
@Bean
@Override
public BeanObject bean1() {
System.out.println("BeanConfigChild1 bean1 被创建");
return new BeanObject();
}
}
输出: BeanConfigChild1 bean1 被创建
答案:子类中重写的方法被执行,父类中的@Bean方法不再被执行
3. 子类重写了父类标有@Bean
的方法,但子类中没有使用@Bean
标记方法,那父类中的是否也会被创建 ?
@Configuration
public class BeanConfigChild1 extends BeanConfigParent {
@Override
public BeanObject bean1() {
System.out.println("BeanConfigChild1 bean1 被创建");
return new BeanObject();
}
}
输出: BeanConfigChild1 bean1 被创建
答案:此时查到的是父类中的@Bean方法,但调用的是子类的实例对象,因此子类中重写的方法被执行,父类中的@Bean方法不再被执行,
4. 如果父类标有@Bean
的方法,多个子类继承父类,是否会多次注册bean ?
@Configuration
public class BeanConfigChild1 extends BeanConfigParent {
}
@Configuration
public class BeanConfigChild2 extends BeanConfigParent {
}
答案: 不会注册多次,只会注册一次,因为是同一个父类,Spring会对已经处理过的父类缓存,如果已经处理过,则不再处理。
5. 如果父类标有@Bean
的方法,一个子类重写该方法,并标记@Bean
注解,另一个子类没有重写,会出现什么情况 ?
@Configuration
public class BeanConfigChild1 extends BeanConfigParent {
@Bean
@Override
public BeanObject bean1() {
System.out.println("BeanConfigChild1 bean1 被创建");
return new BeanObject();
}
}
@Configuration
public class BeanConfigChild2 extends BeanConfigParent {
}
- 因为BeanConfigChild1被先加载,处理到BeanConfigChild2时发现父类被缓存则不再处理,这种情况只会注册BeanConfigChild1中的bean1,只有一个BeanObject对象.
@Configuration
public class BeanConfigChild1 extends BeanConfigParent {
}
@Configuration
public class BeanConfigChild2 extends BeanConfigParent {
@Bean
@Override
public BeanObject bean1() {
System.out.println("aaaa");
return new BeanObject();
}
}
- 因为BeanConfigChild1被先加载,所以会注册BeanConfigParent中的bean1,而BeanConfigChild2中重写的bean1同样被注册,此时会报错. 因为SpringBoot2.1开始,默认bean-definition不允许被覆盖。
所以这种方式是不可控的,我们不应该通过类名来控制执行顺序
SpringBoot中WebMvcAutoConfiguration.EnableWebMvcConfiguration
继承了DelegatingWebMvcConfiguration
,DelegatingWebMvcConfiguration
又继承了WebMvcConfigurationSupport
WebMvcAutoConfiguration.EnableWebMvcConfiguration
和DelegatingWebMvcConfiguration
两个类上都有@Configuration
是否会出现上面的异常情况呢?
原因在于: 工程中默认扫描的包根本不会扫描到org或者org.springframework包下,DelegatingWebMvcConfiguration
实际上不会被注册成bean.
SpringBoot不允许设置or或org.springframework为扫描包,
源码见: ConfigurationWarningsApplicationContextInitializer
最后建议:尽量不要让一个配置类去继承另一个配置类,代码看上去会变得晦涩难懂。