ConfigurationClassPostProcessor
在前面一个章节,笔者和大家介绍了在构造一个应用上下文时,spring会执行到PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(...)方法,我们已经清楚这个方法的整个流程,也知道在这个方法里会调用spring内置的BeanDefinitionRegistryPostProcessor实现类——ConfigurationClassPostProcessor的postProcessBeanDefinitionRegistry(...)方法,在这个方法中会完成BeanDefinition的扫描。本章我们就要来学习ConfigurationClassPostProcessor这个类,看看这个类实现的processConfigBeanDefinitions(...)是如何完成扫描BeanDefinition。
在这个方法中会在<1>处获取传入的BeanDefinitionRegistry对象的唯一哈希码(identityHashCode)registryId,判断这个哈希码是否在registriesPostProcessed集合中,如果存在则表示当前的配置类后置处理器(ConfigurationClassPostProcessor)曾经处理过传入的BeanDefinitionRegistry对象,会进入<2>处的分支抛出异常。如果配置类后置处理器从未处理过传入的BeanDefinitionRegistry对象,在<3>处会把BeanDefinitionRegistry对象的哈希码加入到registriesPostProcessed集合。
代码<1>~<3>只是做了幂等性校验,避免配置类后置处理器重复处理同一个BeanDefinitionRegistry对象,这里我们也可以看出扫描BeanDefinition的工作一定不是在<1>~<3>处的代码完成的,那就只能是在21行执行processConfigBeanDefinitions(registry)时完成的。同时我们也注意到在<2>~<3>之间会把哈希码加到factoriesPostProcessed集合,之所以有这一步操作是为了后续执行配置类后置处理器时如果发现传入的BeanFactory对象的哈希码在factoriesPostProcessed集合中,可以将BeanFactory对象强制转换成BeanDefinitionRegistry类型做一些额外的工作。
public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware { …… private final Set<Integer> registriesPostProcessed = new HashSet<>(); private final Set<Integer> factoriesPostProcessed = new HashSet<>(); …… @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { int registryId = System.identityHashCode(registry);//<1> if (this.registriesPostProcessed.contains(registryId)) {//<2> throw new IllegalStateException( "postProcessBeanDefinitionRegistry already called on this post-processor against " + registry); } if (this.factoriesPostProcessed.contains(registryId)) { throw new IllegalStateException( "postProcessBeanFactory already called on this post-processor against " + registry); } this.registriesPostProcessed.add(registryId);//<3> processConfigBeanDefinitions(registry); } …… }
注:通常获取对象的哈希码都是调用继承自Object类的hashCode()方法,但因为这个方法存在被重写的可能,所以上面的代码在<1>处System.identityHashCode(Object x)本地方法获取对象默认的哈希码。
在下面这个测试用例中,我们创建了A类和B类,B类相较于A类重写了hashCode()方法,我们对比下重写hashCode()方法后调用obj.hashCode()和调用System.identityHashCode(obj)的区别。
@Test public void test08() { class A { } A a = new A(); System.out.println("a hashCode:" + a.hashCode()); System.out.println("a identityHashCode:" + System.identityHashCode(a)); class B { @Override public int hashCode() { return 0; } } B b1 = new B(); System.out.println("b1 hashCode:" + b1.hashCode()); System.out.println("b1 identityHashCode:" + System.identityHashCode(b1)); B b2 = new B(); System.out.println("b2 hashCode:" + b2.hashCode()); System.out.println("b2 identityHashCode:" + System.identityHashCode(b2)); }
执行结果:
a hashCode:1654589030 a identityHashCode:1654589030 b1 hashCode:0 b1 identityHashCode:33524623 b2 hashCode:0 b2 identityHashCode:947679291
从执行结果可以看到,没有重写hashCode()的A类在调用对象本身的hashCode()方法或者调用System.identityHashCode(a),返回的结果都是一样的,而重写hashCode()的B类在调用对象本身的hashCode()方法时返回的都是0,只有调用System.identityHashCode(...)才会返回对象默认的哈希码。
下面我们来看看ConfigurationClassPostProcessor.processConfigBeanDefinitions(...)完成的工作,既然这个方法要扫描类路径,那一定少不了和配置类打交道,读取配置类上用@ComponentScan注解配置的类路径。这里我们在回顾回顾spring是怎样将配置类注册到容器的,我们在一个类上标记@Configuration、@ComponentScan注解,类似下面的MyConfi5,再将MyConfig5.class这个类对象作为构造参数传给注解应用上下文(AnnotationConfigApplicationContext)构造应用上下文对象,在应用上下文的构造函数中会初始化一个AnnotatedBeanDefinitionReader对象,在构造AnnotatedBeanDefinitionReader对象时会调用到AnnotationConfigUtils.registerAnnotationConfigProcessors(...)静态方法,在这个方法会向spring容器注册一些基础组件的BeanDefinition,之后AnnotatedBeanDefinitionReader对象会将配置类MyConfig5的class对象作为AnnotatedGenericBeanDefinition构造函数的参数创建一个BeanDefinition的实例并注册到spring容器。因此在执行spring容器ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(...)方法之前,spring容器已经包含了配置类和基础组件的BeanDefinition。
@Configuration @ComponentScan("org.example.service") public class MyConfig5 { public static void main(String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig5.class); } }
当执行ConfigurationClassPostProcessor.processConfigBeanDefinitions(...)方法的时候,会先从代码<1>处获取容器现有的所有beanName,遍历这些beanName在<2>处过滤出配置类对应的BeanDefinition,将配置类对应的BeanDefinition加入到configCandidates列表,后续会从这些BeanDefinition解析出配置类要求扫描的类路径。如果遍历所有的BeanDefinition都没有找到配置类,则configCandidates列表为空,当执行到<3>处就会退出processConfigBeanDefinitions(...)方法。
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) { 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)) {//<2> configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); } } // Return immediately if no @Configuration classes were found if (configCandidates.isEmpty()) {//<3> return; } …… } …… }
那么我们来思考下,什么样的BeanDefinition能让ConfigurationClassUtils.checkConfigurationClassCandidate(...)方法返回true,将其加入到configCandidates列表呢?首先我们要明白BeanDefinition存在的目的,在spring容器中BeanDefinition存在的目的就是为了描述一个bean对象,spring可以从BeanDefinition得知如何构造一个bean对象?这个bean对象是单例还是原型?这个bean对象是否是懒加载……等等。就笔者看来,BeanDefinition和bean之间的关系有点类似于类和实例的关系,只是BeanDefinition是spring对Java原生的class做了扩展。
在前面学习BeanDefinition章节的时候我们知道,一个BeanDefinition可以不包含class对象,比如下面的abPerson和xiaomi对应的BeanDefinition,spring并不会针对abPerson去构造一个bean对象,abPerson存在的目的仅仅是包装属性让其他BeanDefinition来继承自己。xiaomi对应的BeanDefinition不需要也不会有class对象,因为spring可以通过调用tvFactory这个bean的工厂方法来创建xiaomi的bean对象。所以如果一个BeanDefinition连class对象都没有,那可以肯定这个BeanDefinition一定不是配置类的BeanDefinition,也就没必要加到configCandidates列表。
<bean id="abPerson" abstract="true" scope="prototype"> <property name="age" value="18"></property> </bean> <bean id="sam" class="org.example.beans.Person" parent="abPerson"> <property name="name" value="Sam"></property> </bean> <bean id="tvFactory" class="org.example.beans.TVFactory"></bean> <bean id="xiaomi" factory-bean="tvFactory" factory-method="createMi"> </bean>
那么,如果一个BeanDefinition有class对象,就能保证它是配置类吗?也不一定。比如像下面的代码,当把MyConfig6注册进应用上下文后,MyConfig6在spring容器一定会有一个与之对应的BeanDefinition,同时aBean也会存在一个BeanDefinition,这两个BeanDefinition的class对象都是MyConfig6,但这并不意味着aBean的BeanDefinition就是配置类的BeanDefinition。如果判定一个BeanDefinition有class对象就是配置类,那么myConfig6和aBean两个BeanDefinition都会加到configCandidates列表,在后续扫描配置类的类路径时,org.example.service路径下的类就会被扫描两次,所以如果一个BeanDefinition的factoryMethodName属性不为null,这个BeanDefinition一定不是配置类的BeanDefinition。
@Configuration(proxyBeanMethods = false) @ComponentScan("org.example.service") @ImportResource("spring.xml") public class MyConfig6 { public static A getA() { return new A(); } }
spring.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="aBean" class="org.example.config.MyConfig6" factory-method="getA"></bean> </beans>
测试用例:
@Test public void test11() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig6.class); BeanDefinition myConfig5 = ac.getBeanDefinition("myConfig6"); BeanDefinition getA = ac.getBeanDefinition("aBean"); System.out.println("myConfig6 class:" + myConfig5.getBeanClassName()); System.out.println("aBean class:" + getA.getBeanClassName()); System.out.println("aBean factoryMethodName:" + getA.getFactoryMethodName()); }
运行结果:
myConfig6 class:org.example.config.MyConfig6 aBean class:org.example.config.MyConfig6 aBean factoryMethodName:getA
在判断BeanDefinition包含class对象,且没有工厂方法后,我们还可以接着根据一些基本条件过滤不是配置类的BeanDefinition,比如应用上下文在初始化的时候同样会初始化一个AnnotatedBeanDefinitionReader对象,在初始化AnnotatedBeanDefinitionReader对象时会往spring容器注册一些基础组件的BeanDefinition,这些基础组件或多或少都实现了spring设计的接口,比如用于扫描类路径的ConfigurationClassPostProcessor类就实现了BeanFactoryPostProcessor接口、处理@Autowired和@Inject注解的AutowiredAnnotationBeanPostProcessor类实现了BeanPostProcessor接口、处理@Resource注解的CommonAnnotationBeanPostProcessor类同样实现了BeanPostProcessor接口。所以我们可以规定一些接口,比如:BeanFactoryPostProcessor、BeanPostProcessor……等等,如果一个类实现了这些接口,那么这个类可能不是一个配置类。
下面我们来看看ConfigurationClassUtils.checkConfigurationClassCandidate(...)方法是如何判定一个BeanDefinition是配置类的BeanDefinition。在这个方法执行之初的<1>处,也是先判定传入的BeanDefinition包含一个class对象,且这个BeanDefinition并不是根据工厂函数来创建bean的,表示这个BeanDefinition有可能是一个配置类的BeanDefinition。
之后会根据BeanDefinition获取元数据,如果传进来的BeanDefinition是配置类,则这个BeanDefinition的真实类型为AnnotatedGenericBeanDefinition,这个类是AnnotatedBeanDefinition的子类,会进入<2>处的分支。如果传进来的BeanDefinition是spring内置的基础组件,比如:ConfigurationClassPostProcessor、AutowiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor……等等,其BeanDefinition的真实类型为RootBeanDefinition,这个类是AbstractBeanDefinition的子类,会进入<3>处的分支,在分支<3>内再次判断是否实现了spring设计的BeanFactoryPostProcessor、BeanPostProcessor、AopInfrastructureBean和EventListenerFactory这几个接口,如果是的话则判断这个BeanDefinition不是一个配置类。如果传进来的BeanDefinition既不是AnnotatedBeanDefinition的子类,也不是AbstractBeanDefinition的子类,则会进入<4>处的分支尝试获取元数据对象,这里可能获取失败,如果失败的话则会抛出异常,告诉外部这个BeanDefinition不是配置类的BeanDefinition。
在获取到元数据后,会通过元数据会判断BeanDefinition对应的类上是否有@Configuration注解,如果配置类上有@Configuration注解,则config不为null。spring对配置类分为两种模式:即<7>处的full和<8>处的lite模式。如果配置类上有@Configuration注解,且注解的proxyBeanMethods属性为true,则会进入分支<5>,在元数据上设置CONFIGURATION_CLASS_ATTRIBUTE(org.springframework.context.annotation.ConfigurationClassPostProcessor.configurationClass)属性为full,表示这个配置类是full模式的,@Configuration的proxyBeanMethods属性默认为true,所以如果我们不额外设置@Configuration的属性,配置类通常都是full模式的,比如前面MyConfig5就是一个full模式的配置类,至于设置proxyBeanMethods属性有什么效果笔者会在后面讲解。如果配置类上有配置@Configuration注解,但proxyBeanMethods属性为false,则会进入<6>处的分支。或者配置类上根本没有@Configuration注解,config为null,但isConfigurationCandidate(metadata)返回true,同样会进入<6>处的分支,在<6>处的分支内会在元数据上设置CONFIGURATION_CLASS_ATTRIBUTE为lite,表示这个配置类是lite模式的。
那么这个元数据必须满足什么条件才能让isConfigurationCandidate(metadata)返回true?在isConfigurationCandidate(metadata)方法内会先在<9>处判断元数据对应的类是否是一个接口,如果是接口则代表这个类不能成为配置类。之后会在<10>处判断配置类上有@Component、@ComponentScan、@Import、@ImportResource这四个注解,如果有这几个注解就可以成为配置类。如果配置类上都没这几个注解,会在<11>处判断这个类的方法是否有@Bean注解。
总结一下<5>和<6>的判断,如果一个传给应用上下文的类上有@Configuration,那么这个类一定是个配置类,根据proxyBeanMethods属性判断配置类是full模式还是lite模式,如果proxyBeanMethods为true则是full模式的配置类,为false则是lite模式的配置类,默认proxyBeanMethods为true。如果类上没有@Configuration,会接着判断这个类是否有@Component、@ComponentScan、@Import、@ImportResource这四个注解,有的话则是lite模式的配置类,如果没有这四个注解,会再判断这个类的方法上是否有@Bean注解,有的话则是lite模式的配置类,没有的话这个类就不是一个配置类。
再判断一个类不是配置类后会进入<9>处的分支返回,如果一个类是配置类,会在<12>处尝试获取配置类的权重,spring提供了@Order注解来指定配置类的解析顺序,我们可以在配置类上加上@Order注解并指定一个数字作为权重,如果不指定数字的话则会使用默认权重Integer.MAX_VALUE,代码<12>处如果获取到的权重不为null,则会在<13>处将权重存放到元数据内。当spring收集完容器中所有的配置类后会根据权重对这些配置类做一个排序,权重越小的配置类越优先解析。
abstract class ConfigurationClassUtils { public static final String CONFIGURATION_CLASS_FULL = "full";//<7> public static final String CONFIGURATION_CLASS_LITE = "lite";//<8> public static final String CONFIGURATION_CLASS_ATTRIBUTE = Conventions.getQualifiedAttributeName(ConfigurationClassPostProcessor.class, "configurationClass"); private static final String ORDER_ATTRIBUTE = Conventions.getQualifiedAttributeName(ConfigurationClassPostProcessor.class, "order"); private static final Log logger = LogFactory.getLog(ConfigurationClassUtils.class); 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 checkConfigurationClassCandidate( BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) { String className = beanDef.getBeanClassName(); if (className == null || beanDef.getFactoryMethodName() != null) {//<1> return false; } AnnotationMetadata metadata; if (beanDef instanceof AnnotatedBeanDefinition && className.equals(((AnnotatedBeanDefinition) beanDef).getMetadata().getClassName())) {//<2> // Can reuse the pre-parsed metadata from the given BeanDefinition... metadata = ((AnnotatedBeanDefinition) beanDef).getMetadata(); } else if (beanDef instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) beanDef).hasBeanClass()) {//<3> // Check already loaded Class if present... // since we possibly can‘t even load the class file for this Class. Class<?> beanClass = ((AbstractBeanDefinition) beanDef).getBeanClass(); if (BeanFactoryPostProcessor.class.isAssignableFrom(beanClass) || BeanPostProcessor.class.isAssignableFrom(beanClass) || AopInfrastructureBean.class.isAssignableFrom(beanClass) || EventListenerFactory.class.isAssignableFrom(beanClass)) { return false; } metadata = AnnotationMetadata.introspect(beanClass); } else {//<4> try { MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(className); metadata = metadataReader.getAnnotationMetadata(); } catch (IOException ex) { if (logger.isDebugEnabled()) { logger.debug("Could not find class file for introspecting configuration annotations: " + className, ex); } return false; } } Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName()); if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {//<5> beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL); } else if (config != null || isConfigurationCandidate(metadata)) {//<6> beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE); } else {//<9> return false; } // It‘s a full or lite configuration candidate... Let‘s determine the order value, if any. Integer order = getOrder(metadata);//<12> if (order != null) { beanDef.setAttribute(ORDER_ATTRIBUTE, order);//<13> } return true; } public static boolean isConfigurationCandidate(AnnotationMetadata metadata) { // Do not consider an interface or an annotation... if (metadata.isInterface()) {//<9> return false; } // Any of the typical annotations found? for (String indicator : candidateIndicators) {//<10> if (metadata.isAnnotated(indicator)) { return true; } } // Finally, let‘s look for @Bean methods... try { return metadata.hasAnnotatedMethods(Bean.class.getName());//<11> } catch (Throwable ex) { if (logger.isDebugEnabled()) { logger.debug("Failed to introspect @Bean methods on class [" + metadata.getClassName() + "]: " + ex); } return false; } } public static Integer getOrder(AnnotationMetadata metadata) { Map<String, Object> orderAttributes = metadata.getAnnotationAttributes(Order.class.getName()); return (orderAttributes != null ? ((Integer) orderAttributes.get(AnnotationUtils.VALUE)) : null); } public static int getOrder(BeanDefinition beanDef) { Integer order = (Integer) beanDef.getAttribute(ORDER_ATTRIBUTE); return (order != null ? order : Ordered.LOWEST_PRECEDENCE); } }
我们已经知道配置类分两种模式:full和lite,那么这两种模式的区别是什么呢?我们来看下面的MyConfig7和MyConfig8,根据我们现在对spring的了解,可以知道MyConfig7是full模式的配置类,MyConfig8是lite模式的配置类。这两个配置类都有一个用@Bean注解标记的方法,我们在测试用例获取这两个配置类的bean,打印这两个bean的class对象,再调用这两个bean里唯一的方法,看看会有什么效果。
@Configuration public class MyConfig7 { @Bean public A getA() { return new A(); } } @Configuration(proxyBeanMethods = false) public class MyConfig8 { @Bean public B getB() { return new B(); } }
测试用例:
@Test public void test12() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig7.class, MyConfig8.class); MyConfig7 myConfig7 = ac.getBean(MyConfig7.class); System.out.println("myConfig7 class:" + myConfig7.getClass()); System.out.println("a bean:" + myConfig7.getA()); System.out.println("a bean:" + myConfig7.getA()); MyConfig8 myConfig8 = ac.getBean(MyConfig8.class); System.out.println("myConfig8 class:" + myConfig8.getClass()); System.out.println("b bean:" + myConfig8.getB()); System.out.println("b bean:" + myConfig8.getB()); }
运行结果:
myConfig7 class:class org.example.config.MyConfig7$$EnhancerBySpringCGLIB$$e192874b a bean:org.example.pojo.A@5fbdfdcf a bean:org.example.pojo.A@5fbdfdcf myConfig8 class:class org.example.config.MyConfig8 b bean:org.example.pojo.B@4efc180e b bean:org.example.pojo.B@bd4dc25
从执行结果可以看到,MyConfig7的类对象是一个很奇怪的类对象,同时重复调用MyConfig7的getA()方法获取到的都是同一个对象,这很不符合常理,为什么从spring容器获取MyConfig7的bean对象其对应的类型不是org.example.config.MyConfig7,且重复调用getA()方法应该创建两次A类的实例返回。而proxyBeanMethods属性为false的MyConfig8则符合常理,打印MyConfig8的bean对象其对应的类型是org.example.config.MyConfig8,调用两次getB()也是创建两个不同的B类实例。
MyConfig7之所以与MyConfig8有这样的差别,是因为spring在发现MyConfig7是一个full模式的配置类,会用cglib动态代理技术创建一个class对象作为MyConfig7的子类,并将配置类BeanDefinition的class对象替换成子类的class对象。当spring容器要创建配置类对应的bean对象时,full模式的配置类实际上创建的是代理父类的子类实例。在调用配置类@Bean方法时,这个方法会被子类代理,子类会先检查这个方法产生的bean对象是否已经在spring容器中,如果已经存在于容器中则直接返回容器内的bean对象,否则调用父类的方法构造bean对象,将其存放在容器内后再返回。