Spring源码解析之ConfigurationClassPostProcessor(二)

时间:2023-03-09 16:12:43
Spring源码解析之ConfigurationClassPostProcessor(二)

上一个章节,笔者向大家介绍了spring是如何来过滤配置类的,下面我们来看看在过滤出配置类后,spring是如何来解析配置类的。首先过滤出来的配置类会存放在configCandidates列表, 在代码<1>处会先根据配置类的权重做一个排序,权重越低的配置类排在越前,在解析的时候也越先解析。之后会根据configCandidates列表生成一个set集合candidates,防止configCandidates列表存在相同的元素。之后会在<3>处解析这些配置类,并在<4>处校验解析出来的配置类。由于在解析配置类的时候,可能引入其他的配置类,所以spring这里做了一个do-while循环,这个循环会一直持续到spring确定不会再有新的配置类引入时退出。

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,
PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {
……
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
……
// Sort by previously determined @Order value, if applicable
configCandidates.sort((bd1, bd2) -> {//<1>
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});
……
// 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);//<2>
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
do {
parser.parse(candidates);//<3>
parser.validate();//<4>
……
}
while (!candidates.isEmpty());
……
}
……
}

  

那么什么情况下会出现扫描类的时候引入新的配置类呢?spring提供了@Import注解允许我们用三种不同的方式在解析配置类的时候引入新的配置类。@Import通常和@Configuration搭配使用,但也可以独立存在。我们可以用@Import直接引入一个配置类,也可以实现ImportSelector或ImportBeanDefinitionRegistrar其中一个接口,在接口里面返回或注册配置类,同样用@Import引入这两个接口的实现类。

我们在MyConfig9这个配置类上@Import注解引入MyConfig10配置类,在测试用例中将MyConfig9传给应用上下文,从运行结果可以看到即便MyConfig10并没有直接传给应用上下文,spring容器依旧会扫描出org.example.dao类路径下的类并构造相应的bean对象。

//MyConfig9.java
@ComponentScan("org.example.service")
@Import(MyConfig10.class)
public class MyConfig9 {
} //MyConfig10.java
@ComponentScan("org.example.dao")
public class MyConfig10 {
public MyConfig10() {
System.out.println("构造MyConfig10...");
}
}

  

测试用例:

    @Test
public void test13() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig9.class);
System.out.println("userDao:" + ac.getBean(UserDao.class));
}

  

运行结果:

构造MyConfig10...
userDao:org.example.dao.UserDao@707194ba

  

接下来我们看看如何通过ImportSelector实现类来引入一个配置类,MyConfig10ImportSelector实现了ImportSelector接口,这个接口会要求开发者返回一个配置类类名的列表,然后再传给应用上下文的配置类上用@Import引入ImportSelector的实现类,从运行结果也可以看到spring容器将org.example.dao类路径下的类扫描出来。

//MyConfig11.java
@ComponentScan("org.example.service")
@Import(MyConfig10ImportSelector.class)
public class MyConfig11 {
} //MyConfig10ImportSelector.java
public class MyConfig10ImportSelector implements ImportSelector {
public MyConfig10ImportSelector() {
System.out.println("构造MyConfig10ImportSelector...");
} @Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{MyConfig10.class.getName()};
}
}

    

测试用例:

    @Test
public void test14() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig11.class);
System.out.println("userDao:" + ac.getBean(UserDao.class));
}

  

运行结果:

构造MyConfig10...
userDao:org.example.dao.UserDao@687e99d8

  

ImportBeanDefinitionRegistrar和ImportSelector有些类似,也是提供一个接口让开发者提供配置类,只不过这个接口要求开发者必须对spring的一些实现设计有些了解,这个接口需要我们将配置类以BeanDefinition的形式注册进spring容器,这种做法同样可以让spring将新引入的配置类指定的类路径扫描出来。

//MyConfig12.java
@ComponentScan("org.example.service")
@Import(MyConfig10Registrar.class)
public class MyConfig12 {
} //MyConfig10Registrar.java
public class MyConfig10Registrar implements ImportBeanDefinitionRegistrar {
public MyConfig10Registrar() {
System.out.println("构造MyConfig10Registrar...");
} @Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
AnnotatedGenericBeanDefinition beanDefinition = new AnnotatedGenericBeanDefinition(MyConfig10.class);
registry.registerBeanDefinition("myConfig10", beanDefinition);
}
}

    

测试用例:

    @Test
public void test15() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig12.class);
System.out.println("userDao:" + ac.getBean(UserDao.class));
}

  

运行结果:

构造MyConfig10...
userDao:org.example.dao.UserDao@45b4c3a9

我们总结下三种用@Import引入配置类的方式:如果确定了要引入的配置类,我们可以简单地用@Import引入。如果在引入配置类的时候,需要加一些业务逻辑,可以实现ImportSelector或ImportBeanDefinitionRegistrar接口,然后用@Import将其实现类的类对象引入。这两个接口的区别在笔者看来ImportSelector对开发者的要求是少一些,因为仅需要开发者提供待引入的配置类的类名,我们不需要去管spring是如何将配置类包装成BeanDefinition。而ImportBeanDefinitionRegistrar接口对开发者的要求会更高一些,毕竟我们要自己将配置类包装成BeanDefinition注册进spring容器,但这个接口的灵活性也更高,我们可以从这个接口传进来的BeanDefinitionRegistry对象获取其他BeanDefinition,修改这个BeanDefinition的属性从而改变这个BeanDefinition的行为,比如:我们获取到一个BeanDefinition后再获取其类对象,然后用cglib技术生成该类对象的子类,这个子类在调用父类的方法时可以加一个打印调用父类方法的耗时日志。

那么我们来看看在ConfigurationClassParser.parse(...)方法中是如何完成递归解析的,在parse(...)方法中会遍历配置类的BeanDefinition,根据不同情况调用其他parse(...)的重载方法,从其他3个重载的parse(...)方法来看,最终都会调用到ConfigurationClassParser.processConfigurationClass(...)方法。

class ConfigurationClassParser {
…… public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
} this.deferredImportSelectorHandler.process();
} protected final void parse(@Nullable String className, String beanName) throws IOException {
Assert.notNull(className, "No bean class name for configuration class bean definition");
MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className);
processConfigurationClass(new ConfigurationClass(reader, beanName), DEFAULT_EXCLUSION_FILTER);
} protected final void parse(Class<?> clazz, String beanName) throws IOException {
processConfigurationClass(new ConfigurationClass(clazz, beanName), DEFAULT_EXCLUSION_FILTER);
} protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER);
}
……
}

  

在processConfigurationClass(...)方法中我们又看到一个do-while循环,之所以存在这个循环,是淫威可能存在配置类继承配置类的情况,比如我们声明了一个MyConfig13的配置类,并继承MyConfig9,我们将MyConfig13传给应用上下文,在<1>处的代码处理完MyConfig13后,会判断其父类是否有被处理的需要,如果有的话会进行第二次循环。在处理完配置类后,会在<2>处将配置类放进configurationClasses这个集合。

class ConfigurationClassParser {
……
private final Map<ConfigurationClass, ConfigurationClass> configurationClasses = new LinkedHashMap<>();
……
protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException {
……
// Recursively process the configuration class and its superclass hierarchy.
SourceClass sourceClass = asSourceClass(configClass, filter);
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);//<1>
}
while (sourceClass != null); this.configurationClasses.put(configClass, configClass);//<2>
}
……
}

   

在执行ConfigurationClassParser.doProcessConfigurationClass(...)的时候,会先在<1>处获取配置类的扫描路径,还是以上面MyConfig13继承MyConfig10为例,sourceClass初始的类对象为MyConfig13,所以这里会先获取到MyConfig13指向的类路径,之后会在<2>处遍历MyConfig3指向的类路径,将类路径下的类封装成BeanDefinition,再将BeanDefinition及beanName封装成BeanDefinitionHolder对象存到一个集合内并返回,即<3>处的集合。

当扫描并获取到类路径下的BeanDefinitionHolder,会遍历这些对象执行<4>和<5>这两个方法,相信这两个方法大家一定不会觉得陌生,<4>处是判断一个BeanDefinition是能可以成为配置类,可以的话是full模式还是lite模式,<5>处会再次调用ConfigurationClassParser.parse(...)解析配置类。看到这里也许有些人心里有些猜测,没错,这里会递归解析,ConfigurationClassParser.parse(...)在根据配置类指定的类路径扫描处类后,又会将尝试将一个类当做配置类进行解析。假设配置类的类路径为org.example.service,在这个类路径下有一个用@Component注解标记的TestService类,那么这个类就是一个lite模式的配置类,不管这个类有没有用@ComponentScan或@ComponentScans注解指定类路径,都会尝试对TestService类进行解析,如果有配置类路径,则再一次扫描新的类路径下的类。需要注意一点的是:注解具有继承性,@Repository、@Service、@Controller和@Configuration都继承了@Component,也就是说只要类上标记了这几个注解,spring都会认为这个类是配置类。

如果配置类有加@Import注解,则会在<6>处进行处理。如果配置类有加@@ImportResource注解,则会在<7>处将注解指定的文件名存到configClass对象内,待后续解析XML文件。同样类种有@Bean注解,在<8>处会解析出类里带@Bean注解的方法,存放到configClass对象内。

最后在<9>处判断sourceClass的父类是否需要处理,之前笔者拿MyConfig13继承MyConfig10的例子说过,在初始执行ConfigurationClassParser.doProcessConfigurationClass(...)的时候,configClass和sourceClass都是指向MyConfig13类对象,所以在<9>处会判断MyConfig13有父类,之后获取其父类MyConfig10的类名,在<10>处判断这个类的并不是java.*包下的类,这一步主要是为了防止解析java.lang.Object,大部分开发者都知道Object在Java中是所有类的父类。只要一个类的父类不为null,按类名不是java.*包下的类,且父类没有被处理过(即:不在knownSuperclasses集合),这里会将其父类放到knownSuperclasses集合并返回父类,这里会返回封装了MyConfig10的sourceClass对象。ConfigurationClassParser.doProcessConfigurationClass(...) 判断返回的sourceClass不为null,则会再次处理。如果判断是java.*包下的类,则会返回空,ConfigurationClassParser.doProcessConfigurationClass(...) 在判断返回的sourceClass为null就会退出循环。

class ConfigurationClassParser {
……
private final ComponentScanAnnotationParser componentScanParser;
……
protected final SourceClass doProcessConfigurationClass(
ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
throws IOException {
……
// Process any @ComponentScan annotations
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);//<1>
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {//<2>
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());//<3>
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {//<4>
parse(bdCand.getBeanClassName(), holder.getBeanName());//<5>
}
}
}
} // Process any @Import annotations
processImports(configClass, sourceClass, getImports(sourceClass), filter, true);//<6> // Process any @ImportResource annotations
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {//<7>
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
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);//<8>
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
} // Process default methods on interfaces
processInterfaces(configClass, sourceClass); // Process superclass, if any
if (sourceClass.getMetadata().hasSuperClass()) {//<9>
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&//<10>
!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;
}
……
}

  

在上面代码的<3>处会完成扫描类路径返回一个BeanDefinitionHolder对象列表,BeanDefinitionHolder对象包含BeanDefinition及其beanName,那么我们来看看在<3>处是如何来完成扫描的。

在这个方法里我们又看到ClassPathBeanDefinitionScanner这个类,不知道大家还有没有印象,笔者曾经在Spring源码解析之BeanFactoryPostProcessor(二)这一章节中介绍这个类,在初始化注解应用上下文时(AnnotationConfigApplicationContext),注解上下文的默认构造方法会初始化一个ClassPathBeanDefinitionScanner对象,当时笔者说过这个对象的作用就是用来传递一个类路径,根据类路径扫描BeanDefinition,并且笔者在这个章节也说了,虽然应用上下文对象里的ClassPathBeanDefinitionScanner对象可以根据类路径扫描BeanDefinition,但我们在配置类上指定的类路径并不是应用上下文内部的ClassPathBeanDefinitionScanner对象来扫描的,这里我们也看到了将类路径下的类扫描成BeanDefinition是在执行ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(...)方法时,执行到ComponentScanAnnotationParser.parse(...)这里会初始化一个ClassPathBeanDefinitionScanner对象来解析扫描类路径。

@ComponentScan注解允许我们设置一些扫描规则,比如:includeFilters、excludeFilters这两个属性可以规定哪些类可以扫描,哪些类不可以扫描,lazyInit可以决定类路径下的类是否是懒加载的,如果我们希望某个类路径下的类只有在需要的时候才去创建bean对象,并不需要将类路径下的每个类都加上@Lazy注解,直接针对指定该类路径@ComponentScan注解设置其lazyInit属性为true即可。下面这段代码根据我们在@ComponentScan注解里设置的属性相应的修改scanner对象的属性,比如<1>处增加将类解析为BeanDefinition的条件,<2>处增加判断一个类不是BeanDefinition的条件,<3>处设置这个类路径的类是否都是懒加载。最后会在<4>处调用scanner.doScan(...) 方法扫描出类路径下可以成为BeanDefinition的类。scanner.doScan(...) 方法方法的实现在Spring源码解析之BeanFactoryPostProcessor(二)章节已经讲过,这里就不再赘述。

class ComponentScanAnnotationParser {
……
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
……
for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addIncludeFilter(typeFilter);//<1>
}
}
for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addExcludeFilter(typeFilter);//<2>
}
} boolean lazyInit = componentScan.getBoolean("lazyInit");
if (lazyInit) {
scanner.getBeanDefinitionDefaults().setLazyInit(true);//<3>
} Set<String> basePackages = new LinkedHashSet<>();
String[] basePackagesArray = componentScan.getStringArray("basePackages");
for (String pkg : basePackagesArray) {
String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(basePackages, tokenized);
}
for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
if (basePackages.isEmpty()) {
basePackages.add(ClassUtils.getPackageName(declaringClass));
} scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
@Override
protected boolean matchClassName(String className) {
return declaringClass.equals(className);
}
});
return scanner.doScan(StringUtils.toStringArray(basePackages));//<4>
}
……
}