1. 程式人生 > >Spring原始碼研究之TypeFilter

Spring原始碼研究之TypeFilter

藉助於Spring提供的擴充套件TypeFilter來完成在程式啟動時按需載入同一個介面的不同實現類。

1. 概述

本文的背景是所在公司的業務是面向全國的,因此一直以來研發人員被“各個區域,某個模組功能細節的需求有著些許不同”的難題所困擾,而在此之前一直採用的是 邏輯程式碼裡直接 if / else if 下去,這種簡單粗暴到讓人極度反感的實現方式本人就不多評價了, 最終決定是使用二十三種設計模式之中的橋接模式來緩解這種情況。

在上面的解決方案中涉及到的一點是如何做到最大的靈活性——根據指定的區域載入相應的實現類? 因為這裡面還涉及到依賴的管理等等問題,所以最終放棄手動配置依賴的方法,轉而使用Spring來幫助我們完成這項工作,而 ”按區域載入相應的區域實現類 “ 的需求也順理成章地考慮使用Spring內建提供的擴充套件來完成——也就是本文標題裡的TypeFilter

2. 思路

藉助設計模式裡的橋接模式,將涉及到區域差異性的模組功能單獨抽取到代表區域功能的IArea中; 客戶端的呼叫將直接針對IArea以及其子介面,而在專門的area package則根據實際需求來實現各自區域特有的功能。接下來就讓我們看看如何通過TypeFilter來實現選擇性載入相應區域的實現邏輯。

其實在一年前寫的 Spring原始碼研究之註解掃描context:component-scan/ 對該介面就有粗淺的介紹,但之後因為一直都是簡單的應用,所以沒有作更加細緻的研究。藉著這次機會,對其注意事項作出一些補充和完善。

3. 實現

首先宣告一個註解。

/**
 * <p> 目標是根據配置, 將符合條件的區域實現類從classpath全部掃進來.
 * <p> 我們就不在其上註解 @Component 來越皰代俎。
 * @author LQ
 *
 */
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Areaed { /** * 所屬區域 * @return */ String value(); /** * 描述性資訊 * @return */ String description() default ""; }

然後宣告介面IArea

public interface IArea {
	void say();
}

@Areaed("nx")
@Component("area"
) public class AreaNx implements IArea { @Override public void say() { System.out.println("寧夏"); } } @Areaed("wh") @Component("area") public class AreaWh implements IArea { @Override public void say() { System.out.println("武漢"); } }

最後是本次的主角——TypeFilter的實現類AreaSpringExcludeTypeFilter

public final class AreaSpringExcludeTypeFilter extends AbstractTypeHierarchyTraversingFilter {

	private PathMatcher pathMatcher;
	
	// 注意下面這種方式是獲取不到配置值的, 具體原因下面解釋
	//@Value("${common.area.name}")
	private String configedAreaName;

	public AreaSpringExcludeTypeFilter() {
		// 不考慮基類, 也不考慮介面上的資訊
		super(false, false);
		

		// 藉助Spring預設的 Resource萬用字元路徑 方式
		pathMatcher = new AntPathMatcher();
		
		// 硬編碼讀取配置資訊
		try {
			Properties loadAllProperties = PropertiesLoaderUtils.loadAllProperties("area.properties");
			configedAreaName = loadAllProperties.getProperty("common.area.name");
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	// 注意本類將註冊為Exclude, 返回true代表拒絕
	@Override
	protected boolean matchClassName(String className) {
		if (!isPotentialPackageClass(className)) {
			return false;
		}

		// 判斷當前區域是否和所配置的區域一致, 不一致則阻止載入Spring容器
		Class<?> clazz = ClassUtil.loadClass(className, false);
		Areaed areaedAnnotation = clazz.getAnnotation(Areaed.class);
		if(null == areaedAnnotation){
			return false;
		}		
		final String thisAreaName = areaedAnnotation.value();
		return (!configedAreaName.equalsIgnoreCase(thisAreaName));
	}

	// 潛在的滿足條件的類的類名, 指定package下
	private static final String PATTERN_STANDARD = ClassUtils
			.convertClassNameToResourcePath("spring.theory.typefilter.*");

	// 本類邏輯中可以處理的類 -- 指定package下的才會進行邏輯判斷,
	private boolean isPotentialPackageClass(String className) {	
		// 將類名轉換為資源路徑, 以進行匹配測試
		final String path = ClassUtils.convertClassNameToResourcePath(className);
		return pathMatcher.match(PATTERN_STANDARD, path);
	}
}

相關的配置

<context:property-placeholder location="classpath:area.properties"/>

<context:component-scan base-package="spring.theory.typefilter">
	<context:exclude-filter type="custom"   expression="spring.theory.typefilter.AreaSpringExcludeTypeFilter"/>
</context:component-scan>

測試程式碼

@Test
public void call_SpEL_IN_java() throws Exception {
		URL resource = ResourceUtil.getResource("spring_typefilter.xml", Tester.class);
		final String file = resource.getFile();		
		BeanFactory bf = new FileSystemXmlApplicationContext(file.substring(1));
		// area.properties : common.area.name=wh
		// 輸出 : 武漢 
		bf.getBean("area",IArea.class).say();
}

4. 相關原始碼

正如 Spring原始碼研究之註解掃描context:component-scan/ 提及的 ComponentScanBeanDefinitionParser.parseTypeFilters()方法中,將對使用者配置的TypeFilter進行挨個解析,並填充進ClassPathBeanDefinitionScanner型別的scanner例項欄位中,以便接下來的scanner.doScan(basePackages);中進行過濾篩選操作。

這裡最需要注意的是在例項化TypeFilter的過程中,是直接使用反射的方式完成的,並沒有進行相關依賴的注入,所以上面的AreaSpringExcludeTypeFilter實現中,配置屬性是進行的硬編碼讀取,這一點是需要特別注意的。究其根本原因,還是因為Spring的生命週期裡,負責填充屬性值的InstantiationAwareBeanPostProcessorTypeFilter的例項化過程壓根搭不上邊。

// ComponentScanBeanDefinitionParser類中
protected TypeFilter createTypeFilter(Element element, ClassLoader classLoader, ParserContext parserContext) {
	String filterType = element.getAttribute(FILTER_TYPE_ATTRIBUTE);
	String expression = element.getAttribute(FILTER_EXPRESSION_ATTRIBUTE);
	expression = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(expression);
	try {
		if ("annotation".equals(filterType)) {
			return new AnnotationTypeFilter((Class<Annotation>) classLoader.loadClass(expression));
		}
		else if ("assignable".equals(filterType)) {
			return new AssignableTypeFilter(classLoader.loadClass(expression));
		}
		else if ("aspectj".equals(filterType)) {
			return new AspectJTypeFilter(expression, classLoader);
		}
		else if ("regex".equals(filterType)) {
			return new RegexPatternTypeFilter(Pattern.compile(expression));
		}
		else if ("custom".equals(filterType)) {
			Class<?> filterClass = classLoader.loadClass(expression);
			if (!TypeFilter.class.isAssignableFrom(filterClass)) {
				throw new IllegalArgumentException(
						"Class is not assignable to [" + TypeFilter.class.getName() + "]: " + expression);
			}
			// 直接反射實現, 相關類必須有預設建構函式。
			return (TypeFilter) BeanUtils.instantiateClass(filterClass);
		}
		else {
			throw new IllegalArgumentException("Unsupported filter type: " + filterType);
		}
	}
	catch (ClassNotFoundException ex) {
		throw new FatalBeanException("Type filter class not found: " + expression, ex);
	}
}

另外一個和TypeFilter功能類似的就是註解@Conditional了。針對這個在SpringBoot的autoConfig中大放異彩的註解,在TypeFilter的執行邏輯過程中也是有其身影的;注意以下程式碼

// ClassPathScanningCandidateComponentProvider
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
	// exclude  TypeFilter 滿足直接返回false
	for (TypeFilter tf : this.excludeFilters) {
		if (tf.match(metadataReader, this.metadataReaderFactory)) {
			return false;
		}
	}
	
	// include TypeFilter 滿足後, 還得再進行一次判斷
	for (TypeFilter tf : this.includeFilters) {
		if (tf.match(metadataReader, this.metadataReaderFactory)) {
			// 即使通過了includeFilters, 還會再去判斷一次 Conditional 註解
			// 只有在被@Conditional註解時, 才會進行更詳細的判斷, 否則直接返回true
			// Condition的例項化也是直接反射,  ConditionEvaluator.getCondition()中
			return isConditionMatch(metadataReader);
		}
	}
	return false;
}

上面的註釋已經解釋得很清楚了, 在通過Inculde測試後,Spring會再進行一次@Conditional註解相關的判斷,這才決定該例項是否真正地被注入Spring容器。

5. 總結

Spring或者說IOC真是個好東西,本次需求中,對於各個模組所共有的,特有的依賴,如果我們手動配置的話,這將是一場毫無勝算的戰爭,開發人員將被拖入深深的泥沼裡,越陷越深。

6. Links