Mybatis里@Mapper与@MapperScan的陷阱
起因
项目里有实习生讨论为何他使用@mapper注解,但是mapper却找不到的问题。下面进行分析。
环境
项目是采用SpringBoot,Mybatis-SpringBoot-Starter等库。
使用区别
方法一:使用@Mapper 直接在每个对应的Mapper类上面添加注解@Mapper。Mapper类较多时,这样使用比较麻烦。
@Mapper
public interface IssueMapper {
/**
* 查询试题
*
* @param issueId 试题ID
* @return 试题
*/
public Issue selectIssueById(Long issueId);
}
方法二:使用扫描注解@MapperScan
在每个模块对应的启动类上添加注解@MapperScan("com.example..mapper") 需要指明扫描包的范围,可以扫描1个或多个包,也可以扫描子包,需要通过通配符*或者来指定。
@MapperScan("com.example.issue.mapper")
@SpringCloudApplication
public class ServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceApplication.class, args);
}
}
问题分析
该实习生写的mapper类放在了自定义的包下,类上加了@mapper注解,但该类且不在@mapperScan的扫描范围,这个会有什么问题呢?
@MapperScan原理分析
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
}
里面有一个@Import(MapperScannerRegistrar.class),实现了ImportBeanDefinitionRegistrar,可以在springBoot启动时,向容器内注册bean。 具体如下
void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
BeanDefinitionRegistry registry, String beanName) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);
...
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
向容器里注册了 MapperScannerConfigurer 继承了 BeanDefinitionRegistryPostProcessor -> 执行 ClassPathMapperScanner
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
// 配置扫描类的过滤
public void registerFilters() {
}
@Override
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;
}
// 对扫描到的mapper进行处理
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
AbstractBeanDefinition definition;
BeanDefinitionRegistry registry = getRegistry();
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (AbstractBeanDefinition) holder.getBeanDefinition();
...
// 设置bean的class为mapperFactoryBean,由工厂bean来生成bean
definition.setBeanClass(this.mapperFactoryBeanClass);
...
// 非单例,立即注册到容器
if (!definition.isSingleton()) {
BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(holder, registry, true);
if (registry.containsBeanDefinition(proxyHolder.getBeanName())) {
// 去重
registry.removeBeanDefinition(proxyHolder.getBeanName());
}
registry.registerBeanDefinition(proxyHolder.getBeanName(), proxyHolder.getBeanDefinition());
}
}
}
/**
* {@inheritDoc}
*/
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
// 是接口且是 类是独立的,即它是顶级类/嵌套类(静态内部类),可以独立于封闭类构造,才能作为扫描备选
return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
}
}
@Mapper 原理分析
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {
@org.springframework.context.annotation.Configuration
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
}
}
此类为Mybatis的自动配置类,会自动配置 SqlSessionFactory, SqlSessionTemplate等类到容器,同时当满足 @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class }) ,及容器内无 MapperScannerConfigurer,MapperFactoryBean 时会自动配置@Import(AutoConfiguredMapperScannerRegistrar.class) 导入
AutoConfiguredMapperScannerRegistrar, 此类会手动注册 MapperScannerConfigurer 到容器,但与 mapperScan不同的是 annotationClass 为 Mapper.class
public static class AutoConfiguredMapperScannerRegistrar
implements BeanFactoryAware, EnvironmentAware, ImportBeanDefinitionRegistrar {
private BeanFactory beanFactory;
private Environment environment;
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
if (!AutoConfigurationPackages.has(this.beanFactory)) {
logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
return;
}
logger.debug("Searching for mappers annotated with @Mapper");
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (logger.isDebugEnabled()) {
packages.forEach(pkg -> 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);
Set<String> propertyNames = Stream.of(beanWrapper.getPropertyDescriptors()).map(PropertyDescriptor::getName)
.collect(Collectors.toSet());
if (propertyNames.contains("lazyInitialization")) {
// Need to mybatis-spring 2.0.2+
builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}");
}
if (propertyNames.contains("defaultScope")) {
// Need to mybatis-spring 2.0.6+
builder.addPropertyValue("defaultScope", "${mybatis.mapper-default-scope:}");
}
// for spring-native
boolean injectSqlSession = environment.getProperty("mybatis.inject-sql-session-on-mapper-scan", Boolean.class,
Boolean.TRUE);
if (injectSqlSession && this.beanFactory instanceof ListableBeanFactory) {
ListableBeanFactory listableBeanFactory = (ListableBeanFactory) this.beanFactory;
Optional<String> sqlSessionTemplateBeanName = Optional
.ofNullable(getBeanNameForType(SqlSessionTemplate.class, listableBeanFactory));
Optional<String> sqlSessionFactoryBeanName = Optional
.ofNullable(getBeanNameForType(SqlSessionFactory.class, listableBeanFactory));
if (sqlSessionTemplateBeanName.isPresent() || !sqlSessionFactoryBeanName.isPresent()) {
builder.addPropertyValue("sqlSessionTemplateBeanName",
sqlSessionTemplateBeanName.orElse("sqlSessionTemplate"));
} else {
builder.addPropertyValue("sqlSessionFactoryBeanName", sqlSessionFactoryBeanName.get());
}
}
builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
}
}
总结
由此可以看出 @Mapper和@MapperScan是互斥的。 在springboot环境下,
- 同时配置了@MapperScan和@Mapper,就会造成@Mapper不生效,因为
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })条件不满足 - 单独配置@Mapper可以生效
- 单独配置@MapperScan也可以生效