為了使用MyBatis功能,Spring配置文件中提供了兩個bean,除了之前分析的sqlSessionFactoryBean類型的bean以外,還有一個是MapperFactoryBean類型的bean。
對於單獨使用MyBatis的時候調用數據庫接口的方式是:
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
而在這一過程中,其實是MyBatis在獲取映射的過程中根據配置信息為UserMapper類型動態創建了代理類。而對於Spring的創建方式:
UserMapper userMapper = (UserMapper)context.getBean("userMapper");
Spring中獲取的名為userMapper的bean,其實是與單獨使用MyBatis完成了一樣的功能,那么我們可以推斷,在bean的創建過程中一定使用了MyBatis中的原生方法sqlSession.getMapper(UserMapper.class)進行了再一次封裝。結合配置文件,我們把分析目標轉向org.mybatis.Spring.mapper.MapperFactoryBean,初步推測其中的邏輯應該在此類中實現,同樣,還是先來看一下MapperFactoryBean的類圖:
在實現的接口中有兩個比較重要的接口:InitializingBean和FactoryBean。我們的分析還是從bean的初始化開始。
1.MapperFactoryBean的初始化
因為實現了InitializingBean接口,Spring會保證在bean初始化時首先調用afterPropertiesSet方法來完成其初始化邏輯。追蹤父類,發現afterPropertiesSet方法是在DaoSupport類中實現,代碼如下:
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException { this.checkDaoConfig(); try { this.initDao(); } catch (Exception var2) { throw new BeanInitializationException("Initialization of DAO failed", var2); } }
從函數的名稱推測,MapperFactoryBean的初始化包括對DAO配置的驗證以及對DAO的初始工作,其中的initDao方法時模板方法,設計為留給子類做進一步的邏輯處理。checkDaoConfig才是我們分析的重點:
protected void checkDaoConfig() { super.checkDaoConfig(); Assert.notNull(this.mapperInterface, "Property 'mapperInterface' is required"); Configuration configuration = this.getSqlSession().getConfiguration(); if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) { try { configuration.addMapper(this.mapperInterface); } catch (Exception var6) { this.logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", var6); throw new IllegalArgumentException(var6); } finally { ErrorContext.instance().reset(); } } }
super.checkDaoConfig();:在SqlSessionDaoSupport類中實現,代碼如下:
protected void checkDaoConfig() { Assert.notNull(this.sqlSession, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required"); }
結合代碼我們了解到對於DAO配置的驗證,Spring做了以下幾個方面的工作:
❤ 父類中對於sqlSession不為空的驗證;
sqlSession作為根據接口創建映射器代理的接觸類一定不可以為空,而sqlSession的初始化工作是在設定其sqlSessionFactory屬性時完成的。
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { if (!this.externalSqlSession) { this.sqlSession = new SqlSessionTemplate(sqlSessionFactory); } }
也就是說,對於下面的配置如果忽略了對於sqlSessionFactory屬性的設置,那么在此時就會被檢測出來。
<bean id="userMapper" class="org.mybatis.Spring.mapper.MapperFactoryBean"> <property name="mapperInterface" value="test.mybatis.dao.UserMapper"/> <property name="sqlSessionFactory" ref = "sqlSessionFactory"/> </bean>
❤ 映射接口的驗證;
接口是映射的基礎,sqlSession會根據接口動態創建相應的代理類,所以接口必不可少。
❤ 映射文件存在性驗證;
對於函數的前半部分的驗證我們都很容易理解,無非是對配置文件中的屬性是否存在做驗證,但是后半部分完成了什么方面的驗證呢?
在MyBatis實現過程中並沒有手動調用configuration.addMapper方法,而是在映射文件讀取過程中一旦解析到如<mapper namespace="Mapper.UserMapper">,便會自動進行類型映射的注冊。那么,Spring中為什么會把這個功能單獨拿出來放在驗證里呢?這里是不是多此一舉呢?
在上面的函數中,configuration.addMapper(this.mapperInterface)其實就是將UserMapper注冊到映射類型中,如果你可以保證這個接口一定存在對應的映射文件,那么其實這個驗證並沒有必要。但是,由於這個是我們自行決定的配置,無法保證這里配置的接口一定存在對應的映射文件,所以這里非常有必要進行驗證。在執行此代碼的時候,MyBatis會檢查嵌入的映射接口是否存在對應的映射文件,如果沒有拋出異常,Spring正是用這種方式來完成接口對應的映射文件存在性驗證。
2.獲取MapperFactoryBean的實例
由於MapperFactoryBean實現了FactoryBean接口,所以當通過getBean方法獲取對應實例的時候其實是獲取該類的getObject函數返回的實例。
public T getObject() throws Exception { return this.getSqlSession().getMapper(this.mapperInterface); }
這段代碼正是我們在提供MyBatis獨立使用的時候的一個代碼調用,Spring通過FactoryBean進行了封裝。
3.MapperScannerConfigurer
我們在applicationContext.xml中配置了UserMapper供需要時使用。但如果需要用到的映射器較多的話,采用這種配置方式就會很低效。為了解決這個問題,我們可以使用MapperScannerConfigurer,讓它掃描特定的包,自動幫我們成批的創建映射器。這樣一來,就能大大減少配置的工作量。
<!-- 配置掃描Dao接口包,動態實現Dao接口,注入到spring容器中 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- 注入sqlSessionFactory --> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> <!-- 給出需要掃描Dao接口包 --> <property name="basePackage" value="com.joe.dao"/> </bean>
在上面的配置中,我們屏蔽掉了最原始的代碼而增加了MapperScannerConfigurer的配置,basePackage屬性是讓你為映射器接口文件設置基本的包路徑,可以使用分號或逗號作為分隔符設置多於一個的包路徑。每個映射器將會在指定的包路徑中遞歸地被搜索到。被發現的映射器將會使用Spring對自動偵測組件默認的命名策略來命名。也就是說,如果沒有發現注解,它就會使用映射器的非大寫的非完全限定類名。但是如果發現了@Component或JSR-330@Named注解,它就會獲取名稱。
通過上面的配置,Spring就會幫助我們對com.joe.dao下面的所有接口進行自動的注入,而不需要為每個接口重復在Spring的配置文件中進行聲明了。那么這個功能又是如何做到的呢?MapperScannerConfigurer中又有那些核心的操作呢?同樣,我們先來看一下類圖:
我們又看到了接口InitializingBean,來看它的afterPropertiesSet方法:
public void afterPropertiesSet() throws Exception { Assert.notNull(this.basePackage, "Property 'basePackage' is required"); }
afterPropertiesSet方法除了一句對basePackage屬性的驗證代碼外並木有太多的邏輯實現。所以轉而分析其他的接口。
BeanDefinitionRegistryPostProcessor與BeanFactoryPostProcessor這兩個接口,Spring在初始化的過程中同樣會保證這兩個接口的調用。
首先查看MapperScannerConfigurer類中對於BeanFactoryPostProcessor接口的實現:
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { }
木有任何邏輯,說明我們找錯了,繼續,查看MapperScannerConfigurer類中對於BeanDefinitionRegistryPostProcessor接口的實現:
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { if (this.processPropertyPlaceHolders) { this.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, ",; \t\n")); }
這次找對了,大致看一下代碼實現,正是完成了對指定路徑掃描的邏輯。那么我們就以此為入口,詳細的分析MapperScannerConfigurer所提供的邏輯實現。
1.processPropertyPlaceHolders屬性的處理
首先,難題就是processPropertyPlaceHolders屬性的處理。我們通過processPropertyPlaceHolders函數來反推此屬性所代表的功能。
rivate void processPropertyPlaceHolders() { Map<String, PropertyResourceConfigurer> prcs = this.applicationContext.getBeansOfType(PropertyResourceConfigurer.class); if (!prcs.isEmpty() && this.applicationContext instanceof ConfigurableApplicationContext) { BeanDefinition mapperScannerBean = ((ConfigurableApplicationContext)this.applicationContext).getBeanFactory().getBeanDefinition(this.beanName); DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); factory.registerBeanDefinition(this.beanName, mapperScannerBean); Iterator var4 = prcs.values().iterator(); while(var4.hasNext()) { PropertyResourceConfigurer prc = (PropertyResourceConfigurer)var4.next(); prc.postProcessBeanFactory(factory); } PropertyValues values = mapperScannerBean.getPropertyValues(); this.basePackage = this.updatePropertyValue("basePackage", values); this.sqlSessionFactoryBeanName = this.updatePropertyValue("sqlSessionFactoryBeanName", values); this.sqlSessionTemplateBeanName = this.updatePropertyValue("sqlSessionTemplateBeanName", values); } }
從此函數中可以看出,BeanDefinitionRegistries會在應用啟動的時候調用,並且會早於BeanFactoryPostProcessors的調用,這就意味着PropertyResourceConfigurers還沒有被加載所有對於屬性文件的引用將會失效。為了避免這種情況的發生,此方法手動地找出定義的 PropertyResourceConfigurers並進行提前調用以保證對於屬性的引用可以正常工作。
這個函數所做的事情:
(1)找到所有已經注冊的PropertyResourceConfigurers類型的bean。
(2)模擬Spring中的環境來用處理器。這里通過使用new DefaultListableBeanFactory來模擬Spring中的環境(完成處理器的調用后便失效),將映射的bean,也就是MapperScannerConfigurer類型的bean注冊到環境中進行后處理器的調用,處理器PropertyResourceConfigurers調用完成的功能,即找出所有bean中應用屬性文件的變量並替換。也就是說,在處理器調用后,模擬環境中模擬的MapperScannerConfigurer類型的bean如果有引入屬性文件中的屬性那么已經被替換了,這時,再將模擬bean中的相關的屬性提取出來應用在真實的bean中。
2.根據配置屬性生成過濾器
在PostProcessBeanDefinitionRegistry方法中可以看到,配置中支持很多屬性的設定,但是,我們感興趣或者說影響掃描結果的並不多,屬性設置后通過scanner.registerFilters()代碼中生成對應的過濾器來控制掃描結果。
1 public void registerFilters() { 2 boolean acceptAllInterfaces = true; 3 if (this.annotationClass != null) { 4 this.addIncludeFilter(new AnnotationTypeFilter(this.annotationClass)); 5 acceptAllInterfaces = false; 6 } 7 8 if (this.markerInterface != null) { 9 this.addIncludeFilter(new AssignableTypeFilter(this.markerInterface) { 10 protected boolean matchClassName(String className) { 11 return false; 12 } 13 }); 14 acceptAllInterfaces = false; 15 } 16 17 if (acceptAllInterfaces) { 18 this.addIncludeFilter(new TypeFilter() { 19 public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { 20 return true; 21 } 22 }); 23 } 24 25 this.addExcludeFilter(new TypeFilter() { 26 public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { 27 String className = metadataReader.getClassMetadata().getClassName(); 28 return className.endsWith("package-info"); 29 } 30 }); 31 }
上述代碼是根據之前的屬性的配置生成了對應的過濾器。
(1)annotationClass屬性的處理(3~6行)。
如果annotationClass不為空,表示用戶設置了此屬性,那么就要根據此屬性生成過濾器以保證達到用戶想要的效果,而封裝此屬性的過濾器就是AnnotationTypeFilter。AnnotationTypeFilter保證在掃描對應Java文件時只接受標記為注解annotationClass的接口。
(2)makerInterface屬性處理(8~15行)。
如果makerInterface不為空,表示用戶設置了此屬性,那么就要根據此屬性生成過濾器以保證達到用戶想要的結果,而封裝此屬性的過濾器就是實現AssignableTypeFilter接口的局部類,表示掃描過程中只有makerInterface接口的接口才會被接受。
(3)全局默認處理(17~23行)。
在上面的兩個屬性中國如果存在其中任何屬性,acceptAllInterfaces的值將會改變,但是如果用戶沒有設置以上兩個屬性,那么,Spring會為我們增加一個默認的過濾器實現TypeFilter接口的局部類,旨在接收所有接口文件。
(4)package-info.java處理(25~30行)。
對於名為package-info的Java文件,默認不作為邏輯實現接口,將其排除掉,使用TypeFilter接口的局部類實現match方法。
從上面的函數中我們看出,控制掃描文件Spring通過不同的過濾器完成,這些定義的過濾器記錄在了includeFilter和excludeFilter屬性中。
public void addIncludeFilter(TypeFilter includeFilter) { this.includeFilters.add(includeFilter); }
public void addExcludeFilter(TypeFilter excludeFilter) { this.excludeFilters.add(0, excludeFilter); }
3.掃描Java文件
設置了相關屬性以及生成了對應的過濾器后便可以進行文件的掃描了,掃描工作是由ClassPathMapperScanner類型的實例scanner中的scan方法完成的。
public int scan(String... basePackages) { int beanCountAtScanStart = this.registry.getBeanDefinitionCount(); doScan(basePackages); // 如果配置了includeAnnotationConfig,則注冊對應注解的處理器以保證注解功能的正常使用 if (this.includeAnnotationConfig) { AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); } return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart); }
scan是個全局方法,掃描工作委托給了doScan方法,同時,還包括了includeAnnotationConfig屬性的處理,AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry)代碼主要是完成對於注解處理器的簡單注冊,比如 AutowireAnnotationBeanPostProcessor、RequiredAnnotationBeanPostProcessor等,這里就不再贅述,我們的重點是研究文件的掃描功能的研究。
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,需要檢測下常用注釋如:Lazy、Primary。 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; }
來看一下findCandidateComponents方法:
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這句代碼用於判斷當前掃描的文件是否符合要求,而我們之前注冊的一些過濾信息也正是在此時派上用場的。
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方法所返回的信息來判斷的,當然用戶可以實現並注冊滿足自己業務邏輯的過濾器來控制掃描的結果,metedataReader中有你過濾所需要的全部文件信息。至此,我們完成了文件的掃描過程的分析。
參考:《Spring源碼深度解析》 郝佳 編著: