Spring源码解析之ConfigurationClassPostProcessor(三)
在上一章笔者介绍了ConfigurationClassParser.doProcessConfigurationClass(...)方法,在这个方法里调用了processImports(...)方法处理配置类的@Import注解,getImports(sourceClass)能从一个配置类上获取@Import注解配置的所有类形成一个集合,如果集合不为空则会在下面代码的<1>处开始遍历处理。
如果一个类是ImportSelector接口的实现类,会进入<2>处的分支,并在<3>处创建实现类的实例,在<4>处调用实例的selectImports(...)方法获取配置类列表,之后在<5>处以递归的方式调用doProcessConfigurationClass(...)将获取到的配置类传入。
如果一个类是ImportBeanDefinitionRegistrar接口的实现类,则会进入<6>处的分支,这里依旧生成一个实现类的实例,只是这里不立马回调该接口的方法,而是暂存在配置类中,待从ConfigurationClassParser.processImports(...)逐层返回到ConfigurationClassPostProcessor.processConfigBeanDefinitions(...)的do-while循环后会执行ImportBeanDefinitionRegistrar实例的回调方法。
如果一个类仅仅是平平无奇的配置类,即不是ImportSelector的实现类,也不是ImportBeanDefinitionRegistrar的实现类,则会进入<7>处的分支,这里会将配置类传给processConfigurationClass(...)方法,这里我们也可以认为是递归调用,这个方法笔者之前讲过,会将配置类上的@ComponentScans、@ComponentScan注解指定的类路径解析出来,根据类路径扫描BeanDefinition,再判断扫描出来的类是否有资格成为配置类,如果可以的话会再进一步解析。
class ConfigurationClassParser { …… protected final SourceClass doProcessConfigurationClass( ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter) throws IOException { …… processImports(configClass, sourceClass, getImports(sourceClass), filter, true); …… } private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter, boolean checkForCircularImports) { if (importCandidates.isEmpty()) { return; } if (checkForCircularImports && isChainedImportOnStack(configClass)) { this.problemReporter.error(new CircularImportProblem(configClass, this.importStack)); } else { this.importStack.push(configClass); try { for (SourceClass candidate : importCandidates) {//<1> if (candidate.isAssignable(ImportSelector.class)) {//<2> // Candidate class is an ImportSelector -> delegate to it to determine imports Class<?> candidateClass = candidate.loadClass(); ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class, this.environment, this.resourceLoader, this.registry);//<3> Predicate<String> selectorFilter = selector.getExclusionFilter(); if (selectorFilter != null) { exclusionFilter = exclusionFilter.or(selectorFilter); } if (selector instanceof DeferredImportSelector) { this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector); } else { String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());//<4> Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter); processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);//<5> } } else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {//<6> // Candidate class is an ImportBeanDefinitionRegistrar -> // delegate to it to register additional bean definitions Class<?> candidateClass = candidate.loadClass(); ImportBeanDefinitionRegistrar registrar = ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class, this.environment, this.resourceLoader, this.registry); configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata()); } else {//<7> // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar -> // process it as an @Configuration class this.importStack.registerImport( currentSourceClass.getMetadata(), candidate.getMetadata().getClassName()); processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter); } } } catch (BeanDefinitionStoreException ex) { throw ex; } catch (Throwable ex) { throw new BeanDefinitionStoreException( "Failed to process import candidates for configuration class [" + configClass.getMetadata().getClassName() + "]", ex); } finally { this.importStack.pop(); } } } …… }
那么我们来总结一下ConfigurationClassParser.processImports(...)完成的工作,这个方法主要用于处理配置类上的@Import注解,@Import注解允许我们引入多个配置类,在这个方法会遍历处理这些配置类,如果配置类是ImportSelector的实现类,执行实现类的回调方法能立马拿到待引入的配置类列表,我们可以递归调用processImports(...)方法将拿到的配置类列表传入,在这个方法会处理来自上层传入的配置类。如果配置类是ImportBeanDefinitionRegistrar接口的实现类,这里仅仅是实例化这个类的实例并暂存到配置类,并不立即调用实现类的回调方法,因为spring不能肯定开发者会往传入的registrar对象注册多少个配置类,甚至有的开发者一个都不会注册。如果一个类不是ImportSelector或ImportBeanDefinitionRegistrar接口的实现类,会调用processConfigurationClass(...)方法将配置类传入,我们可以认为这里是递归调用,因为processConfigurationClass(...)方法最终会执行到processImports(...)方法。processConfigurationClass(...)方法会解析配置类指定的类路径,并将可以成为BeanDefinition的类注册进spring容器,在尝试将扫描出来的类作为配置类进行解析。
在了解ConfigurationClassParser.parse(...)方法完成的工作后,我们来整体过一下ConfigurationClassPostProcessor.processConfigBeanDefinitions(...)方法,这个方法在最开始的时候会获取spring容器所有的beanName,遍历这些beanName拿到开发者传给容器的配置类,因为开发者可能传入多个配置类,所以这里会行程一个配置类列表,如果列表为空则退出该方法,如果列表不为空,则对列表中的配置类进行排序。
在对配置类列表排序完毕后,会进入一个do-while循环解析配置类。解析配置类是一项非常复杂的工作,解析配置类的时候不但会扫描配置类指定的类路径,还会尝试将扫描出来的BeanDefinition当做一个配置类再次进行解析,如果扫描出来的BeanDefinition可以作为配置类会再次进行二次解析,这里又会重复之前所说的解析工作,在解析完毕后,才会处理@Import注解。
在了解完spring是如何解析配置类后,我们来再来整体过下ConfigurationClassPostProcessor.processConfigBeanDefinitions(...)的逻辑,在<1>处会先用candidateNames这个数组存放spring容器目前所有的beanName。
candidateNames有以下两点作用:
- spring会在<2>处先根据candidateNames中存放的beanName过滤出可以成为配置类的的BeanDefinition。
- candidateNames可以用于判断解析配置类时是否有新的BeanDefinition被注册进spring容器,可以认为candidateNames是解析配置类前的快照,如果解析完配置类后发现spring容器的BeanDefinition数量大于快照中beanName的数量,表示在解析的时候有引入新的BeanDefinition。
在<2>处完成遍历后会将配置类的BeanDefinition和beanName包装成一个BeanDefinitionHolder对象存进configCandidates列表,之后再<3>处会根据configCandidates列表生成一个配置类集合candidates,<4>处同样会根据配置类的长度生成一个集合alreadyParsed,这个集合会存放已经解析的配置类。
之后会进入一个do-while循环,在循环中会解析、校验配置类。<5>处会获取所有解析出来的配置类,这里笔者所说的不单单是我们平常用@Configuration注解标记的配置类,在解析方法里会把类路径下描到的BeanDefinition尝试作为配置类进行解析,所以<5>处的方法还会将类路径下可以作为配置类的BeanDefinition返回。我们可以在配置类上用@Import注解引入一个ImportBeanDefinitionRegistrar实现类,也可以用@ImportResource注解引入一个XML配置文件,ImportBeanDefinitionRegistrar实现类和XML文件在解析的时候会暂存在配置类中,等到从解析方法回到do-while循环时就会在<7>处配置类中暂存的ImportBeanDefinitionRegistrar实例和XML文件,这里可能会向spring容器注册新的BeanDefinition。
在执行完<7>处的代码后,配置类才算完全被解析,在<8>会将已经被完全解析的配置类存放进alreadyParsed集合,同时我们往回看,每次执行do-while循环时,<5>处总会把目前获取到的配置类返回,configClasses可能存在已经被完全解析的配置类,所以在<6>处会把已经完全解析的配置类移除,<7>处仅处理目前需要处理尚未回调ImportBeanDefinitionRegistrar接口的实例和XML文件。
在<8>处将完全处理完毕的配置类放到alreadyParsed集合后,会清空candidates集合,如果在<9>处判断目前spring容器拥有的BeanDefinition数量大于解析配置类前beanName的数量,会进入分支<9>看看是否有需要再解析的配置类,如果有配置类添加到candidates集合则开启新的一轮的循环。如果<9>处判断解析完配置类后spring容器的BeanDefinition数量等于原先解析前beanName的数量,则candidates集合为空退出do-while循环。
那么在分支<9>中又是如何判断是否应该往candidates集合添加新的配置类呢?在分支<9>内会先把spring容器目前所有的beanName存放到newCandidateNames这个数组中,oldCandidateNames用于存放解析前的beanName,alreadyParsedClasses用于存放解析后的所有配置类。之后会在<10>处遍历spring容器目前所有的beanName,如果beanName在oldCandidateNames集合中,表示其BeanDefinition在本次循环或之前的循环已经被解析,这里不会进入<11>处的分支。如果beanName不在oldCandidateNames集合中,那么这个beanName对应的BeanDefinition有可能是在解析时注册进spring容器,也可能是解析配置类完毕后在<7>处注册进spring容器的,如果是在解析时引入,那么alreadyParsedClasses会包含这个配置类,这里不会进入<12>处的分支。如果是在<7>处处理暂存在配置类的ImportBeanDefinitionRegistrar实例和XML文件,那么alreadyParsedClasses不会包含BeanDefinition对应的类,这里只要ConfigurationClassUtils.checkConfigurationClassCandidate(...)判断BeanDefinition对应的类是配置类就会把BeanDefinition和beanName包装成BeanDefinitionHolder对象存进candidates。最后在<13>处将spring目前所有的beanName存进数组candidateNames,作为下一轮循环解析前的快照。如果下一轮循环在<9>处判断spring容器中的BeanDefinition数量等于上一轮的快照数量,就会退出循环。另外进入了<9>处的分支并不意味着一定会有下个循环,do-while退出循环的条件是candidates为空,只要新引入的BeanDefinition都是解析出来的配置类,或者新引入的BeanDefinition都不是配置类,则不会进入分支<12>,则candidates为空,会退出do-while循环。
public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware { …… public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { List<BeanDefinitionHolder> configCandidates = new ArrayList<>(); String[] candidateNames = registry.getBeanDefinitionNames();//<1> for (String beanName : candidateNames) {//<2> BeanDefinition beanDef = registry.getBeanDefinition(beanName); if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) { if (logger.isDebugEnabled()) { logger.debug("Bean definition has already been processed as a configuration class: " + beanDef); } } else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) { configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); } } …… // Parse each @Configuration class ConfigurationClassParser parser = new ConfigurationClassParser( this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry); Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);//<3> Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());//<4> do { parser.parse(candidates); parser.validate(); Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());//<5> configClasses.removeAll(alreadyParsed);//<6> // Read the model and create bean definitions based on its content if (this.reader == null) { this.reader = new ConfigurationClassBeanDefinitionReader( registry, this.sourceExtractor, this.resourceLoader, this.environment, this.importBeanNameGenerator, parser.getImportRegistry()); } this.reader.loadBeanDefinitions(configClasses);//<7> alreadyParsed.addAll(configClasses);//<8> candidates.clear(); if (registry.getBeanDefinitionCount() > candidateNames.length) {//<9> String[] newCandidateNames = registry.getBeanDefinitionNames(); Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames)); Set<String> alreadyParsedClasses = new HashSet<>(); for (ConfigurationClass configurationClass : alreadyParsed) { alreadyParsedClasses.add(configurationClass.getMetadata().getClassName()); } for (String candidateName : newCandidateNames) {//<10> if (!oldCandidateNames.contains(candidateName)) {//<11> BeanDefinition bd = registry.getBeanDefinition(candidateName); if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) && !alreadyParsedClasses.contains(bd.getBeanClassName())) {//<12> candidates.add(new BeanDefinitionHolder(bd, candidateName)); } } } candidateNames = newCandidateNames;//<13> } } while (!candidates.isEmpty()); …… } …… }
至此,我们了解在执行ConfigurationClassPostProcessor.processConfigBeanDefinitions(...)的时候是如何完成扫描类路径,笔者再介绍在上面的方法是如何完成配置类的校验,如何执行配置类中暂存的ImportBeanDefinitionRegistrar实例引入新的BeanDefinition,就结束本章对ConfigurationClassPostProcessor的介绍。
首先我们来看配置类是如何完成校验的,在执行ConfigurationClassParser.validate()方法的时候会遍历已经获取到的所有配置类ConfigurationClass.validate(...)方法,在配置类校验的时候,会检查如果配置类是full模式(有标记@Configuration注解且proxyBeanMethods为true),这个full模式的配置类是否有使用fina修饰符,如果是full模式的配置类,那么类中是否有同时使用了@Bean注解和fina修饰符的方法?之所以做这样的校验:是因为一个full模式的配置类是要使用cglib技术代理的,如果配置类本身使用了final修饰符是无法被继承的,则不能通过校验。如果full模式的配置类有用@Bean注解标记的方法,方法本身也使用了final修饰符,这个方法是不能被重写的,就不允许通过校验。
但如果大家将@Configuration、@Bean和final修饰符搭配使用,会发现java会出现编译错误,既然连编译这关都过不了,为何还要校验呢?spring这里是防止编译出来的字节码被篡改,有人特意在编译后的full模式配置类字节码加上final修饰符,在在这个方法里再次对配置类进行校验。
class ConfigurationClassParser { …… public void validate() { for (ConfigurationClass configClass : this.configurationClasses.keySet()) { configClass.validate(this.problemReporter); } } …… } final class ConfigurationClass { …… public void validate(ProblemReporter problemReporter) { // A configuration class may not be final (CGLIB limitation) unless it declares proxyBeanMethods=false Map<String, Object> attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName()); if (attributes != null && (Boolean) attributes.get("proxyBeanMethods")) { if (this.metadata.isFinal()) {//校验full模式的配置类是否使用final修饰符 problemReporter.error(new FinalConfigurationProblem()); } for (BeanMethod beanMethod : this.beanMethods) {//校验full模式配置类的方法 beanMethod.validate(problemReporter); } } } …… } final class BeanMethod extends ConfigurationMethod { …… public void validate(ProblemReporter problemReporter) { if (getMetadata().isStatic()) { // static @Bean methods have no constraints to validate -> return immediately return; } if (this.configurationClass.getMetadata().isAnnotated(Configuration.class.getName())) { if (!getMetadata().isOverridable()) {//校验方法是否允许被重写 // instance @Bean methods within @Configuration classes must be overridable to accommodate CGLIB problemReporter.error(new NonOverridableMethodError()); } } } …… }
最后,我们来了解下ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(...)是如何在解析完配置类后,又从配置类加载新的BeanDefinition到spring容器里。这个方法会调用loadBeanDefinitionsForConfigurationClass(...)方法遍历传入的配置类,在代码<1>处会遍历配置类中加了@Bean注解的方法,这里会调用loadBeanDefinitionsForBeanMethod(...)将@Bean方法包装成一个BeanDefinition注册进spring容器,因为我们知道spring会根据@Bean方法实例化一个bean对象,而BeanDefinition是生产bean对象的原料。
在根据@Bean方法生成BeanDefinition时,会先在<2>处获取方法名,然后在<3>处获取bean对象的别名,并在<4>处建立beanName和别名的映射。之后会在<5>处根据配置类、元数据和方法名创建一个ConfigurationClassBeanDefinition对象,创建bean对象的方法是否是静态决定了控制流是进入<6>处还是<7>处的分支。如果是用静态方法来创建bean对象,在<6>处就设置静态方法对应的类以及方法名。如果是实例方法创建bean对象则进入<7>处的分支,在<7>处的分支会设置可以创建当前bean对象的工厂beanName,即配置类的beanName,再设置创建bean对象的方法名。
在<8>处会设置@Bean方法的BeanDefinition的默认自动装配模型为AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR,但@Bean注解默认的自动装配模型为Autowire.NO,在<9>处会获取@Bean注解配置的自动装配模型并存放到BeanDefinition。在<10>处会获取@Bean注解设置的是否自动参与候选,默认为true。之后在<11>处获取初始化方法,在<12>处获取销毁方法,如果这两个字段有设置的话。最后在<13>处将beanName和BeanDefinition的映射注册进spring容器中。这里我们又看到一个BeanDefinition的实现类型。
在<1>处的方法遍历完所有的beanMethod将其BeanDefinition注册进spring容器后,会分别在<14>和<15>处处理暂存在配置类的XML配置文件和ImportBeanDefinitionRegistrar实例。在<15>处loadBeanDefinitionsFromRegistrars(...)方法的实现我们也看到在这个方法内会遍历配置类中所有的ImportBeanDefinitionRegistrar实例,回调其registerBeanDefinitions(...)方法,在这个方法中开发者可以向spring容器注册新的BeanDefinition。
class ConfigurationClassBeanDefinitionReader { …… public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) { TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator(); for (ConfigurationClass configClass : configurationModel) { loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator); } } private void loadBeanDefinitionsForConfigurationClass( ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) { …… for (BeanMethod beanMethod : configClass.getBeanMethods()) {//<1> loadBeanDefinitionsForBeanMethod(beanMethod); } loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());//<14> loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());//<15> } …… private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { ConfigurationClass configClass = beanMethod.getConfigurationClass(); MethodMetadata metadata = beanMethod.getMetadata(); String methodName = metadata.getMethodName();//<2> …… AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class); Assert.state(bean != null, "No @Bean annotation attributes"); // Consider name and any aliases List<String> names = new ArrayList<>(Arrays.asList(bean.getStringArray("name")));//<3> String beanName = (!names.isEmpty() ? names.remove(0) : methodName); // Register aliases even when overridden for (String alias : names) {//<4> this.registry.registerAlias(beanName, alias); } …… ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata, beanName);//<5> beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource())); if (metadata.isStatic()) {//<6> // static @Bean method if (configClass.getMetadata() instanceof StandardAnnotationMetadata) { beanDef.setBeanClass(((StandardAnnotationMetadata) configClass.getMetadata()).getIntrospectedClass()); } else { beanDef.setBeanClassName(configClass.getMetadata().getClassName()); } beanDef.setUniqueFactoryMethodName(methodName); } else {//<7> // instance @Bean method beanDef.setFactoryBeanName(configClass.getBeanName()); beanDef.setUniqueFactoryMethodName(methodName); } if (metadata instanceof StandardMethodMetadata) { beanDef.setResolvedFactoryMethod(((StandardMethodMetadata) metadata).getIntrospectedMethod()); } beanDef.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);//<8> beanDef.setAttribute(org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor. SKIP_REQUIRED_CHECK_ATTRIBUTE, Boolean.TRUE); AnnotationConfigUtils.processCommonDefinitionAnnotations(beanDef, metadata); Autowire autowire = bean.getEnum("autowire");//<9> if (autowire.isAutowire()) { beanDef.setAutowireMode(autowire.value()); } boolean autowireCandidate = bean.getBoolean("autowireCandidate");//<10> if (!autowireCandidate) { beanDef.setAutowireCandidate(false); } String initMethodName = bean.getString("initMethod");//<11> if (StringUtils.hasText(initMethodName)) { beanDef.setInitMethodName(initMethodName); } String destroyMethodName = bean.getString("destroyMethod");//<12> beanDef.setDestroyMethodName(destroyMethodName); …… BeanDefinition beanDefToRegister = beanDef; …… this.registry.registerBeanDefinition(beanName, beanDefToRegister);//<13> } …… private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) { registrars.forEach((registrar, metadata) -> registrar.registerBeanDefinitions(metadata, this.registry, this.importBeanNameGenerator)); } …… }