注解bean
我们常用的向spring容器中添加bean的方式主要有三种
- @Component注解
- @Configuration 加 @bean
- @Import
那么spring是如何解析这些注解的,本文具体研究这个问题
parse
spring解析注解bean的代码写在ConfigurationClassParser
类的parse
方法,参数就是SpringApplication.run
时传入的启动主类
spring启动时会解析我们的主配置类(就是带@SpringBootApplication的启动类),解析的任务交给解析器ConfigurationClassParser,对应的方法就是parse
ConfigurationClassParser
中一个属性configurationClasses
,用来存放解析出来的结果
private final Map<ConfigurationClass, ConfigurationClass> configurationClasses = new LinkedHashMap<>();
对应的get方法
public Set<ConfigurationClass> getConfigurationClasses() {
return this.configurationClasses.keySet();
}
processConfigurationClass
parse
方法最终会走向processConfigurationClass
方法
这个方法贴主要代码
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
}
while (sourceClass != null);
调用doProcessConfigurationClass
方法,如果有返回值,递归调用doProcessConfigurationClass,看spring的注释说// Recursively process the configuration class and its superclass hierarchy.
,也就是递归解析配置类和他的父类,所以这代码的意思就是解析配置类,如果有父类再解析父类,如果父类有父类再一直解析下去。
所以重点就来到了doProcessConfigurationClass
方法
doProcessConfigurationClass
doProcessConfigurationClass
(spring源码中的doXXX一般都很重要),接下来就分析这个代码,贴完整代码
protected final SourceClass doProcessConfigurationClass(
ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
throws IOException {
if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
// 1.内部类 这一步看看有没有内部类,一般不咋用
processMemberClasses(configClass, sourceClass, filter);
}
// 2.@PropertySource 这一步解析@PropertySource注解,更改配置文件位置时会使用,一般使用默认位置,不咋更改
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}
// 3.@ComponentScan 这一步就很重要了,解析@ComponentScan注解
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// 开始扫描,把@ComponentScan指定包下的@Component类全部扫描出来
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// 把所有扫描到的beanClass递归解析,所以我们也可以加多个@Configuration类
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
//判断是不是ConfigurationClass 带@Configuration注解和@Component注解都算
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
// 解析
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
// 4.@Import 解析@Import注解
processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
// 5.@ImportResource解析@ImportResource解析
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
String[] resources = importResource.getStringArray("locations");
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
}
}
// 6.@Bean 解析带有@Bean注解的方法,加入到configClass的beanMethods属性中
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}
// 6.接口@Bean 解析实现的接口中带有@Bean注解的默认方法,加入到configClass的beanMethods属性中
processInterfaces(configClass, sourceClass);
// 7.父类 如果有父类返回父类,以继续解析
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
// Superclass found, return its annotation metadata and recurse
return sourceClass.getSuperClass();
}
}
// 没有父类,解析结束
return null;
}
整个流程总结如下
- 解析内部类
- 解析@PropertySource注解
- 解析@ComponentScan注解,扫描指定包下的所有@Component类,并递归解析
- 解析@Import注解
- 解析@ImportResource注解
- 解析@Bean
- 解析实现接口中的@Bean
- 返回父类
接下来一个个看
解析内部类
if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
// 1.内部类 这一步看看有没有内部类,一般不咋用
processMemberClasses(configClass, sourceClass, filter);
}
也就是说如果一个类有@Component
注解,会解析他的内部类如果也有@Component
会注册成bean,写个代码测试一下
@Configuration
@ComponentScan("com.pqsir.parser")
public class ClassParserApplication {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
Nested nested = context.getBean(Nested.class);
System.out.println(nested); // com.pqsir.parser.ClassParserApplication$Nested@4e7912d8
}
@Component
class Nested {
}
}
所以内部类也可以注册到bean容器
解析@PropertySource注解
这个真没用过,我觉得配置文件放在规定的地就不错,以后也好找,这个就不研究了
解析@ComponentScan
这个都懂,就是扫描的包路径,值的注意的是扫描到的class都会递归调用parser
这一步的代码细分析下
- 扫描包下的类
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
这个componentScanParser
内部有个scaner(扫描器),扫描@Component
注解的类(包括子注解@Configuration
,@Service
等)
- 循环判断扫描到的类是否是ConfigurationClass,如果是则递归解析
// 如果是ConfigurationClass
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
// 解析
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
这一步ConfigurationClass
不单单是指带@Configuration
注解的类,带@Component
注解的也算ConfigurationClass,spring内部有个集合,只要是这个集合里的注解,都算ConfigurationClass
// ConfigurationClassUtils
private static final Set<String> candidateIndicators = new HashSet<>(8);
static {
candidateIndicators.add(Component.class.getName());
candidateIndicators.add(ComponentScan.class.getName());
candidateIndicators.add(Import.class.getName());
candidateIndicators.add(ImportResource.class.getName());
}
public static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
// 省略
// 是否有候选注解
for (String indicator : candidateIndicators) {
if (metadata.isAnnotated(indicator)) {
return true;
}
}
// 省略
}
总结【1】带@Configuration或@Component类都是ConfigurationClass
最后做个小测试--主类制定了@ComponentScan
包下又一个带@ComponentScan的类指向另一个包,那么这两个包下的bean都会被注入,测试一下,我们建两个包parser
和parser2
parser2下一个普通bean:BeanOut
package com.pqsir.parser2;
@Component
public class BeanOut {
@Override
public String toString() {
return "BeanOut";
}
}
parser下ClassParserApplication主类扫描parser
和 OtherConfiguration:在parser包下定义扫描parser2
package com.pqsir.parser
@Configuration
@ComponentScan("com.pqsir.parser") //扫描parser
public class ClassParserApplication {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
BeanOut bean = context.getBean(BeanOut.class);
System.out.println(bean); // BeanOut
}
}
@Configuration
@ComponentScan("com.pqsir.parser2")
public class OtherConfiguration {
}
最终正常输出"BeanOut"
解析@Import注解
这个也比较好理解,就是如果某个bean带@Import
注解,就把@Import注解指定的类也注册成bean
测试一下,我们把上一步BeanOut的@Component
注解去掉,删除OtherConfiguration
@Configuration
@ComponentScan("com.pqsir.parser")
@Import({BeanOut.class})
public class ClassParserApplication {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
BeanOut bean = context.getBean(BeanOut.class);
System.out.println(bean); // BeanOut
}
}
结果也可以正常输出
这个@Import的最大好处可以把一些第三方的类给注入到bean容器,因为第三方的类一般也改不了总不能去加@Component
注解吧,而且如果你是一个第三方开发者,肯定不希望一直的工具依赖spring(万一哪天没人用了你的工具也废了),所以通过@Import把你的工具引入spring是一个完美的解决方案。
还有个比较大的好处是可以做封装,@Import注解可以被继承,比如我们写个自定义注解继承了@Import,并指定import的类,就可以把这个类注册到bean容器中,甚至可以让这个类继承一些后置处理器来给bean容器做调整,比如AOP的@EnableAspectJAutoProxy就是用到这一点,还有mybaits的也是用@MapperScan也是使用Import的方式完成一些mapper bean的生成工作
上例是@Configuration+@Import,用@Component+@Import也ok,因为【1】
解析@ImportResource注解
主要为了兼容之前的xml写法
解析@Bean
@Bean注解一般经常使用,使用工厂方法创建一个bean,一般就是@Configuration+@Bean,由于上述原因【1】,所以@Component+@Bean也可以。
解析实现接口中的@Bean
这是对@Bean注解的一个扩展,解析实现的接口中带有@Bean注解的默认方法,写个例子测试一下
- 接口(包含默认方法带@Bean注解,本身不带任何注解)
public interface IBeanA {
@Bean
default BeanOut beanOut() {
return new BeanOut();
}
}
- 实现(带@Component注解)
@Component
//@Import({BeanOut.class})
public class BeanA implements IBeanA {
}
- 测试类
@Configuration
@ComponentScan("com.pqsir.parser")
public class ClassParserApplication {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
BeanOut bean = context.getBean(BeanOut.class);
System.out.println(bean); // BeanOut
}
}
也会正常输出,这个真没想到什么使用场景,遇到再说吧
返回父类
最后一步如果返回父类继续递归解析,测试一下
- 父类(没有@Component注解)
public class BeanFather {
@Override
public String toString() {
return "BeanFather";
}
}
- 子类(有@Component注解)
@Component
public class BeanSon extends BeanFather{
}
- 测试类(尝试获取父类bean)
@Configuration
@ComponentScan("com.pqsir.parser")
public class ClassParserApplication {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
BeanFather bean = context.getBean(BeanFather.class);
System.out.println(bean); // BeanFather
}
}
正常可获取
最后
完成这一系列的解析扫描再解析过程,就可以通过getConfigurationClasses
拿到所有扫描并解析到的类。
spring拿到这些类之后再通过一个reader
把ConfigurationClasse
转换为bean定义,注册到beanFactory,所以parser
+reader
,就完成了这些bean的扫描&解析&注册工作,代码在ConfigurationClassPostProcessor
中。
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
// 创建一个解析器
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
// 开始解析,这个candidates就是我们传入的MainApplication
parser.parse(candidates);
// 获取解析到的类
Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
// 初始化一个reader
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
// 把上面解析到的类转化为bean定义并注册到bean定义注册器(registry)
this.reader.loadBeanDefinitions(configClasses);
}
扩展
@Configuration和@Component的区别
上文很多@Configuration
的注解都可以用@Component
代替,甚至主类使用@Component
来替换@Configuration
也能正常跑,那问题来了,他俩就没有区别吗,那要@Configuration
有啥用。
却别主要两方面
一.首先,@Configuration和@Service,@Controller注解很像,都继承@Component注解,没有实际的什么作用只是告诉别人这个类是个配置类型的bean
二.其次,也是实际功能上的区别,使用@Configuration类+@Bean,sping会生成一个cglib动态代理,这个代理的工能就是第一次调用@Bean的方法会直接执行并返回结果,同时存储结果,下一次调用同样方法直接返回结果,这样可以保证单例,不管调用多少次@Bean的方法最终得到的结果是同一个对象,而使用@Component则不会
做个测试
通过@Bean注册三个bean A B C,其中BC依赖注入A,先使用@Configuration
@Configuration
public class BeanConfiguration {
@Bean
public A a() {
return new A();
}
@Bean
public B b() {
B b = new B();
b.a = a();
return b;
}
@Bean
public C c() {
C c = new C();
c.a = a();
return c;
}
static class A {
}
static class B {
public A a;
}
static class C {
public A a;
}
}
试一下
ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
BeanConfiguration.B b = context.getBean(BeanConfiguration.B.class);
BeanConfiguration.C c = context.getBean(BeanConfiguration.C.class);
System.out.println(b.a.equals(c.a));
输出结果是true
如果把@Configuration
改成@Component
,输出结果就变成false了,这个代理的代码ConfigurationClassPostProcessor.enhanceConfigurationClasses
中,有兴趣的自己研究吧
其实这种写法本来就不太好,还是觉得用依赖注入更好,如下
@Configuration
public class BeanConfiguration {
@Bean
public A a() {
return new A();
}
@Bean
public B b(A a) {
B b = new B();
b.a = a;
return b;
}
@Bean
public C c(A a) {
C c = new C();
c.a = a;
return c;
}
static class A {
}
static class B {
public A a;
}
static class C {
public A a;
}
}
这种写法就算改成@Component
也没问题
@Import+ImportBeanDefinitionRegistrar
上面说了@Import可以导入bean,一个或多个bean,但如果比如把一个包下的所有类都注入到bean,它就不能实现了,除非你一个一个写,但是这样新增一个就得写一个。
这种需求其实很常见,比如mybaits的@MapperScan
,他需要你指定一个mapper的位置,然后把mapper全部注入到bean容器,不管你加多少mapper都会注入进去。
spring批量注册bean是有个后置处理器支持的,就是BeanDefinitionRegistryPostProcessor
,如果你某个bean实现了BeanDefinitionRegistryPostProcessor,就会拿到bean定义注册器BeanDefinitionRegistry
,然后爱怎么注册bean定义、注册多少随你。
所以我最开始遇到这种需求解决思路是@Import+BeanDefinitionRegistryPostProcessor,虽然可行,但获取不到注解的属性,比如@MapperScan
的value
所以spring要引入ImportBeanDefinitionRegistrar
这个接口,其中重要方法
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}
通过实现这个方法也能拿到BeanDefinitionRegistry
(ConfigurationClassPostProcessor本身继承BeanDefinitionRegistryPostProcessor,所以可以拿到),然后可以按照自己的意思注册bean定义,更重要的:通过第一个参数importingClassMetadata可以获取使用@Import注解的类的元数据,就可以获取到外层注解的属性值
那么问题来了,什么时候执行呐。
刚才"最后"章节的代码有一句this.reader.loadBeanDefinitions(configClasses);
,就是在这个时候继承这个接口的类(被@Import引入)执行registerBeanDefinitions
方法
可以自己去找代码,整个过程再ConfigurationClassPostProcessor
执行的生命周期执行完毕