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對象,將其存放在容器內后再返回。