1. 程式人生 > 實用技巧 >原創 | 從Spring Boot 2.x整合Mybatis-Plus深入理解Mybatis解析Mapper底層原理

原創 | 從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-pluspom依賴入手,找到mybatis-plus總共依賴三個jar包:

  1. mybatis-plus-boot-starter 3.4.1
  2. mybatis-plus-extension 3.4.1
  3. 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-plusServiceImpl實現類;自定義的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模式,在ClassPathMapperScannercheckCandidate()方法體打斷點,驗證該方法是否重複呼叫兩次。

  • 第一次Spring Boot程式啟動時會自動裝配mybatis-spring-boot-autoconfigure這個jar包中的MybatisAutoConfiguration配置類,通過其內部類AutoConfiguredMapperScannerRegistrarregisterBeanDefinitions()註冊bean方法,呼叫了ClassPathMapperScannerdoScan()方法,然後通過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/官網可知,這個包是mybatisspring結合具備事務管理功能的資料訪問應用程式包,涉及到資料庫操作,如資料來源(DataSoure),操作SqlSqlSessionFactory工廠類,以及 初始化MapperMapperFactoryBean工廠類等等。

解決問題我是有原則的

從上面的debug除錯程式碼分析可以得出,mapper確實被例項化了2次,也驗證了我當初的判斷。

那為什麼會這樣呢?

我們不妨先把工程依賴的pagehelper-spring-boot-starter升級最新版到1.3.0版本,mybatis-plus-boot-startermybatis-plus-extension已經是最新版本3.4.1,再次Application啟動警告盡然自動消失了。這裡我對比了在mybatis-spring-boot-autoconfigure包中MybatisAutoConfiguration所屬內部類AutoConfiguredMapperScannerRegistrarregisterBeanDefinitions()方法,發現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.22.1.3原始碼對比可以看出:

2.1.3版本中,在MapperScannerRegistrarNotFoundConfiguration類的條件註解@ConditionalOnMissingBean加上了MapperScannerConfigurer.class這個mapper配置掃描類判斷。

也就是說在bean容器中,只會存在一個單例的MapperScannerConfigurer物件,並且只會在spring容器註冊bean的時候,通過postProcessBeanDefinitionRegistry()方法初始化一次mapper物件,不像1.3.2版本那樣通過不同的類兩次去例項化ClassPathMapperScanner類,重新註冊mapper物件。

而造成不一致的直接原因是mybatis-plus-extensionpagehelper-spring-boot-starter共同依賴的mybatis-spring的版本不一致導致的。

mybatis-plus-extension依賴的是mybatis-spring2.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-spring1.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-startermybatis-plus-extension共同依賴的mybatis-spring的版本不一致導致。

但根本原因在於MapperScannerConfigurerAutoConfiguredMapperScannerRegistrar類中兩次例項化ClassPathMapperScanner物件註冊mapper物件所導致。

後記

在實際的生產環境中,每次開源框架級別的升級,要特別注意框架所依賴的版本對應關係,最好的辦法是去相關開源框架的官網瞭解具體的版本升級部落格文章或升級日誌,避免帶來不必要的麻煩和損失。

作者簡介:編筐少年,一枚簡單的北漂程式設計師。喜歡用簡單的文字記錄工作與生活中的點點滴滴,願與你一起分享程式設計師靈魂深處真正的內心獨白。我的微訊號:WooolaDunzung,公眾號【猿芯】輸入1024,有份驚喜送給你哦

< END >

【猿芯】微信掃描二維碼,關注我的公眾號。

來源:鄂爾多斯SEO