Spring如何整合Mybatis,原始碼不難嘛!
Spring整合Mybtais會進行如下的配置(條條大路通羅馬,方式不唯一)。
private static final String ONE_MAPPER_BASE_PACKAGE = "com.XXX.dao.mapper.one";
@Bean
public MapperScannerConfigurer oneMapperScannerConfigurer() {
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
mapperScannerConfigurer.setBasePackage(ONE_MAPPER_BASE_PACKAGE);
mapperScannerConfigurer.
set SqlSessionFactoryBeanName("oneSqlSessionFactoryBean");
return mapperScannerConfigurer;
}
@Primary
@Bean(name="oneSqlSessionFactoryBean")
public SqlSessionFactoryBean oneSqlSessionFactoryBean( @Qualifier("oneDataSource") DruidDataSource oneDataSource) {
return getSqlSessionFactoryBeanDruid(oneDataSource,ONE_MAPPER_XML);
}
複製程式碼
短短不到20行程式碼,就完成了Spring整合Mybatis。
Amazing!!! 這背後到底發生了什麼?
還要從MapperScannerConfigurer 和SqlSessionFactoryBean 著手。
MapperScannerConfigurer
類註釋
-
beanDefinitionRegistryPostProcessor從 base package遞迴搜尋介面,將它們註冊為MapperFactoryBean。注意介面必須包含至少一個方法,其實現類將被忽略。
-
1.0.1以前是對BeanFactoryPostProcessor進行擴充套件,1.0.2以後是對 BeanDefinitionRegistryPostProcessor進行擴充套件,具體原因請查閱https://jira.springsource.org/browse/SPR-8269
-
basePackage可以配置多個,使用逗號或者分號分割。
-
通過annotationClass或markerInterface,可以設定指定掃描的介面。預設情況下這個2個屬性為空,basePackage下的所有介面將被掃描。
-
MapperScannerConfigurer為它建立的bean自動注入SqlSessionFactory或SqlSessionTemplate如果存在多個SqlSessionFactory,需要設定sqlSessionFactoryBeanName或sqlSessionTemplateBeanName來指定具體注入的sqlSessionFactory或sqlSessionTemplate。
-
不能傳入有佔位符的物件(例如: 包含資料庫的使用者名稱和密碼佔位符的物件)。可以使用beanName,將實際的物件建立推遲到所有佔位符替換完成後。注意MapperScannerConfigurer支援它自己的屬性使用佔位符,使用${property}這個種格式。
類圖找關鍵方法
從類圖上看MapperScannerConfigurer實現了BeanDefinitionRegistryPostProcessor,InitializingBean,ApplicationContextAware,BeanNameAware介面。各個介面具體含義如下:
- ApplicationContextAware:當spring容器初始化後,會自動注入ApplicationContext
- BeanNameAware :設定當前Bean在Spring中的名字
- InitializingBean介面只包括afterPropertiesSet方法,在初始化bean的時候會執行
- BeanDefinitionRegistryPostProcessor: 對BeanFactoryPostProcessor的擴充套件,允許在BeanFactoryPostProcessor執行前註冊多個bean的定義。需要擴充套件的方法為postProcessBeanDefinitionRegistry。
查詢,MapperScannerConfigurer的afterPropertiesSet方法如下,無具體擴充套件資訊。
@Override public void afterPropertiesSet() throws Exception {
notNull(this.basePackage,"Property 'basePackage' is required");
}
複製程式碼
結合MapperScannerConfigurer的註釋與類圖分析,確定其核心方法為:postProcessBeanDefinitionRegistry
postProcessBeanDefinitionRegistry分析
@Override
public void postProcessBeanDefinitionRegistry(
BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
//1. 佔位符屬性處理
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);
//2.設定過濾器
scanner.registerFilters();
//3.掃描java檔案
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage,ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
複製程式碼
從原始碼中看到除了processPropertyPlaceHolders外,其他工作都委託了ClassPathMapperScanner
processPropertyPlaceHolders處理佔位符
之前說BeanDefinitionRegistryPostProcessor在BeanFactoryPostProcessor執行前呼叫,
這就意味著Spring處理佔位符的類PropertyResourceConfigurer還沒有執行!
那MapperScannerConfigurer是如何支撐自己的屬性使用佔位符的呢?這一切的答案都在
processPropertyPlaceHolders這個方法中。
private void processPropertyPlaceHolders() {
Map<String,PropertyResourceConfigurer> prcs =
applicationContext.getBeansOfType(PropertyResourceConfigurer.class);
if (!prcs.isEmpty() && applicationContext
instanceof GenericApplicationContext) {
BeanDefinition mapperScannerBean =
((GenericApplicationContext) applicationContext)
.getBeanFactory().getBeanDefinition(beanName);
// PropertyResourceConfigurer 沒有暴露方法直接替換佔位符,
// 建立一個 BeanFactory包含MapperScannerConfigurer
// 然後執行BeanFactory後處理即可
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);
}
}
複製程式碼
看完processPropertyPlaceHolders,可以總結 MapperScannerConfigurer支援它自己的屬性使用佔位符的方式
-
找到所有已經註冊的PropertyResourceConfigurer型別的Bean
-
使用new DefaultListableBeanFactory()來模擬Spring環境,將MapperScannerConfigurer註冊到這個BeanFactory中,執行BeanFactory的後處理,來替換佔位符。
ClassPathMapperScanner的registerFilters方法
MapperScannerConfigurer的類註釋中有一條:
通過annotationClass或markerInterface,可以設定指定掃描的介面,預設情況下這個2個屬性為空,basePackage下的所有介面將被掃描。 scanner.registerFilters(),就是對annotationClass和markerInterface的設定。
public void registerFilters() {
boolean acceptAllInterfaces = true;
// 如果指定了annotationClass,
if (this.annotationClass != null) {
addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
acceptAllInterfaces = false;
}
// 重寫AssignableTypeFilter以忽略實際標記介面上的匹配項
if (this.markerInterface != null) {
addIncludeFilter(new AssignableTypeFilter(this.markerInterface) {
@Override
protected boolean matchClassName(String className) {
return false;
}
});
acceptAllInterfaces = false;
}
if (acceptAllInterfaces) {
// 預設處理所有介面
addIncludeFilter(new TypeFilter() {
@Override
public boolean match(
MetadataReader metadataReader,MetadataReaderFactory metadataReaderFactory) throws IOException {
return true;
}
});
}
// 不包含以package-info結尾的java檔案
// package-info.java包級檔案和包級別註釋
addExcludeFilter(new TypeFilter() {
@Override
public boolean match(MetadataReader metadataReader,MetadataReaderFactory metadataReaderFactory) throws IOException {
String className = metadataReader.getClassMetadata().getClassName();
return className.endsWith("package-info");
}
});
}
複製程式碼
雖然設定了過濾器,如何在掃描中起作用就要看scanner.scan方法了。
ClassPathMapperScanner的scan方法
public int scan(String... basePackages) {
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
doScan(basePackages);
// 註冊註解配置處理器
if (this.includeAnnotationConfig) {
AnnotationConfigUtils
.registerAnnotationConfigProcessors(this.registry);
}
return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}
複製程式碼
doScan方法如下:
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;
}
複製程式碼
位於ClassPathMapperScanner的父類ClassPathBeanDefinitionScanner的doScan方法,就是
掃描包下的所有java檔案轉換為BeanDefinition(實際是ScannedGenericBeanDefinition)。
processBeanDefinitions就是將之前的BeanDefinition轉換為MapperFactoryBean的BeanDefinition。
至於過濾器如何生效(即annotationClass或markerInterface)呢?我一路追蹤原始碼
終於在ClassPathScanningCandidateComponentProvider的isCandidateComponent找到了對過濾器的處理
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
for (TypeFilter tf : this.excludeFilters) {
if (tf.match(metadataReader,this.metadataReaderFactory)) {
return false;
}
}
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader,this.metadataReaderFactory)) {
return isConditionMatch(metadataReader);
}
}
return false;
}
複製程式碼
總結MapperScannerConfigurer的作用
MapperScannerConfigurer實現了beanDefinitionRegistryPostProcessor的postProcessBeanDefinitionRegistry方法
從指定的 basePackage的目錄遞迴搜尋介面,將它們註冊為MapperFactoryBean
SqlSessionFactoryBean
類註釋
-
建立Mybatis的SqiSessionFactory,用於Spring上下文中進行共享。
-
SqiSessionFactory可以通過依賴注入到與mybatis的daos中。
-
datasourcetransactionmanager,jtatransactionmanager與sqlsessionfactory想結合實現事務。
類圖找關鍵方法
SqlSessionFactoryBean實現了ApplicationListener ,InitializingBean,FactoryBean介面,各個介面的說明如下:
- ApplicationListener 用於監聽Spring的事件
- InitializingBean介面只包括afterPropertiesSet方法,在初始化bean的時候會執行
- FactoryBean:返回的物件不是指定類的一個例項,其返回的是該FactoryBean的getObject方法所返回的物件
應該重點關注afterPropertiesSet和getObject的方法。
關鍵方法分析
afterPropertiesSet方法
public void afterPropertiesSet() throws Exception {
notNull(dataSource,"Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder,"Property 'sqlSessionFactoryBuilder' is required");
state((configuration == null && configLocation == null)
|| !(configuration != null && configLocation != null),"Property 'configuration' and 'configLocation' can not specified with together");
this.sqlSessionFactory = buildSqlSessionFactory();
}
複製程式碼
buildSqlSessionFactory看方法名稱就知道在這裡進行了SqlSessionFactory的建立,具體原始碼不在贅述。
getObject方法
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
複製程式碼
總結SqlSessionFactoryBean
實現了InitializingBean的afterPropertiesSet,在其中建立了Mybatis的SqlSessionFactory
實現了FactoryBean的getObject 返回建立好的sqlSessionFactory。
疑問
看完這SqlSessionFactoryBean和MapperScannerConfigurer之後,不知道你是否有疑問!一般在Spring中使用Mybatis的方式如下:
ApplicationContext context=new AnnotationConfigApplicationContext();
UsrMapper usrMapper=context.getBean("usrMapper");
實際上呼叫的是
sqlSession.getMapper(UsrMapper.class);
複製程式碼
SqlSessionFactoryBean建立了Mybatis的SqlSessionFactory。MapperScannerConfigurer將介面轉換為了MapperFactoryBean。那又哪裡呼叫的sqlSession.getMapper(UsrMapper.class)呢???
MapperFactoryBean是這一切的答案(MapperFactoryBean:注意看我的名字---Mapper的工廠!!)
MapperFactoryBean說明
類註釋
能夠注入MyBatis對映介面的BeanFactory。它可以設定SqlSessionFactory或預配置的SqlSessionTemplate。
注意這個工廠僅僅注入介面不注入實現類
類圖找關鍵方法
看類圖,又看到了InitializingBean和FactoryBean!!!
- InitializingBean介面只包括afterPropertiesSet方法,在初始化bean的時候會執行
- FactoryBean:返回的物件不是指定類的一個例項,其返回的是該FactoryBean的getObject方法所返回的物件
再次重點關注afterPropertiesSet和getObject的實現!
關鍵方法分析
DaoSupport類中有afterPropertiesSet的實現如下:
public final void afterPropertiesSet()
throws IllegalArgumentException,BeanInitializationException {
this.checkDaoConfig();
try {
this.initDao();
} catch (Exception var2) {
throw
new BeanInitializationException(
"Initialization of DAO failed",var2);
}
}
複製程式碼
initDao是個空實現,checkDaoConfig在MapperFactoryBean中有實現如下:
protected void checkDaoConfig() {
super.checkDaoConfig();
notNull(this.mapperInterface,"Property 'mapperInterface' is required");
Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
configuration.addMapper(this.mapperInterface);
} catch (Exception e) {
logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.",e);
throw new IllegalArgumentException(e);
} finally {
ErrorContext.instance().reset();
}
}
}
複製程式碼
關鍵的語句是configuration.addMapper(this.mapperInterface),將介面新增到Mybatis的配置中。
getObject方法超級簡單,就是呼叫了sqlSession.getMapper(UsrMapper.class);
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
複製程式碼
總結MapperFactoryBean
實現了InitializingBean的afterPropertiesSet方法,在其中將mapper介面設定到mybatis的配置中。
實現了FactoryBean的getObject 方法,呼叫了sqlSession.getMapper,返回mapper物件。
總結
Spring整合Mybatis核心3類:
MapperScannerConfigurer
實現了beanDefinitionRegistryPostProcessor的postProcessBeanDefinitionRegistry方法,在其中從指定的 basePackage的目錄遞迴搜尋介面,將它們註冊為MapperFactoryBean型別的BeanDefinition
SqlSessionFactoryBean
實現了InitializingBean的afterPropertiesSet,在其中建立了Mybatis的SqlSessionFactory。
實現了FactoryBean的getObject 返回建立好的sqlSessionFactory。
MapperFactoryBean
實現了InitializingBean的afterPropertiesSet方法,將mapper介面設定到mybatis的配置中。
實現了FactoryBean的getObject 方法,呼叫了sqlSession.getMapper,返回mapper物件。