spring如何扫描解析bean(注册bean的多种方式)

注解bean

我们常用的向spring容器中添加bean的方式主要有三种

  • @Component注解
  • @Configuration 加 @bean
  • @Import

那么spring是如何解析这些注解的,本文具体研究这个问题

parse

spring解析注解bean的代码写在ConfigurationClassParser类的parse方法,参数就是SpringApplication.run时传入的启动主类

spring启动时会解析我们的主配置类(就是带@SpringBootApplication的启动类),解析的任务交给解析器ConfigurationClassParser,对应的方法就是parse

ConfigurationClassParser中一个属性configurationClasses,用来存放解析出来的结果

java 复制代码
private final Map<ConfigurationClass, ConfigurationClass> configurationClasses = new LinkedHashMap<>();

对应的get方法

java 复制代码
public Set<ConfigurationClass> getConfigurationClasses() {
	return this.configurationClasses.keySet();
}

processConfigurationClass

parse方法最终会走向processConfigurationClass方法 这个方法贴主要代码

java 复制代码
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一般都很重要),接下来就分析这个代码,贴完整代码

java 复制代码
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
  • 返回父类

接下来一个个看

解析内部类

java 复制代码
if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
	// 1.内部类  这一步看看有没有内部类,一般不咋用
	processMemberClasses(configClass, sourceClass, filter);
}

也就是说如果一个类有@Component注解,会解析他的内部类如果也有@Component会注册成bean,写个代码测试一下

java 复制代码
@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 这一步的代码细分析下

  • 扫描包下的类
java 复制代码
Set<BeanDefinitionHolder> scannedBeanDefinitions =
                    this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());

这个componentScanParser内部有个scaner(扫描器),扫描@Component注解的类(包括子注解@Configuration@Service等)

  • 循环判断扫描到的类是否是ConfigurationClass,如果是则递归解析
java 复制代码
// 如果是ConfigurationClass
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
    // 解析
    parse(bdCand.getBeanClassName(), holder.getBeanName());
}

这一步ConfigurationClass不单单是指带@Configuration注解的类,带@Component注解的也算ConfigurationClass,spring内部有个集合,只要是这个集合里的注解,都算ConfigurationClass

java 复制代码
// 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都会被注入,测试一下,我们建两个包parserparser2 parser2下一个普通bean:BeanOut

java 复制代码
package com.pqsir.parser2;
@Component
public class BeanOut {
	@Override
	public String toString() {
		return "BeanOut";
	}
}

parser下ClassParserApplication主类扫描parser和 OtherConfiguration:在parser包下定义扫描parser2

java 复制代码
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
	}
}
java 复制代码
@Configuration
@ComponentScan("com.pqsir.parser2")
public class OtherConfiguration {
}

最终正常输出"BeanOut"

解析@Import注解

这个也比较好理解,就是如果某个bean带@Import注解,就把@Import注解指定的类也注册成bean 测试一下,我们把上一步BeanOut的@Component注解去掉,删除OtherConfiguration

java 复制代码
@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注解,本身不带任何注解)
java 复制代码
public interface IBeanA {
	@Bean
	default BeanOut beanOut() {
		return new BeanOut();
	}
}
  • 实现(带@Component注解)
java 复制代码
@Component
//@Import({BeanOut.class})
public class BeanA implements IBeanA {

}
  • 测试类
java 复制代码
@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注解)
java 复制代码
public class BeanFather {
	@Override
	public String toString() {
		return "BeanFather";
	}
}
  • 子类(有@Component注解)
java 复制代码
@Component
public class BeanSon extends BeanFather{
}
  • 测试类(尝试获取父类bean)
java 复制代码
@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拿到这些类之后再通过一个readerConfigurationClasse转换为bean定义,注册到beanFactory,所以parser+reader,就完成了这些bean的扫描&解析&注册工作,代码在ConfigurationClassPostProcessor中。

java 复制代码
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

java 复制代码
@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;
	}
}

试一下

java 复制代码
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中,有兴趣的自己研究吧 其实这种写法本来就不太好,还是觉得用依赖注入更好,如下

java 复制代码
@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这个接口,其中重要方法

java 复制代码
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}

通过实现这个方法也能拿到BeanDefinitionRegistry(ConfigurationClassPostProcessor本身继承BeanDefinitionRegistryPostProcessor,所以可以拿到),然后可以按照自己的意思注册bean定义,更重要的:通过第一个参数importingClassMetadata可以获取使用@Import注解的类的元数据,就可以获取到外层注解的属性值 那么问题来了,什么时候执行呐。 刚才"最后"章节的代码有一句this.reader.loadBeanDefinitions(configClasses);,就是在这个时候继承这个接口的类(被@Import引入)执行registerBeanDefinitions方法

可以自己去找代码,整个过程再ConfigurationClassPostProcessor执行的生命周期执行完毕

相关推荐
pq2171 小时前
Spring FactoryBean源码解析
java·spring boot·spring
IT空门:门主4 小时前
spring ai alibaba -流式+invoke的人工介入的实现
java·后端·spring
javachen__5 小时前
Spring MVC 动态支持 JSON/XML 的技巧
spring·springmvc
敲敲千反田6 小时前
Spring 相关
java·后端·spring
树下水月7 小时前
Easyswoole 框架session在高并发/频繁请求下数据丢失问题记录
java·后端·spring
贫民窟的勇敢爷们9 小时前
Spring Security OAuth2.0 技术详解:分布式系统安全认证的标准方案
java·安全·spring
_Evan_Yao10 小时前
return 的迷途:try-catch-finally 中 return 的诡异顺序与 Spring 事务暗坑
java·后端·spring·mybatis
一只大袋鼠10 小时前
Spring 事务管理三种实现方式
java·数据库·spring·声明式事务
nj012820 小时前
Spring 循环依赖详解:三级缓存、早期引用、AOP 代理与懒加载
java·spring·缓存