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的生命週期裡,負責填充屬性值的InstantiationAwareBeanPostProcessor
與TypeFilter
的例項化過程壓根搭不上邊。
// 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真是個好東西,本次需求中,對於各個模組所共有的,特有的依賴,如果我們手動配置的話,這將是一場毫無勝算的戰爭,開發人員將被拖入深深的泥沼裡,越陷越深。