最近在使用高版本Spring Boot 2.x
整合mybatis-plus 3.4.1
时,控制台出现大量的warn
提示XxxMapper
重复定义信息:Bean already defined with the same name
。
2020-12-07 19:37:26.025 WARN 25756 --- [ main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'roleMapper' and 'com.dunzung.java.spring.mapper.RoleMapper' mapperInterface. Bean already defined with the same name!
2020-12-07 19:37:26.025 WARN 25756 --- [ main] o.m.s.mapper.ClassPathMapperScanner : Skipping MapperFactoryBean with name 'userMapper' and 'com.dunzung.java.spring.mapper.UserMapper' mapperInterface. Bean already defined with the same name!
2
虽然这些警告并不影响程序正确运行,但是每次启动程序看到控制台输出这些警告日志信息,心情不是很美丽呀。
从Bean already defined with the same name
警告信息来看,感觉应该是:重复加载 mapper 的 bean 对象定义了。所以我从mybatis-plus
的pom
依赖入手,找到mybatis-plus
总共依赖三个 jar
包:
接着,看了下 mybatis-plus
启动相关配置,发现也没啥毛病。
@Configuration
@MapperScan(basePackages = "com.dunzung.**.mapper.**")
public class MybatisPlusConfiguration {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
paginationInterceptor.setDbType(DbType.MYSQL);
return paginationInterceptor;
}
}
自定义的MybatisServiceImpl
继承了mybatis-plus
的ServiceImpl
实现类;自定义的MybatisService
继承了IService
接口类。
/**
* 自定义 Service 接口基类
*/
public interface MybatisService<T> extends IService<T> {
}
public interface RoleService extends MybatisService<RoleEntity> {
}
/**
* 自定义 Service 实现接口基类
*/
public class MybatisServiceImpl<M extends DaoMapper<T>, T> extends ServiceImpl<M, T> implements MybatisService<T> {
}
@Slf4j
@Service
public class RoleServiceImpl extends MybatisServiceImpl<RoleMapper, RoleEntity> implements RoleService {
}
Mapper
类定义RoleMapper
基于注解@Mapper
配置,基本上零配置(xml
)。
@Mapper
public interface RoleMapper extends DaoMapper<RoleEntity> {
}
上面的 mybatis-plus
相关配置非常简单,没啥毛病,所以只能从 mybatis-plus
相关的三个jar
源码入手了。
从日志输出信息定位可以看出是o.m.s.mapper.ClassPathMapperScanner
打印的警告日志,于是在ClassPathMapperScanner
类中找到了输出警告日志的checkCandidate()
方法:
/**
* {@inheritDoc}
*/
@Override
protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) {
if (super.checkCandidate(beanName, beanDefinition)) {
return true;
} else {
LOGGER.warn(() -> "Skipping MapperFactoryBean with name '" + beanName + "' and '"
+ beanDefinition.getBeanClassName() + "' mapperInterface" + ". Bean already defined with the same name!");
return false;
}
}
}
打开Debug
模式,在ClassPathMapperScanner
的checkCandidate()
方法体打断点,验证该方法是否重复调用两次。
Spring Boot
程序启动时会自动装配mybatis-spring-boot-autoconfigure
这个jar
包中的MybatisAutoConfiguration
配置类,通过其内部类AutoConfiguredMapperScannerRegistrar
的registerBeanDefinitions()
注册bean
方法,调用了ClassPathMapperScanner
的doScan()
方法,然后通过checkCandidate()
方法判断mapper
对象是否已注册。doScan
方法详细代码如下:
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
...
for (String basePackage : basePackages) {
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
...
if (checkCandidate(beanName, candidate)) {
...
}
}
}
“
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
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(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 版本写法
/**
* This will just scan the same base package as Spring Boot does. If you want
* more power, you can explicitly use
* {@link org.mybatis.spring.annotation.MapperScan} but this will get typed
* mappers working correctly, out-of-the-box, similar to using Spring Data JPA
* repositories.
*/
public static class AutoConfiguredMapperScannerRegistrar
implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private BeanFactory beanFactory;
private ResourceLoader resourceLoader;
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
logger.debug("Searching for mappers annotated with @Mapper");
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
try {
if (this.resourceLoader != null) {
scanner.setResourceLoader(this.resourceLoader);
}
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (logger.isDebugEnabled()) {
for (String pkg : packages) {
logger.debug("Using auto-configuration base package '{}'", pkg);
}
}
scanner.setAnnotationClass(Mapper.class);
scanner.registerFilters();
scanner.doScan(StringUtils.toStringArray(packages));
} catch (IllegalStateException ex) {
logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.", ex);
}
}
}
/**
* {@link org.mybatis.spring.annotation.MapperScan} ultimately ends up
* creating instances of {@link MapperFactoryBean}. If
* {@link org.mybatis.spring.annotation.MapperScan} is used then this
* auto-configuration is not needed. If it is _not_ used, however, then this
* will bring in a bean registrar and automatically register components based
* on the same component-scanning path as Spring Boot itself.
*/
@org.springframework.context.annotation.Configuration
@Import({ AutoConfiguredMapperScannerRegistrar.class })
@ConditionalOnMissingBean(MapperFactoryBean.class)
public static class MapperScannerRegistrarNotFoundConfiguration {
@PostConstruct
public void afterPropertiesSet() {
logger.debug("No {} found.", MapperFactoryBean.class.getName());
}
}
}
mybatis-spring-boot-autoconfigure 的 2.1.3 版本写法
@Configuration
@Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
@ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
public MapperScannerRegistrarNotFoundConfiguration() {
}
public void afterPropertiesSet() {
MybatisAutoConfiguration.logger.debug("Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
}
}
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {
private BeanFactory beanFactory;
public AutoConfiguredMapperScannerRegistrar() {
}
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
if (!AutoConfigurationPackages.has(this.beanFactory)) {
MybatisAutoConfiguration.logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
} else {
MybatisAutoConfiguration.logger.debug("Searching for mappers annotated with @Mapper");
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (MybatisAutoConfiguration.logger.isDebugEnabled()) {
packages.forEach((pkg) -> {
MybatisAutoConfiguration.logger.debug("Using auto-configuration base package '{}'", pkg);
});
}
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);
builder.addPropertyValue("annotationClass", Mapper.class);
builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
Stream.of(beanWrapper.getPropertyDescriptors()).filter((x) -> {
return x.getName().equals("lazyInitialization");
}).findAny().ifPresent((x) -> {
builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}");
});
registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
}
}
public void setBeanFactory(BeanFactory beanFactory) {
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
对象所导致。
在实际的生产环境中,每次开源框架级别的升级,要特别注意框架所依赖的版本对应关系,最好的办法是去相关开源框架的官网了解具体的版本升级博客文章或升级日志,避免带来不必要的麻烦和损失。