摘要: 本文結合《Spring源碼深度解析》來分析Spring 5.0.6版本的源代碼。若有描述錯誤之處,歡迎指正。
目錄
一、processPropertyPlaceHolders屬性的處理
二、根據配置屬性生成過濾器
三、掃描Java文件
我們在applicationContext.xml中配置了userMapper供需要時使用。但如果需要用到的映射器較多的話,采用這種配置方式就顯得很低效。為了解決這個問題,我們可以使用MapperScannerConfigurer,讓它掃描特定的包,自動幫我們成批地創建映射器。這樣一來,就能大大減少配置的工作量,比如我們將applicationContext.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="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close" lazy-init="true"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <!-- 配置連接池初始化大小、最小、最大 --> <property name="initialSize" value="${jdbc.initialSize}"/> <property name="minIdle" value="${jdbc.minIdle}"/> <property name="maxActive" value="${jdbc.maxActive}"/> <!-- 配置獲取連接等待超時的時間,單位是毫秒 --> <property name="maxWait" value="${jdbc.maxWait}"/> <!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}"/> <!-- 配置一個連接在池中最小生存的時間,單位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}"/> <property name="validationQuery" value="select 1"/> <property name="testWhileIdle" value="${jdbc.testWhileIdle}"/> <property name="testOnBorrow" value="${jdbc.testOnBorrow}"/> <property name="testOnReturn" value="${jdbc.testOnReturn}"/> <!-- 打開PSCache,並且指定每個連接上PSCache的大小 --> <property name="poolPreparedStatements" value="${jdbc.poolPreparedStatements}"/> <property name="maxPoolPreparedStatementPerConnectionSize" value="${jdbc.maxPoolPreparedStatementPerConnectionSize}"/> <!-- 統計sql filter --> <property name="proxyFilters"> <list> <bean class="com.alibaba.druid.filter.stat.StatFilter"> <property name="mergeSql" value="true"/> <property name="slowSqlMillis" value="${jdbc.slowSqlMillis}"/> <property name="logSlowSql" value="true"/> </bean> </list> </property> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="org.cellphone.uc.repo.mapper"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean> </beans>
在上面的配置中,我們屏蔽掉了最原始的代碼(userMapper的創建)而增加了MapperScannerConfigurer的配置,basePackage屬性是讓你為映射器接口文件設置基本的包路徑。你可以使用分號或逗號作為分隔符設置多於一個的包路徑。每個映射器將會在指定的包路徑中遞歸地被搜索到。被發現的映射器將會使用Spring對自動偵測組件默認的命名策略來命名。也就是說,如果沒有發現注解,它就會使用映射器的非大寫的非完全限定類名。但是如果發現了@Component 或 JSR-330@Named 注解,它會獲取名稱。
通過上面的配罝,Spring就會幫助我們對org.cellphone.uc.repo.mapper下面的所有接口進行自動的注入,而不需要為每個接口重復在Spring配置文件中進行聲明了。那么,這個功能又是如何做到的呢? MapperScannerConfigurer 中又有哪些核心操作呢?同樣,首先査看類的層次結構圖,如下圖所示。
我們又看到了令人感興趣的接口InitializingBean,馬上査找類的afterPropertiesSet方法來看看類的初始化邏輯。
@Override public void afterPropertiesSet() throws Exception { notNull(this.basePackage, "Property 'basePackage' is required"); }
很遺憾,分析並沒有像我們之前那樣順利,afterPropertiesSet()方法除了一句對basePackage屬性的驗證代碼外並沒有太多的邏輯實現。好吧,讓我們回過頭再次査看MapperScannerConfigurer類層次結構圖中感興趣的接口。於是,我們發現了BeanDefinitionRegistryPostProcessor與BeanFactoryPostProcessor,Spring在初始化的過程中同樣會保證這兩個接口的調用。
首先査看 MapperScannerConflgurer 類中對於 BeanFactoryPostProcessor 接口的實現:
@Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { // left intentionally blank }
沒有任何邏輯實現,只能說明我們找錯地方了,繼續找,査看MapperScannerConfigurer 類中對於 BeanDefinitionRegistryPostProcessor 接口的實現。
@Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { if (this.processPropertyPlaceHolders) { processPropertyPlaceHolders(); } ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); scanner.setAddToConfig(this.addToConfig); scanner.setAnnotationClass(this.annotationClass); scanner.setMarkerInterface(this.markerInterface); scanner.setSqlSessionFactory(this.sqlSessionFactory); scanner.setSqlSessionTemplate(this.sqlSessionTemplate); scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName); scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName); scanner.setResourceLoader(this.applicationContext); scanner.setBeanNameGenerator(this.nameGenerator); scanner.registerFilters(); scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); }
Bingo!這次找對地方了。大致看一下代碼實現,正是完成了對指定路徑掃描的邏輯。那么,我們就以此為入口,詳細地分析MapperScannerConfigurer 所提供的邏輯實現。
一、processPropertyPlaceHolders屬性的處理
首先,難題就是processPropertyPlaceHolders屬性的處理。或許讀者並未過多接觸此屬性,我們只能查看processPropertyPlaceHolders()函數來反推此屬性所代表的功能。
/* * BeanDefinitionRegistries are called early in application startup, before * BeanFactoryPostProcessors. This means that PropertyResourceConfigurers will not have been * loaded and any property substitution of this class' properties will fail. To avoid this, find * any PropertyResourceConfigurers defined in the context and run them on this class' bean * definition. Then update the values. */ private void processPropertyPlaceHolders() { Map<String, PropertyResourceConfigurer> prcs = applicationContext.getBeansOfType(PropertyResourceConfigurer.class); if (!prcs.isEmpty() && applicationContext instanceof ConfigurableApplicationContext) { BeanDefinition mapperScannerBean = ((ConfigurableApplicationContext) applicationContext) .getBeanFactory().getBeanDefinition(beanName); // PropertyResourceConfigurer does not expose any methods to explicitly perform // property placeholder substitution. Instead, create a BeanFactory that just // contains this mapper scanner and post process the factory. DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); factory.registerBeanDefinition(beanName, mapperScannerBean); for (PropertyResourceConfigurer prc : prcs.values()) { prc.postProcessBeanFactory(factory); } PropertyValues values = mapperScannerBean.getPropertyValues(); this.basePackage = updatePropertyValue("basePackage", values); this.sqlSessionFactoryBeanName = updatePropertyValue("sqlSessionFactoryBeanName", values); this.sqlSessionTemplateBeanName = updatePropertyValue("sqlSessionTemplateBeanName", values); } }
不知讀者是否悟出了此函數的作用呢?或許此函數的說明會給我們一些提示:BeanDefinitionRegistries會在應用啟動的時候調用,並且會早於BeanFactoryPostProcessors的調用,這就意味着PropertyResourceConfigurers還沒有被加載所有對於屬性文件的引用將會失效。為避免此種情況發生,此方法手動地找出定義的PropertyResourceConfigurers並進行提前調用以保證對於屬性的引用可以正常工作。
我想讀者已經有所感悟,結合之前講過的PropertyResourceConfigurer的用法,舉例說明一 下,如要創建配置文件如test.properties,並添加屬性對:
basePackage = org.cellphone.uc.repo.mapper
然后在Spring配置文件中加入屬性文件解析器:
<bean id="mesHandler" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>config/test.properties</value> </list> </property> </bean>
修改 MapperScannerConfigurer 類型的 bean 的定義:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="${basePackage}"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean>
此時你會發現,這個配置並沒有達到預期的效果,因為在解析${basePackage}的時候 PropertyPlaceholderConfigurer還沒有被調用,也就是屬性文件中的屬性還沒有加載至內存中,Spring還不能直接使用它。為了解決這個問題,Spring提供了processPropertyPlaceHolders屬性,你需要這樣配置MapperScannerConfigurer類型的 bean。
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="${basePackage}"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> <property name="processPropertyPlaceHolders" value="true"/> </bean>
通過processPropertyPlaceHolders屬性的配置,將程序引入我們正在分析的processPropertyPlaceHolders函數中來完成屬性文件的加載。至此,我們終於理清了這個屬性的作用,再次回顧這個函數所做的事情。
(1)找到所有已經注冊的PropertyResourceConflgurer類型的bean。
(2)模擬Spring中的環境來用處理器。這里通過使用new DefaultListableBeanFactory()來 模擬Spring中的環境(完成處理器的調用后便失效),將映射的bean,也就是MapperScannerConfigurer類型bean注冊到環境中來進行后理器的調用,處理器PropertyPlaceholderConfigurer調用完成的功能,即找出所有bean中應用屬性文件的變量並替換。也就是說,在處理器調用后,模擬環境中模擬的MapperScannerConfigurer類型的bean如果有引入屬性文件中的屬性那么已經被替換了,這時,再將模擬bean中相關的屬性提取出來應用在真實的bean中。
二、根據配置屬性生成過濾器
在postProcessBeanDefinitionRegistry方法中可以看到,配置中支持很多屬性的設定,但是我們感興趣的或者說影響掃描結果的並不多,屬性設置后通過在scanner.registerFilters()代碼中生成對應的過濾器來控制掃描結果。
/** * Configures parent scanner to search for the right interfaces. It can search * for all interfaces or just for those that extends a markerInterface or/and * those annotated with the annotationClass */ public void registerFilters() { boolean acceptAllInterfaces = true; // if specified, use the given annotation and / or marker interface // 對於annotationClass屬性的處理 if (this.annotationClass != null) { addIncludeFilter(new AnnotationTypeFilter(this.annotationClass)); acceptAllInterfaces = false; } // override AssignableTypeFilter to ignore matches on the actual marker interface // 對於markerInterface屬性的處理 if (this.markerInterface != null) { addIncludeFilter(new AssignableTypeFilter(this.markerInterface) { @Override protected boolean matchClassName(String className) { return false; } }); acceptAllInterfaces = false; } if (acceptAllInterfaces) { // default include filter that accepts all classes addIncludeFilter((metadataReader, metadataReaderFactory) -> true); } // exclude package-info.java // 不掃描package-info.java文件 addExcludeFilter((metadataReader, metadataReaderFactory) -> { String className = metadataReader.getClassMetadata().getClassName(); return className.endsWith("package-info"); }); }
代碼中得知,根據之前屬性的配置生成了對應的過濾器。
(1)annotationClass 屬性處理。
如果annotationClass不為空,表示用戶設置了此屬性,那么就要根據此屬性生成過濾器以保證達到用戶想要的效果,而封裝此屬性的過濾器就是AnnotationTypeFilter。AnnotationTypeFilter保證在掃描對應Java文件時只接受標記有注解為annotationClass的接口。
(2)markerlnterface 屬性處理。
如果markerlnterface不為空,表示用戶設置了此屬性,那么就要根據此屬性生成過濾器以保證達到用戶想要的效果,而封裝此屬性的過濾器就是實現AssignableTypeFilter接口的局部類。表示掃描過程中只有實現markerlnterface接口的接口才會被接受。
(3)全局默認處理。
在上面兩個屬性中如果存在其中任何屬性,acceptAllInterfaces的值將會改變,但是如果用戶沒有設定以上兩個屬性,那么,Spring會為我們增加一個默認的過濾器實現TypeFilter接口的局部類,旨在接受所有接口文件。
(4)package-info.java 處理。
對於命名為package-info的Java文件,默認不作為邏輯實現接口,將其排除掉,使用TypeFilter接口的局部類實現match方法。
從上面的函數我們看出,控制掃描文件Spring通過不同的過濾器完成,這些定義的過濾器記錄在了includeFilters和excludeFilters屬性中。
/** * Add an include type filter to the <i>end</i> of the inclusion list. */ public void addIncludeFilter(TypeFilter includeFilter) { this.includeFilters.add(includeFilter); } /** * Add an exclude type filter to the <i>front</i> of the exclusion list. */ public void addExcludeFilter(TypeFilter excludeFilter) { this.excludeFilters.add(0, excludeFilter); }
至於過濾器為什么會在掃描過程中起作用,我們在講解掃描實現時候再繼續深人研究。
三、掃描Java文件
設置了相關屬性以及生成了對應的過濾器后便可以進行文件的掃描了,掃描工作是由ClassPathMapperScanner類型的實例 scanner 中的 scan 方法完成的。
/** * Perform a scan within the specified base packages. * @param basePackages the packages to check for annotated classes * @return number of beans registered */ public int scan(String... basePackages) { int beanCountAtScanStart = this.registry.getBeanDefinitionCount(); doScan(basePackages); // Register annotation config processors, if necessary. // 如果配置了includeAnnotationConfig, 則注冊對應注解的處理器以保證注解功能的正常使用 if (this.includeAnnotationConfig) { AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); } return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart); }
scan是個全局方法,掃描工作通過doScan(basePackages)委托給了 doScan方法,同時,還包括了 includeAnnotationConfig 屬性的處理,AnnotationConfigUtils.registerAnnotationConfigProcessors (this.registry)代碼主要是完成對於注解處理器的簡單注冊,比如AutowiredAnnotationBeanPostProcessor、RequiredAnnotationBeanPostProcessor等,這里不再贊述,我們重點研究文件掃描功能的實現。
/** * Calls the parent search that will search and register all the candidates. * Then the registered objects are post processed to set them as * MapperFactoryBeans */ @Override public Set<BeanDefinitionHolder> doScan(String... basePackages) { Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages); if (beanDefinitions.isEmpty()) { // 如果沒有掃描到任何文件發出警告 LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration."); } else { processBeanDefinitions(beanDefinitions); } return beanDefinitions; } private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { GenericBeanDefinition definition; for (BeanDefinitionHolder holder : beanDefinitions) { definition = (GenericBeanDefinition) holder.getBeanDefinition(); String beanClassName = definition.getBeanClassName(); LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName + "' mapperInterface"); // the mapper interface is the original class of the bean // but, the actual class of the bean is MapperFactoryBean // 開始構造MapperFactoryBean類型的bean definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59 definition.setBeanClass(this.mapperFactoryBean.getClass()); definition.getPropertyValues().add("addToConfig", this.addToConfig); boolean explicitFactoryUsed = false; if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) { definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName)); explicitFactoryUsed = true; } else if (this.sqlSessionFactory != null) { definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory); explicitFactoryUsed = true; } if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) { if (explicitFactoryUsed) { LOGGER.warn(() -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored."); } definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName)); explicitFactoryUsed = true; } else if (this.sqlSessionTemplate != null) { if (explicitFactoryUsed) { LOGGER.warn(() -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored."); } definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate); explicitFactoryUsed = true; } if (!explicitFactoryUsed) { LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'."); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); } } }
此時,雖然還沒有完成介紹到掃描的過程,但是我們也應該理解了Spring中對於自動掃描的注冊,聲明MapperScannerConfigurer類型的bean目的是不需要我們對於每個接口都注冊一個MapperFactoryBean類型的對應的bean,但是,不在配置文件中注冊並不代表這個bean不存在,而是在掃描的過程中通過編碼的方式動態注冊。實現過程我們在上面的函數中可以看得非常清楚。
/** * Perform a scan within the specified base packages, * returning the registered bean definitions. * <p>This method does <i>not</i> register an annotation config processor * but rather leaves this up to the caller. * @param basePackages the packages to check for annotated classes * @return set of beans registered if any for tooling registration purposes (never {@code null}) */ protected Set<BeanDefinitionHolder> doScan(String... basePackages) { Assert.notEmpty(basePackages, "At least one base package must be specified"); Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>(); for (String basePackage : basePackages) { // 掃描basePackage路徑下java文件 Set<BeanDefinition> candidates = findCandidateComponents(basePackage); for (BeanDefinition candidate : candidates) { // 解析scope屬性 ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); candidate.setScope(scopeMetadata.getScopeName()); String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry); if (candidate instanceof AbstractBeanDefinition) { postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); } if (candidate instanceof AnnotatedBeanDefinition) { // 如果是AnnotatedBeanDefinition類型的bean,需要檢測下常用注解如:Primary、Lazy 等 AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); } // 檢測當前bean是否已經注冊 if (checkCandidate(beanName, candidate)) { BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); // 如果當前bean是用於生成代理的bean那么需要進一步處理 definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); beanDefinitions.add(definitionHolder); registerBeanDefinition(definitionHolder, this.registry); } } } return beanDefinitions; } /** * Scan the class path for candidate components. * @param basePackage the package to check for annotated classes * @return a corresponding Set of autodetected bean definitions */ public Set<BeanDefinition> findCandidateComponents(String basePackage) { if (this.componentsIndex != null && indexSupportsIncludeFilters()) { return addCandidateComponentsFromIndex(this.componentsIndex, basePackage); } else { return scanCandidateComponents(basePackage); } } private Set<BeanDefinition> scanCandidateComponents(String basePackage) { Set<BeanDefinition> candidates = new LinkedHashSet<>(); try { String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + resolveBasePackage(basePackage) + '/' + this.resourcePattern; Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath); boolean traceEnabled = logger.isTraceEnabled(); boolean debugEnabled = logger.isDebugEnabled(); for (Resource resource : resources) { if (traceEnabled) { logger.trace("Scanning " + resource); } if (resource.isReadable()) { try { MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource); if (isCandidateComponent(metadataReader)) { ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader); sbd.setResource(resource); sbd.setSource(resource); if (isCandidateComponent(sbd)) { if (debugEnabled) { logger.debug("Identified candidate component class: " + resource); } candidates.add(sbd); } else { if (debugEnabled) { logger.debug("Ignored because not a concrete top-level class: " + resource); } } } else { if (traceEnabled) { logger.trace("Ignored because not matching any filter: " + resource); } } } catch (Throwable ex) { throw new BeanDefinitionStoreException( "Failed to read candidate component class: " + resource, ex); } } else { if (traceEnabled) { logger.trace("Ignored because not readable: " + resource); } } } } catch (IOException ex) { throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex); } return candidates; }
findCandidateComponents方法根據傳人的包路徑信息並結合類文件路徑拼接成文件的絕對路徑,同時完成了文件的掃描過程並且根據對應的文件生成了對應的bean,使用 ScannedGenericBeanDefinition 類型的 bean 承載信息,bean 中只記錄了resource 和 source 信息。 這里,我們更感興趣的是isCandidateComponent(metadataReader),此句代碼用於判斷當前掃描的文件是否符合要求,而我們之前注冊的一些過濾器信息也正是在此時派上用場的。
/** * Determine whether the given class does not match any exclude filter * and does match at least one include filter. * @param metadataReader the ASM ClassReader for the class * @return whether the class qualifies as a candidate component */ protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException { for (TypeFilter tf : this.excludeFilters) { if (tf.match(metadataReader, getMetadataReaderFactory())) { return false; } } for (TypeFilter tf : this.includeFilters) { if (tf.match(metadataReader, getMetadataReaderFactory())) { return isConditionMatch(metadataReader); } } return false; }
我們看到了之前加入過濾器的兩個屬性excludeFilters、includeFilters,並且知道對應的文件是否符合要求是根據過濾器中的match方法所返回的信息來判斷的,當然用戶可以實現並注冊滿足自己業務邏輯的過濾器來控制掃描的結果,metadataReader中有你過濾所需要的全部文件信息。至此,我們完成了文件的掃描過程的分析。