前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【小家Spring】Spring向容器注册Bean的高级应用:@Import、DeferredImportSelector、ImportBeanDefinitionRegistrar的使用

【小家Spring】Spring向容器注册Bean的高级应用:@Import、DeferredImportSelector、ImportBeanDefinitionRegistrar的使用

作者头像
YourBatman
发布2019-09-03 16:32:12
3.2K0
发布2019-09-03 16:32:12
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦

前言

关于@Import在之前的文章里,也都零散的提到过多次,也支出了它的重要性,甚至它的一个解析过程。

但是由于@Import模式向容器导入Bean确实非常非常的重要,特别是在注解驱动的Spring项目中、@Enablexxx的设计模式中有大量的使用,在当下最流行的Spring Boot中,可以说作为设置是最重要的一种方式,来做底层抽象、组件式的设计。

比如我们熟悉的:@EnableAsync@EnableAspectJAutoProxy@EnableMBeanExport@EnableTransactionManagement…等等统一采用的都是借助@Import注解来实现的

本篇文章旨在着眼于对@Import的使用上,以及结合ImportSelector、DeferredImportSelector、ImportBeanDefinitionRegistrar这三个接口的一些高级应用~

需要注意的是:ImportSelector、DeferredImportSelector、ImportBeanDefinitionRegistrar这三个接口都必须依赖于@Import一起使用,而@Import可以单独使用

基本环境如下
代码语言:javascript
复制
@ComponentScan(value = "com.fsx", excludeFilters = {
        @Filter(type = FilterType.ANNOTATION, classes = {Controller.class}),
        //排除掉web容器的配置文件,否则会重复扫描
        @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {AppConfig.class}),
})
@Configuration
public class RootConfig {

}


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class})
public class TestSpringBean {

    @Autowired
    private ApplicationContext applicationContext;

    @Test
    public void test1() {
        Arrays.stream(applicationContext.getBeanDefinitionNames())
                .filter(x -> !x.contains(".internal")) //过滤调用Spring内部给我们默认注册Bean,方便我们查看结果
                .forEach(System.out::println);
	// 当前输出两个Bean:rootConfig、helloServiceImpl
    }

}

说一句,采用SpringJUnit4ClassRunner为我们自动创建的容器,为GenericApplicationContext这个类型的容器。Bean工厂为DefaultListableBeanFactory

关于GenericApplicationContext的使用,相当来说是都需要手动的,比如根据配置类加载Bean、刷新容器等等。。。这里面Spring的Spring-test包都帮我们把这些事做了~

目前这种容器(只能测试Service、Dao),不能测试Controller(web环境)下的Bean或者接口,因为junit这不是web环境,是不会启动web容器的。(毕竟缺少相关tomcat环境 jar包等等),若想测试web环境,请增加这么处理

代码语言:javascript
复制
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class, AppConfig.class})
@WebAppConfiguration //创建web容器,这样就能初始化AppConfig配置类,也能记载进来web先关的Bean了(比如Spring MVC的九大组件等等)
public class TestSpringBean { ... }

至于Controller层的接口怎么通过url方式去请求测试 ,可以结合MockMvc来mock测试。具体的使用方式,这里就不多介绍了~(Junit不会有父子容器的概念。。。

@Import注解

这里讲述单独使用@Import的例子,使用它有一个非常方便的地方在于:它可以导入Jar包里面的类(因为我们的@ComponentScan是不会扫描jar包的),可以看看下面这个例子:

代码语言:javascript
复制
//@ComponentScan 部分省略,下同
@Configuration
@Import({Parent.class,
        // 这是Spring-code包里面的Bean,我随便找的一个
        AntPathMatcher.class})
public class RootConfig {

}

打印输出如下: 我们成功的向容器内注入了这些Bean,并且BeanName为全类名

代码语言:javascript
复制
rootConfig
helloServiceImpl
com.fsx.bean.Parent
org.springframework.util.AntPathMatcher

那么,若我不把它放在@Configuration上,而是放在一个普通的@Component组件上呢?比如我们放在HelloServiceImpl上:

代码语言:javascript
复制
@Service
@Import({Parent.class, AntPathMatcher.class})
public class HelloServiceImpl implements HelloService { ... }

我们发现,效果是相同的。(其实如果你阅读过之前的@Configuration的解析过程,就能知道不管是Lite模式还是Full模式,这里导入Bean方面都是一样的)

参见ConfigurationClassParser#parse/processConfigurationClass方法

虽然放哪个组件都行,但在实际开发中,我们一般约定都放在@Configuration配置文件里

ImportSelector和DeferredImportSelector

使用@Import的时候,它的类可以是实现了ImportSelector或者DeferredImportSelector接口的类。

Spring容器会实例化这个实现类并且执行其selectImports方法(执行时机,下面会源码分析)

我们先来看一个ImportSelector的Demo Show:

代码语言:javascript
复制
public class MyImportSelector implements ImportSelector
        //虽然不能@Autowired,但是实现了这些接口是可以感知到的,下面看源码会发现,Spring会给他注入进去
        // 这样我们就可以根据特定的条件,来决定某些Bean能注入,有些Bean不能注入了
        //,BeanClassLoaderAware,BeanFactoryAware,EnvironmentAware,ResourceLoaderAware
{


    // 备注:这里各种@Autowired的注入都是不生效的,都是null
    // 了解Spring容器刷新过程的时候就知道,这个时候还没有开始解析@Autowired,所以肯定是不生效的
    @Autowired
    private HelloService helloService;

    /**
     * 容器在会在特定的时机,帮我们调用这个方法,向容器里注入Bean信息
     *
     * @param importingClassMetadata 包含配置类上面所有的注解信息,以及该配置类本身
     *                               若有需要,可以根据这些其它注解信息,来判断哪些Bean应该注册进去,哪些不需要
     * @return 返回String数组,注意:都必须是类的全类名,才会被注册进去的(若你返回的全类名不存在该类,容器会抛错)
     */
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        System.out.println("this MyImportSelector...");
        //return new String[]{"com.fsx.bean.Child"};
        // 一般建议这么玩 用字符串写死的方式只是某些特殊场合(比如这个类不一定存在之类的。。。)
        return new String[]{Child.class.getName()};
        
    }
}

输出如下:

代码语言:javascript
复制
rootConfig
helloServiceImpl
com.fsx.bean.Parent
org.springframework.util.AntPathMatcher
com.fsx.bean.Child

这里我提供提一个Spring的默认实现AdviceModeImportSelector(它通过解析注解信息,选择合适的Bean加入),大家可以供以参考。 这个类在后续分析事物的原理以及AOP的原理的时候,会再次见面~

再来一个DeferredImportSelector的Demo Show:

代码语言:javascript
复制
public class MyDeferredImportSelector implements DeferredImportSelector {

    // 同样的,它也只需要实现这个方法即可 但是它还提供了一些更高级的功能
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        System.out.println("this MyDeferredImportSelector...");
        // 这里面若容器里已经有名为`com.fsx.bean.Child`的Bean,就不会再注册进去了的
        return new String[]{"com.fsx.bean.Child"};
    }
}

我们发现使用方式几乎一样,真的一样吗?其实容器启动的时候还有一个细节输出:

代码语言:javascript
复制
this MyImportSelector...
this MyDeferredImportSelector...

从现象已经和名字中,我们能够更加直观的看出来:DeferredImportSelector显然是属于延迟加载、靠后加载的,那到底有多延迟,他们执行时机都是啥时候呢? 这就是我们接下来讨论的重点

再次强调一次:实现此接口的Bean必须是放在@Import进去的才会生效,而不能直接@Bean加入进去

ImportSelector和DeferredImportSelector的区别:

看了看DeferredImportSelector类的JavaDoc,得到如下信息:

  1. DeferredImportSelector是ImportSelector的一个扩展
  2. ImportSelector实例的selectImports方法的执行时机,是在@Configguration注解中的其他逻辑被处理**之前**,所谓的其他逻辑,包括对@ImportResource、@Bean这些注解的处理(注意,这里只是对@Bean修饰的方法的处理,并不是立即调用@Bean修饰的方法,这个区别很重要!);
  3. DeferredImportSelector实例的selectImports方法的执行时机,是在@Configguration注解中的其他逻辑被处理**完毕之后**
  4. DeferredImportSelector的实现类可以用Order注解,或者实现Ordered接口来对selectImports的执行顺序排序(ImportSelector不支持)
  5. ImportSelector是Spring3.1提供的,DeferredImportSelector是Spring4.0提供的
  6. Spring Boot的自动配置功能就是通过DeferredImportSelector接口的实现类EnableAutoConfigurationImportSelector做到的(因为自动配置必须在我们自定义配置后执行才行
分析Spring源码中对此两个接口的处理

结合这篇博文【小家Spring】Spring解析@Configuration注解的处理器:ConfigurationClassPostProcessor(ConfigurationClassParser)分析,public void parse(Set<BeanDefinitionHolder> configCandidates){ ... }的最最最后一步,才去处理实现了DeferredImportSelector接口的类,因此是非常滞后的(此事已经处理好了@Bean、@ComponentScan、@ImportResource等等事宜)

ImportSelector 被设计成其实和@Import注解的类同样的导入效果,但是实现 ImportSelector的类可以条件性地决定导入哪些配置。 DeferredImportSelector 的设计目的是在所有其他的配置类被处理后才处理。这也正是该语句被放到本函数最后一行的原因。

看看前面做了些什么,我们直接来到核心处理方法doProcessConfigurationClass:(此处稍微详细点)

代码语言:javascript
复制
	@Nullable
	protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
			throws IOException {

		// Recursively process any member (nested) classes first
		// 递归循环的解析内部类的配置类(因此,即使是内部类的配置类,我们Spring也是支持的,很强大有木有)
		processMemberClasses(configClass, sourceClass);

		// Process any @PropertySource annotations
		// 处理@PropertySources注解和@PropertySource注解,交给processPropertySource去解析
		// 显然必须是ConfigurableEnvironment的环境采取解析,否则发出警告:会忽略这个不进行解析
		for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
				sourceClass.getMetadata(), PropertySources.class,
				org.springframework.context.annotation.PropertySource.class)) {
			if (this.environment instanceof ConfigurableEnvironment) {
				processPropertySource(propertySource);
			}
			else {
				logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
						"]. Reason: Environment must implement ConfigurableEnvironment");
			}
		}

		// Process any @ComponentScan annotations
		// 解析@ComponentScans和@ComponentScan注解,进行包扫描。最终交给ComponentScanAnnotationParser#parse方法进行处理
		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) {
				// The config class is annotated with @ComponentScan -> perform the scan immediately
				Set<BeanDefinitionHolder> scannedBeanDefinitions =
						this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
				// Check the set of scanned definitions for any further config classes and parse recursively if needed
				// 这一步非常重要:如果被扫描的Bean定义信息,还是属于@Configuration的配置组件,那就继续调用本类的parse方法,进行递归解析==============
				// 所以我们在进行包扫描的时候,也是会扫描到@Configuration并且进行解析的。。。
				for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
					BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
					if (bdCand == null) {
						bdCand = holder.getBeanDefinition();
					}
					if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
						parse(bdCand.getBeanClassName(), holder.getBeanName());
					}
				}
			}
		}

		// Process any @Import annotations
		// 这里是今天的主菜:解析@Import注解,后面详解processImports方法
		processImports(configClass, sourceClass, getImports(sourceClass), true);

		// Process any @ImportResource annotations
		// 显然,先是处理了@Import,才过来解析@ImportResource的====最终交给environment.resolveRequiredPlaceholders(resource)去处理了
		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);
			}
		}

		// Process individual @Bean methods
		// 处理被标注了@Bean注解的方法们
		// 遍历@Bean注释的方法,添加到configClass中的BeanMethod
		// 这里需要注意的是:最终会实例化的时候是执行此工厂方法来获取到对应实例的
		// if (mbd.getFactoryMethodName() != null) { ... }  这里会是true,从而执行此方法内部逻辑。   原理同XML中的FactoryMethod方式创建Bean
		Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
		for (MethodMetadata methodMetadata : beanMethods) {
			configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
		}

		// Process default methods on interfaces
		// 这个特别有意思:处理接口中被@Bean注解默认方法,代码如下
		// 因为JDK8以后接口可以写default方法了,所以接口竟然也能给容器里注册Bean了
		// 但是需要注意:这里的意思并不是说你写个接口然后标注上@Configuration注解,然后@Bean注入就可以了
		// 这个解析的意思是我们的配置类可以实现接口,然后在所实现的接口里面若有@Bean的注解默认方法,是会加入到容器的
		processInterfaces(configClass, sourceClass);

		// Process superclass, if any
		// 如果有父类的话,则返回父类进行进一步的解析,否则返回null
		// 这个也是很厉害的,如果有父类,也是能够继续解析的。@EnableWebMvc中的DelegatingWebMvcConfiguration就是这么玩的
		// 它自己标注了@Configuration注解,但是真正@Bean注入,都是它父类去干的
		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();
			}
		}

		// No superclass -> processing is complete
		// 没有父类,就停止了,处理结束
		return null;
	}

因此本文,就重点来看看processImports这个方法,如下:

代码语言:javascript
复制
	private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
			Collection<SourceClass> importCandidates, boolean checkForCircularImports) {
		
		// 相当于没有找到@Import注解,那就不处理了
		// 说明:获取@Import是递归获取,任意子类父类上标注有都行的
		if (importCandidates.isEmpty()) {
			return;
		}
		
		//循环依赖检查:如果存在循环依赖的话,则直接抛出异常(比如你@Import我,我@Import你这种情况)
		if (checkForCircularImports && isChainedImportOnStack(configClass)) {
			this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
		}
		else {
			this.importStack.push(configClass);
			try {
				// 依次处理每个@Import里面候选的Bean们
				for (SourceClass candidate : importCandidates) {
					
					// 分之一:如果实现了ImportSelector接口(又分为两种,因为有子接口DeferredImportSelector呢)
					if (candidate.isAssignable(ImportSelector.class)) {
						// Candidate class is an ImportSelector -> delegate to it to determine imports
						Class<?> candidateClass = candidate.loadClass();
			
						// 根据空的构造函数,把这个Bean实例化出来,
						ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
						// 这里面注入了一下感知接口的元素,包括environment、resourceLoader、registry等等(实现了DeferredImportSelector也在此处注入了哦)
						ParserStrategyUtils.invokeAwareMethods(selector, this.environment, this.resourceLoader, this.registry);

						// 判断是否是DeferredImportSelectorHolder的子类,是的话先加入进入  不处理先
						if (this.deferredImportSelectors != null && selector instanceof DeferredImportSelector) {
							this.deferredImportSelectors.add(
									new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector));
						}
						
						// 否则立马调用它的`selectImports`方法,拿到一个BeanName的数组
						else {
							String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
							Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
							// 这里面高级了:因为我们这里放进去的Bean,有可能是普通Bean,当然也还有可能是实现了ImportSelector等等接口的,因此此处继续调用processImports进行处理,递归的效果~~~~
							processImports(configClass, currentSourceClass, importSourceClasses, false);
						}
					}
					//如果实现了ImportBeanDefinitionRegistrar这个接口的
					else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
						// Candidate class is an ImportBeanDefinitionRegistrar ->
						// delegate to it to register additional bean definitions
						Class<?> candidateClass = candidate.loadClass();
						ImportBeanDefinitionRegistrar registrar =
								BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
						ParserStrategyUtils.invokeAwareMethods(
								registrar, this.environment, this.resourceLoader, this.registry);

						// 完成了实例化后和Aware方法后,添加进configClass类的属性importBeanDefinitionRegistrars里先缓存着(至于执行时机,留给下面讲吧)
						configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
					}
					else {
						// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
						// process it as an @Configuration class
						// 什么都接口都没有实现,那就是普通的配置类嘛,那就直接交给processConfigurationClass()去处理了
						// 备注:这个方法的处理流程,请参照上面哦
						// 这里面有个特别重要的地方:是candidate.asConfigClass(configClass)这一句,给包装陈一个ConfigurationClass去处理
						// 因为传入了configClass属于它的importedBy属性,这样一来ConfigurationClass#isImported()就返回true了,表面这个Bean是被单纯的、单纯的、单纯的的导入进来的
						this.importStack.registerImport(
								currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
						processConfigurationClass(candidate.asConfigClass(configClass));
					}
				}
			}
			catch (BeanDefinitionStoreException ex) {
				throw ex;
			}
			catch (Throwable ex) {
				throw new BeanDefinitionStoreException(
						"Failed to process import candidates for configuration class [" +
						configClass.getMetadata().getClassName() + "]", ex);
			}
			finally {
				// 上面push,下面pop出来
				this.importStack.pop();
			}
		}
	}

从上面的源码处理过程,我们可以很清楚的知道了ImportSelector#selectImports执行时机,然后并且把DeferredImportSelectorImportBeanDefinitionRegistrar都先装起来了。

doProcessConfigurationClass执行完成之后,processConfigurationClass也就执行完了,接下来就开始执行顶层parse方法内部的:processDeferredImportSelectors()

附录上DeferredImportSelector的源码:(Spring4和Spring4差异很大) 本文都是基于Spring5进行讲解的

代码语言:javascript
复制
// Spring4的源码,啥都木有
public interface DeferredImportSelector extends ImportSelector {
}

// Sparing5的源码,加了不少东西
public interface DeferredImportSelector extends ImportSelector {
	@Nullable
	default Class<? extends Group> getImportGroup() {
		return null;
	}
	
	// 内部接口
	interface Group {
		void process(AnnotationMetadata metadata, DeferredImportSelector selector);
		Iterable<Entry> selectImports();
		
		// 内部的内部类
		class Entry {
			private final AnnotationMetadata metadata;
			private final String importClassName;
			
			public Entry(AnnotationMetadata metadata, String importClassName) {
				this.metadata = metadata;
				this.importClassName = importClassName;
			}


			public AnnotationMetadata getMetadata() {
				return this.metadata;
			}
			public String getImportClassName() {
				return this.importClassName;
			}

			@Override
			public boolean equals(Object o) {
				if (this == o) {
					return true;
				}
				if (o == null || getClass() != o.getClass()) {
					return false;
				}
				Entry entry = (Entry) o;
				return Objects.equals(this.metadata, entry.metadata) &&
						Objects.equals(this.importClassName, entry.importClassName);
			}

			@Override
			public int hashCode() {
				return Objects.hash(this.metadata, this.importClassName);
			}
		}
	}
}

源码的差异很大,就就造成了processDeferredImportSelectors的处理方式不尽相同。同样的,本文就以Spring5的源码进行讲解了:

代码语言:javascript
复制
	private void processDeferredImportSelectors() {
		List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
		this.deferredImportSelectors = null;
		if (deferredImports == null) {
			return;
		}
		
		// 排序:注意这个比较器。它是按照PriorityOrdered、Ordered等进行优先级排序的
		// 因此我们可以看到一大特性:DeferredImportSelector是支持Order排序的
		deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
		// 这个Map厉害了,key竟然是Object。。。
		Map<Object, DeferredImportSelectorGrouping> groupings = new LinkedHashMap<>();
		Map<AnnotationMetadata, ConfigurationClass> configurationClasses = new HashMap<>();

		// 对这些个DeferredImportSelector一个个处理吧
		//遍历DeferredImportSelector接口集合,获取Group集合类,默认为DefaultDeferredImportSelectorGroup
		for (DeferredImportSelectorHolder deferredImport : deferredImports) {
			
			// getImportGroup()方法是DeferredImportSelector接口的default方法,若不复写,默认return null
			// 该接口的作用是:子类可以对一些Import的类进行分类 
			//Group 为DeferredImportSelector的一个内部接口~~~~~~~~~~~
			Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup();
			
			// 按照group 或者 deferredImport 进行分组
			DeferredImportSelectorGrouping grouping = groupings.computeIfAbsent((group == null ? deferredImport : group), (key) -> new DeferredImportSelectorGrouping(createGroup(group)));
			grouping.add(deferredImport);
			configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(),
					deferredImport.getConfigurationClass());
		}
		
		//遍历Group集合,作用也是调用processImport()方法用于解析@Import
		for (DeferredImportSelectorGrouping grouping : groupings.values()) {
			grouping.getImports().forEach((entry) -> {
				ConfigurationClass configurationClass = configurationClasses.get(
						entry.getMetadata());
				try {
					processImports(configurationClass, asSourceClass(configurationClass),
							asSourceClasses(entry.getImportClassName()), false);
				}
				catch (BeanDefinitionStoreException ex) {
					throw ex;
				}
				catch (Throwable ex) {
					throw new BeanDefinitionStoreException(
							"Failed to process import candidates for configuration class [" +
									configurationClass.getMetadata().getClassName() + "]", ex);
				}
			});
		}

DeferredImportSelector接口在Spring-core/Context中没有实现类。但是在Spring Boot的自动配置中有大量的实现。那么我们将在讲解Spring Boot源码分析的时候,会再回来重点讲解。也看看Spring5为何还要对此接口继续提升

ImportBeanDefinitionRegistrar

该接口功能非常强大,能够实现快速的、批量的、扫描式的注册。比如我们熟悉的ServletComponentScanRegistrar就是去解析注解@ServletComponentScan实现批量注册Bean定义 MapperScannerRegistrar就是MyBatis用来解析@MapperScan注解,来扫描的 等等还有很多类似的设计方式

先看一个最简单的效果吧:

代码语言:javascript
复制
@Configuration
@Import({Parent.class, AntPathMatcher.class, MyImportSelector.class, MyDeferredImportSelector.class, MyImportBeanDefinitionRegistrar.class})
public class RootConfig {

}


public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    // 同样的,这种注入都是不好使的(相同的,那些感知接口是可以实现的,从而注入对应组件)
    @Autowired
    private HelloService helloService;

    /**
     * 实现了该接口让我们的这个类成为了拥有注册bean的能力
     * 也可以让我们实现动态注入(根据条件、逻辑进行动态注入)
     *
     * @param importingClassMetadata 注解信息和本类信息
     * @param registry               注册器,我们可以向容器里面注册[Bean定义信息]
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        System.out.println("this MyImportBeanDefinitionRegistrar");
        RootBeanDefinition beanDefinition = new RootBeanDefinition();
        beanDefinition.setBeanClass(GenericBean.class);
        registry.registerBeanDefinition("genericBean", beanDefinition);
    }
}

输出:(genericBean成功被注入容器了)

代码语言:javascript
复制
rootConfig
helloServiceImpl
com.fsx.bean.Parent
org.springframework.util.AntPathMatcher
com.fsx.bean.Child
genericBean

从另外一个日志的打印来看:MyImportBeanDefinitionRegistrar是更加滞后执行的。那么下面我们就要看看它到底啥时候执行的呢?

代码语言:javascript
复制
this MyImportSelector...
this MyDeferredImportSelector...
this MyImportBeanDefinitionRegistrar

有了上面的源码分析我们知道了,实现了ImportBeanDefinitionRegistrar接口的,最后是这么一句话给缓存下来了(还木有执行):

代码语言:javascript
复制
// registrar是已经被实例化了的当前类
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());

下面重点看看,该接口到底什么时候执行的呢?根据断点,跟踪到它的加载时机是这句代码里:this.reader.loadBeanDefinitions(configClasses);

代码语言:javascript
复制
	public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
		TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
		// 依次解析每一个配置类,这里面注意了configClass 比如我们的RootConfig这个对象里有个字段
		//importBeanDefinitionRegistrars是记录着了我们前面add进去的ImportBeanDefinitionRegistrar的,因此它会在此处开始执行了
		for (ConfigurationClass configClass : configurationModel) {
			loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
		}
	}

	// 这个方法很重要:它处理了多种方式(@Bean、实现接口类注册等等)完成向容器里注册Bean定义信息
	private void loadBeanDefinitionsForConfigurationClass(
			ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
		
		// 如果这咯Config不需要被解析,做一些清理、移除的操作~~~~
		if (trackedConditionEvaluator.shouldSkip(configClass)) {
			String beanName = configClass.getBeanName();
			if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
				this.registry.removeBeanDefinition(beanName);
			}
			this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
			return;
		}

///////////////////////稍微注意一下Spring处理这些Bean定义的顺序,在某些判断逻辑中或许能用到///////////////////////
		
		// 如果是被单纯@Import进来的,这个值是true的,默认值是false哦
		if (configClass.isImported()) {
			// 这个处理源码这里不分析了,比较简单。支持@Scope、@Lazy、@Primary、@DependsOn、@Role、@Description等等一些通用的基本属性
			registerBeanDefinitionForImportedConfigurationClass(configClass);
		}
		
		//处理方法上的@Bean 解析@Bean上面各种属性值。也处理上面提到的那些通用注解@Lazy等等吧
		//这里面只说一个内部比较重要的方法isOverriddenByExistingDefinition(beanMethod, beanName)  
		// 该方法目的主要是去重。其实如果是@Configuration里面Bean重名了,IDEA类似工具发现,但是无法判断xml是否也存在(注意,发现归发现,但并不是编译报错哦~~~)
		// 它的处理策略为:若来自同一个@Configuration配置类,那就保留之前的。若来自不同配置类,那就覆盖
		for (BeanMethod beanMethod : configClass.getBeanMethods()) {
			loadBeanDefinitionsForBeanMethod(beanMethod);
		}
		//处理@ImportResource,里面解析xml就是上面说到的解析xml的XmlBeanDefinitionReader
		//所以,咱们@Configuration和xml是可以并行使用的
		loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
		
		// 最后,这里就是咱们今天的主菜了:解析咱们的ImportBeanDefinitionRegistrars
		// configClass.getImportBeanDefinitionRegistrars():就是我们上面异步add进去的那些注册器们
		loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
	}

loadBeanDefinitionsFromRegistrars完整解析:

代码语言:javascript
复制
// 没什么多余的代码  所有的注册逻辑(哪些Bean需要注册,哪些不需要之类的,全部交给子类去实现)
// 用处:上面有提到,比如@MapperScan这种批量扫描的===
	private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
		registrars.forEach((registrar, metadata) ->
				registrar.registerBeanDefinitions(metadata, this.registry));
	}
应用场景分析

根据各个接口的特点,有各自的应用场景。因为直接@Import普通类的场景相对较少,这里主要说说实现接口的方式的场景:

ImportSelector接口应用场景
  • AdviceModeImportSelector:它是个抽象类。(实现类有出名的AsyncConfigurationSelectorCachingConfigurationSelector等,因为都是基于代理来做的,所以都继承了此抽象)。 它拿到泛型类型(比如@EnableAsync或者@EnableCaching),然后解析注解为AnnotationAttributes,最后由子类去实现select逻辑(具体要向容器注入的Class全类名),比如注入ProxyAsyncConfiguration,或者其它的。。。

总之,向这种还不能决定去注入哪个处理器(如果你能决定,那就直接@Import那个类好了,没必要实现接口了嘛),然后可以实现此接口,写出一些判断逻辑,不同的配置情况注入不同的处理类。

DeferredImportSelector接口应用场景

它和上面只是执行的时机不同。在Spring内部没有应用,但是在Spring Boot中却有大量的应用,比如:

AutoConfigurationImportSelectorEnableCircuitBreakerImportSelector等等(在SpirngBoot章节中还会重点分析)

实现这个接口的基本思想是:默认处理(以用户配置的为准,若用户没管,那就执行我的默认配置呗)。执行生效的一个先后顺序的简单控制

ImportBeanDefinitionRegistrar接口应用场景

它的应用场景特别的有用,因此也是最常使用的。因为她直接可以向工厂里注册Bean的定义信息(当然也可以拿出来Bean定义信息,做出对应的修改)~

下面两个实现,都喝@EnableAspectJAutoProxy注解相关:

  • AspectJAutoProxyRegistrar:它能解析注解的时候,从BeanFactory拿出指定的Bean,设置一些参数值等等
  • AutoProxyRegistrar:自动代理的注册器。它和上面的区别在于它和代理的类型无关(它可以指定mode类型),而上面是表示就是用AspectJ来做切面代理。

实现它的基本思想是:当自己需要操作BeanFactory里面的Bean的时候,那就必须只有它才能做到了。而且它还有个方便的地方,那就是做包扫描的时候,比如@MapperScan类似这种的时候,用它处理更为方便(因为扫描到了直接注册即可) 备注:@Mapper的扫描依赖于ClassPathMapperScanner,它由mybatis-spring提供。它继承与ClassPathBeanDefinitionScanner由Spring底层提供

总结

注意,完成了所有的这些注解、配置文件的解析,Bean们都还只是被加载了,还没有加入到Bean的定义信息里面,更别谈实例化了。要加入到Bean的定义信息里面存储好,还得这一步:

代码语言:javascript
复制
//configClasses就是parser.parse(candidates);上面解析完成了的配置类
//根据这些已经解析好了的配置类,由这个ConfigurationClassBeanDefinitionReader去加载Bean定义信息
this.reader.loadBeanDefinitions(configClasses);

然后,上面已经从源码上分析了Spring处理的一个流程,希望各位小伙伴能更宏观的明晰这个执行的时序图(流程图),最好是能画出来(我这里就不画了哈,若哪位小伙伴有心,欢迎留言贡献一份,不胜感激~)

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年03月15日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 基本环境如下
  • @Import注解
  • ImportSelector和DeferredImportSelector
    • ImportSelector和DeferredImportSelector的区别:
      • 分析Spring源码中对此两个接口的处理
      • ImportBeanDefinitionRegistrar
      • 应用场景分析
        • ImportSelector接口应用场景
          • DeferredImportSelector接口应用场景
            • ImportBeanDefinitionRegistrar接口应用场景
            • 总结
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档