原創 | 從Spring Boot 2.x整合Mybatis-Plus深入理解Mybatis解析Mapper底層原理
背景
最近在使用高版本Spring Boot 2.x
整合mybatis-plus 3.4.1
時,控制檯出現大量的warn
提示XxxMapper
重複定義資訊:Bean already defined with the same name
。
2020-12-0719:37:26.025WARN25756---[main]o.m.s.mapper.ClassPathMapperScanner:SkippingMapperFactoryBeanwithname'roleMapper'and'com.dunzung.java.spring.mapper.RoleMapper'mapperInterface.Beanalreadydefinedwiththesamename!
2020-12-0719:37:26.025WARN25756---[main]o.m.s.mapper.ClassPathMapperScanner:SkippingMapperFactoryBeanwithname'userMapper'and'com.dunzung.java.spring.mapper.UserMapper'mapperInterface.Beanalreadydefinedwiththesamename!
2
雖然這些警告並不影響程式正確執行,但是每次啟動程式看到控制檯輸出這些警告日誌資訊,心情不是很美麗呀。
於是趁著最近這段空閒時間,快馬加鞭動起了我的“發財”小手,擼起袖子加油幹,花了一點時間研究了下mybatis-plus
如何初始化mapper
物件的相關原始碼。
問題分析開掛模式
Maven 依賴
從Bean already defined with the same name
警告資訊來看,感覺應該是:重複載入 mapper 的 bean 物件定義了。所以我從mybatis-plus
的pom
依賴入手,找到mybatis-plus
總共依賴三個jar
包:
- mybatis-plus-boot-starter 3.4.1
- mybatis-plus-extension 3.4.1
- pagehelper-spring-boot-starter 1.2.10
接著,看了下mybatis-plus
啟動相關配置,發現也沒啥毛病。
mybatis-plus 配置類
@Configuration
@MapperScan(basePackages="com.dunzung.**.mapper.**")
publicclassMybatisPlusConfiguration{
@Bean
publicPaginationInterceptorpaginationInterceptor(){
PaginationInterceptorpaginationInterceptor=newPaginationInterceptor();
paginationInterceptor.setDbType(DbType.MYSQL);
returnpaginationInterceptor;
}
}
Service 類定義
自定義的MybatisServiceImpl
繼承了mybatis-plus
的ServiceImpl
實現類;自定義的MybatisService
繼承了IService
介面類。
/**
*自定義Service介面基類
*/
publicinterfaceMybatisService<T>extendsIService<T>{
}
publicinterfaceRoleServiceextendsMybatisService<RoleEntity>{
}
/**
*自定義Service實現介面基類
*/
publicclassMybatisServiceImpl<MextendsDaoMapper<T>,T>extendsServiceImpl<M,T>implementsMybatisService<T>{
}
@Slf4j
@Service
publicclassRoleServiceImplextendsMybatisServiceImpl<RoleMapper,RoleEntity>implementsRoleService{
}
Mapper
類定義
RoleMapper
基於註解@Mapper
配置,基本上零配置(xml
)。
@Mapper
publicinterfaceRoleMapperextendsDaoMapper<RoleEntity>{
}
上面的mybatis-plus
相關配置非常簡單,沒啥毛病,所以只能從mybatis-plus
相關的三個jar
原始碼入手了。
祖傳原始碼分析
從日誌輸出資訊定位可以看出是o.m.s.mapper.ClassPathMapperScanner
列印的警告日誌,於是在ClassPathMapperScanner
類中找到了輸出警告日誌的checkCandidate()
方法:
/**
*{@inheritDoc}
*/
@Override
protectedbooleancheckCandidate(StringbeanName,BeanDefinitionbeanDefinition){
if(super.checkCandidate(beanName,beanDefinition)){
returntrue;
}else{
LOGGER.warn(()->"SkippingMapperFactoryBeanwithname'"+beanName+"'and'"
+beanDefinition.getBeanClassName()+"'mapperInterface"+".Beanalreadydefinedwiththesamename!");
returnfalse;
}
}
}
開啟Debug
模式,在ClassPathMapperScanner
的checkCandidate()
方法體打斷點,驗證該方法是否重複呼叫兩次。
- 第一次
Spring Boot
程式啟動時會自動裝配mybatis-spring-boot-autoconfigure
這個jar
包中的MybatisAutoConfiguration
配置類,通過其內部類AutoConfiguredMapperScannerRegistrar
的registerBeanDefinitions()
註冊bean
方法,呼叫了ClassPathMapperScanner
的doScan()
方法,然後通過checkCandidate()
方法判斷mapper
物件是否已註冊。
doScan
方法詳細程式碼如下:
protectedSet<BeanDefinitionHolder>doScan(String...basePackages){
...
for(StringbasePackage:basePackages){
Set<BeanDefinition>candidates=findCandidateComponents(basePackage);
for(BeanDefinitioncandidate:candidates){
...
if(checkCandidate(beanName,candidate)){
...
}
}
}
Tips
“
checkCandidate()
對已註冊mapper
物件進行是否重複定義判斷
- 第二次通過
MapperScans
註解,通過@Import
註解,匯入並呼叫了mybatis-spring-2.0.5
這個jar
包中MapperScannerConfigurer
類的postProcessBeanDefinitionRegistry()
方法,在postProcessBeanDefinitionRegistry()
方法中 再一次例項化mapper
的掃描類ClassPathMapperScanner
,並又一次呼叫doScan
方法初始化mapper
物件,且也呼叫了checkCandidate()
方法,從而有了文章開頭日誌輸出的Bean already defined with the same name
警告資訊。
@Override
publicvoidpostProcessBeanDefinitionRegistry(BeanDefinitionRegistryregistry){
if(this.processPropertyPlaceHolders){
processPropertyPlaceHolders();
}
ClassPathMapperScannerscanner=newClassPathMapperScanner(registry);
...
scanner.registerFilters();
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage,ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
Debug
除錯到這裡,大致猜到是mybatis-plus
相關jar
包有bug
了,主要涉及兩個jar
:
-
第一個是
mybatis-spring-boot-autoconfigure
,主要是用於spring
自動裝配mybatis
相關初始化配置,mybatis
自動裝配配置類是MybatisAutoConfiguration
。 -
第二個是
mybatis-spring
,從http://mybatis.org/
官網可知,這個包是mybatis
與spring
結合具備事務管理功能的資料訪問應用程式包,涉及到資料庫操作,如資料來源(DataSoure
),操作Sql
的SqlSessionFactory
工廠類,以及 初始化Mapper
的MapperFactoryBean
工廠類等等。
解決問題我是有原則的
從上面的debug
除錯程式碼分析可以得出,mapper
確實被例項化了2
次,也驗證了我當初的判斷。
那為什麼會這樣呢?
我們不妨先把工程依賴的pagehelper-spring-boot-starter
升級最新版到1.3.0
版本,mybatis-plus-boot-starter
和mybatis-plus-extension
已經是最新版本3.4.1
,再次Application
啟動警告盡然自動消失了。這裡我對比了在mybatis-spring-boot-autoconfigure
包中MybatisAutoConfiguration
所屬內部類AutoConfiguredMapperScannerRegistrar
的registerBeanDefinitions()
方法,發現1.3.2
版本和2.1.3
版本的程式碼實現區別非常大,幾乎是重寫了該方法。
mybatis-spring-boot-autoconfigure 的 1.3.2 版本寫法
/**
*ThiswilljustscanthesamebasepackageasSpringBootdoes.Ifyouwant
*morepower,youcanexplicitlyuse
*{@linkorg.mybatis.spring.annotation.MapperScan}butthiswillgettyped
*mappersworkingcorrectly,out-of-the-box,similartousingSpringDataJPA
*repositories.
*/
publicstaticclassAutoConfiguredMapperScannerRegistrar
implementsBeanFactoryAware,ImportBeanDefinitionRegistrar,ResourceLoaderAware{
privateBeanFactorybeanFactory;
privateResourceLoaderresourceLoader;
@Override
publicvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry){
logger.debug("Searchingformappersannotatedwith@Mapper");
ClassPathMapperScannerscanner=newClassPathMapperScanner(registry);
try{
if(this.resourceLoader!=null){
scanner.setResourceLoader(this.resourceLoader);
}
List<String>packages=AutoConfigurationPackages.get(this.beanFactory);
if(logger.isDebugEnabled()){
for(Stringpkg:packages){
logger.debug("Usingauto-configurationbasepackage'{}'",pkg);
}
}
scanner.setAnnotationClass(Mapper.class);
scanner.registerFilters();
scanner.doScan(StringUtils.toStringArray(packages));
}catch(IllegalStateExceptionex){
logger.debug("Couldnotdetermineauto-configurationpackage,automaticmapperscanningdisabled.",ex);
}
}
}
/**
*{@linkorg.mybatis.spring.annotation.MapperScan}ultimatelyendsup
*creatinginstancesof{@linkMapperFactoryBean}.If
*{@linkorg.mybatis.spring.annotation.MapperScan}isusedthenthis
*auto-configurationisnotneeded.Ifitis_not_used,however,thenthis
*willbringinabeanregistrarandautomaticallyregistercomponentsbased
*onthesamecomponent-scanningpathasSpringBootitself.
*/
@org.springframework.context.annotation.Configuration
@Import({AutoConfiguredMapperScannerRegistrar.class})
@ConditionalOnMissingBean(MapperFactoryBean.class)
publicstaticclassMapperScannerRegistrarNotFoundConfiguration{
@PostConstruct
publicvoidafterPropertiesSet(){
logger.debug("No{}found.",MapperFactoryBean.class.getName());
}
}
}
mybatis-spring-boot-autoconfigure 的 2.1.3 版本寫法
@Configuration
@Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
@ConditionalOnMissingBean({MapperFactoryBean.class,MapperScannerConfigurer.class})
publicstaticclassMapperScannerRegistrarNotFoundConfigurationimplementsInitializingBean{
publicMapperScannerRegistrarNotFoundConfiguration(){
}
publicvoidafterPropertiesSet(){
MybatisAutoConfiguration.logger.debug("Notfoundconfigurationforregisteringmapperbeanusing@MapperScan,MapperFactoryBeanandMapperScannerConfigurer.");
}
}
publicstaticclassAutoConfiguredMapperScannerRegistrarimplementsBeanFactoryAware,ImportBeanDefinitionRegistrar{
privateBeanFactorybeanFactory;
publicAutoConfiguredMapperScannerRegistrar(){
}
publicvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry){
if(!AutoConfigurationPackages.has(this.beanFactory)){
MybatisAutoConfiguration.logger.debug("Couldnotdetermineauto-configurationpackage,automaticmapperscanningdisabled.");
}else{
MybatisAutoConfiguration.logger.debug("Searchingformappersannotatedwith@Mapper");
List<String>packages=AutoConfigurationPackages.get(this.beanFactory);
if(MybatisAutoConfiguration.logger.isDebugEnabled()){
packages.forEach((pkg)->{
MybatisAutoConfiguration.logger.debug("Usingauto-configurationbasepackage'{}'",pkg);
});
}
BeanDefinitionBuilderbuilder=BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders",true);
builder.addPropertyValue("annotationClass",Mapper.class);
builder.addPropertyValue("basePackage",StringUtils.collectionToCommaDelimitedString(packages));
BeanWrapperbeanWrapper=newBeanWrapperImpl(MapperScannerConfigurer.class);
Stream.of(beanWrapper.getPropertyDescriptors()).filter((x)->{
returnx.getName().equals("lazyInitialization");
}).findAny().ifPresent((x)->{
builder.addPropertyValue("lazyInitialization","${mybatis.lazy-initialization:false}");
});
registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(),builder.getBeanDefinition());
}
}
publicvoidsetBeanFactory(BeanFactorybeanFactory){
this.beanFactory=beanFactory;
}
}
}
從1.3.2
和2.1.3
原始碼對比可以看出:
2.1.3
版本中,在MapperScannerRegistrarNotFoundConfiguration
類的條件註解@ConditionalOnMissingBean
加上了MapperScannerConfigurer.class
這個mapper
配置掃描類判斷。
也就是說在bean
容器中,只會存在一個單例的MapperScannerConfigurer
物件,並且只會在spring
容器註冊bean
的時候,通過postProcessBeanDefinitionRegistry()
方法初始化一次mapper
物件,不像1.3.2
版本那樣通過不同的類兩次去例項化ClassPathMapperScanner
類,重新註冊mapper
物件。
而造成不一致的直接原因是mybatis-plus-extension
和pagehelper-spring-boot-starter
共同依賴的mybatis-spring
的版本不一致導致的。
mybatis-plus-extension
依賴的是mybatis-spring
的2.0.5
版本
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.5</version>
<scope>compile</scope>
</dependency>
pagehelper-spring-boot-starter
依賴的是mybatis-spring
的1.3.2
版本
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.2</version>
</dependency>
所以由上總述,知道了問題產生的原因,解決辦法就很簡單了,只需要把pagehelper-spring-boot-starter
的版本升級到1.3.0
即可。
有態度的良心總結
雖然提示Bean already defined with the same name
警告資訊的直接原因是pagehelper-spring-boot-starter
和mybatis-plus-extension
共同依賴的mybatis-spring
的版本不一致導致。
但根本原因在於MapperScannerConfigurer
和AutoConfiguredMapperScannerRegistrar
類中兩次例項化ClassPathMapperScanner
物件註冊mapper
物件所導致。
後記
在實際的生產環境中,每次開源框架級別的升級,要特別注意框架所依賴的版本對應關係,最好的辦法是去相關開源框架的官網瞭解具體的版本升級部落格文章或升級日誌,避免帶來不必要的麻煩和損失。
作者簡介:編筐少年,一枚簡單的北漂程式設計師。喜歡用簡單的文字記錄工作與生活中的點點滴滴,願與你一起分享程式設計師靈魂深處真正的內心獨白。我的微訊號:WooolaDunzung,公眾號【猿芯】輸入1024,有份驚喜送給你哦。
< END >
【猿芯】微信掃描二維碼,關注我的公眾號。
來源:鄂爾多斯SEO