Spring原始碼解析之ConfigurationClassPostProcessor(一)

北洛發表於2021-08-18

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物件,將其存放在容器內後再返回。

 

相關文章