Invalid bound statement 算是Mybaits中比较常见的一个异常了:
org.apache.ibatis.binding.bindingexception:
invalid bound statement (not found):
com.dhy.testMapper.query
这个异常不管具体什么原因导致,归根结底,就是mapper接口和对应的xml没有绑定成功。
mapper接口和xml没有绑定成功存在很多原因,但是大部分原因都比较容易发现,本节我想来分享一个不那么容易发现的原因:
假设我们有两个数据源,一个是DataSourceA,一个是DataSourceB,那么我们可以在Mybaits-Spring环境下这样配置:
@Bean
public SqlSessionFactory sqlSessionFactoryOfDataSourceA(@Qualifier("datasourceA") DataSource dataSourceA) throws Exception {
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSourceA);
sessionFactoryBean.setConfigLocation(configLocation);
sessionFactoryBean.setTypeAliasesPackage("com.dhy.mapper");
Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath:dhy/mapper/*.xml");
sessionFactoryBean.setMapperLocations(resources);
return sessionFactoryBean.getObject();
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurerOfDataSourceA() {
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
mapperScannerConfigurer.setSqlSessionFactoryBeanName("sqlSessionFactoryOfDataSourceA");
mapperScannerConfigurer.setBasePackage("com.dhy.mapper");
return mapperScannerConfigurer;
}
@Bean
public SqlSessionFactory sqlSessionFactoryOfDataSourceB(@Qualifier("datasourceB") DataSource dataSourceB) throws Exception {
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSourceB);
sessionFactoryBean.setConfigLocation(configLocation);
sessionFactoryBean.setTypeAliasesPackage("com.dhy.mapper.b");
Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath:dhy/mapper/*.xml");
sessionFactoryBean.setMapperLocations(resources);
return sessionFactoryBean.getObject();
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurerOfDataSourceB() {
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
mapperScannerConfigurer.setSqlSessionFactoryBeanName("sqlSessionFactoryOfDataSourceB");
mapperScannerConfigurer.setBasePackage("com.dhy.mapper.b");
return mapperScannerConfigurer;
}
通过上面的配置信息,我们可以观测到一点,那就是MapperScannerA和MapperScannerB扫描范围存在重叠,也就是MapperScannerA会把MapperScannerB的mapper接口也扫描进来:
其实默认情况下,mybaits-spring自身还会向IOC中自动注入一个MapperScanner,该mapper扫描器不同于我们上面配置的两个扫描器,它默认会基于SpringBoot自身包扫描范围开始,递归扫描并获取所有的标注了@Mapper注解的接口:
我们上面提供的两个扫描器是递归获取指定包路径下所有的类,不管是否加了@Mapper注解
/**
Auto-Configuration for Mybatis.
Contributes a SqlSessionFactory and a SqlSessionTemplate.
If org.mybatis.spring.annotation.MapperScan is used,
or a configuration file is specified as a property,
those will be considered,
otherwise this auto-configuration will attempt to register mappers
based on the interface definitions in or under the root auto-configuration package.
*/
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnBean(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration {
...
//如果我们不提供SqlSessionFactory,默认会自动注入一个
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
...
}
//如果我们不提供SqlSessionTemplate ,默认会自动注入一个
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
...
}
/**
MapperScan ultimately ends up creating instances of MapperFactoryBean.
If 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());
}
}
}
MybatisAutoConfiguration会在我们没有配置sqlSessionFactory和sqlSessionTemplate时,自动帮助我们注入一个,并且其内部的静态内部类MapperScannerRegistrarNotFoundConfiguration,会在我们没有使用MapperScan注解手动指定包扫描路径的情况下,帮我们导入一个AutoConfiguredMapperScannerRegistrar类,负责基于SpringBoot自身包扫描范围进行扫描。
/**
This will just scan the same base package as Spring Boot does.
If you want more power, you can explicitly use 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");
//mybaits提供的mapper扫描器
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
try {
if (this.resourceLoader != null) {
scanner.setResourceLoader(this.resourceLoader);
}
//获取SpringBoot当前项目的包扫描路径
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
...
//设置根据注解进行过滤
scanner.setAnnotationClass(Mapper.class);
//注册包扫描过程用用户过滤预期结果集合的Filter
//我们上面设置了注解后,这里就会被封装为AnnotationTypeFilter注册进扫描器中
scanner.registerFilters();
//包扫描
scanner.doScan(StringUtils.toStringArray(packages));
}...
}
...
}
调用mybaits提供的ClassPathMapperScanner类的scan方法完成包扫描过程:
ClassPathMapperScanner继承了ClassPathBeanDefinitionScanner,包扫描和根据Filter在扫描过程中进行过滤的能力都是Spring已经提供好了的。
/**
* Calls the parent search that will search and register all the candidates. Then the registered objects are post
* processed to set them as MapperFactoryBeans
*/
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
//调用ClassPathBeanDefinitionScanner的doScan方法,完成包扫描和Filter过滤
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 {
//mybaits负责进行bean定义处理
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
对于AutoConfiguredMapperScannerRegistrar来说,上面过滤得到的结果集合就是指定包路径集合下,所有加了@Mapper注解的类。
Mybaits在借助Spring的包扫描器完成过滤后,下一步就是对符合条件的BeanDefintion进行进一步处理:
包扫描得到的都是mapper接口,但是最终mybaits需要为这些接口生成代理对象,并且用户最终注入得到的Bean对象类型应该也是代理对象。 所以mybaits实际注入容器中的mapper类型为一个FactoryBean,即MapperFactoryBean,他的getObject方法中负责为当前mapper接口创建一个代理对象然后返回。
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
AbstractBeanDefinition definition;
BeanDefinitionRegistry registry = getRegistry();
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (AbstractBeanDefinition) holder.getBeanDefinition();
...
// the mapper interface is the original class of the bean
// but, the actual class of the bean is MapperFactoryBean
//为MapperFactoryBean添加构造器参数
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
//添加属性注入关系映射
definition.getPropertyValues().add("mapperInterface", Resources.classForName(beanClassName));
//设置当前bean实际类型为mapperFactoryBeanClass
definition.setBeanClass(this.mapperFactoryBeanClass);
//添加属性注入关系映射
definition.getPropertyValues().add("addToConfig", this.addToConfig);
...
//我们是否设置了ClassPathMapperScanner的sqlSessionFactoryBeanName属性
//设置了该属性表明,由当前扫描器扫描得到的mapper接口,最终都会交给这个sqlSessionFactory管理
boolean explicitFactoryUsed = false;
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
//如果设置了,那么添加属性映射
definition.getPropertyValues().add("sqlSessionFactory",
//RuntimeBeanReference在bean的属性注入阶段会被替换为实际的sqlSessionFactoryBean实例
new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
//或者我们直接指定了sqlSessionFactory对象实例,那么就可以将RuntimeBeanReference直接替换为实际的bean实例
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
//如果指定了sqlSessionTemplate也可以,处理同上
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
if (explicitFactoryUsed) {
LOGGER.warn(
() -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate",
new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
if (explicitFactoryUsed) {
LOGGER.warn(
() -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
//如果sqlSessionTemplate和sqlSessionFactory都没有指定过,那么设置支持自动注入
if (!explicitFactoryUsed) {
LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
//设置当前bean是否需要进行懒加载
definition.setLazyInit(lazyInitialization);
...
//作用域设置
if (ConfigurableBeanFactory.SCOPE_SINGLETON.equals(definition.getScope()) && defaultScope != null) {
definition.setScope(defaultScope);
}
...
}
}
processBeanDefinitions方法中主要是为当前MapperFactoryBean指定好了相关的依赖注入映射关系,相当于手动操作propertyValues指定。
除了手动编码指定,我们还可以通过以下方式指定依赖注入映射关系:
还有一点需要大家注意,就是如果我们没有为当前ClassPathMapperScanner指定sqlSessionFactory或者sqlSessionTemplate的beanName或者bean实例,那么扫描器会为当前bean开启按照类型的自动注入:
什么叫自动注入?
自动注入就是说,我们在没有通过上述方式手动指定依赖注入映射关系的情况下,spring会自动尝试为我们当前bean对象进行依赖注入:
上述过程的逻辑体现在AbstractAutowireCapableBeanFactory的populateBean方法中,也就是bean的属性注入阶段:
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
...
//获取beanDefintion中已有的PropertyValues集合---集合中可能存在依赖注入映射关系了
//例如: 我们通过xml配置了当前bean,或者像上面那样手动操作PropertyValues集合
PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);
//获取当前bean的自动注入模式--默认是不开启的
int resolvedAutowireMode = mbd.getResolvedAutowireMode();
//如果当前bean开启了自动注入模式,那么判断是按照名称注入,还是按照类型注入
//例如: 如果开启了自动注入模式,并且按照类型自动注入,那么spring会尝试为当前bean每个属性都进行自动注入
//如果某几个属性可以按照类型自动注入成功,那么就创建好对应的依赖注入映射关系---propertyValue
//然后把propertyValue加入当前bean已有的PropertyValues集合
if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
// Add property values based on autowire by name if applicable.
if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}
// Add property values based on autowire by type if applicable.
if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
autowireByType(beanName, mbd, bw, newPvs);
}
pvs = newPvs;
}
//后置处理器处理注解方式指定的依赖关系映射注入逻辑省略
...
//最终使用BeanWrapper配合spring的类型转换模块
//根据PropertyValues集合中保存的依赖注入映射关系,完成基于setter方法的依赖注入
if (pvs != null) {
applyPropertyValues(beanName, mbd, bw, pvs);
}
}
上面说过,在没有为ClassPathMapperScanner指定sqlSessionTemplate或sqlSessionFactory的情况下,ClassPathMapperScanner会对其扫描到的每个MapperFactoryBean开启按照类型的自动注入模式:
当然,发生这个问题的前提是自动注入的ClassPathMapperScanner扫描到了标注有@Mapper注解的mapper接口。
解决这个问题有两个思路:
@MapperScan注解背后的实现可以参考补充说明2
当然,一般情况下发生这个问题,可以通过在其中一个Bean上标注@Primary注解,告诉Spring在出现多个候选bean的情况下,优先选择标注有@Primary注解的Bean。
在多数据源配置场景下,我们可以同时配置多个ClassPathMapperScanner和SqlSessionFactory,负责扫描不同包路径下的mapper接口。
不同包路径下的mapper接口分别属于不同的数据源,所以交给不同的SqlSessionFactory管理:
如上图所示,扫描器A和扫描器B分别扫描不同的包路径,但是扫描器A扫描的路径覆盖了扫描器B的路径,并且由于扫描器A优先于扫描器B执行,所以会导致扫描器B扫描不到对应包下mapper接口,这是为什么呢?
这是因为ClassPathBeanDefinitionScanner的doScan方法中,会将每个扫描器扫描得到的bean都进行注册,如果当前扫描器扫描到的某个bean已经存在于容器中了,那么当前扫描器则会跳过不进行处理:
/**
Check the given candidate's bean name, determining whether the corresponding bean definition needs to be registered or conflicts with an existing definition.
*/
protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException {
if (!this.registry.containsBeanDefinition(beanName)) {
return true;
}
BeanDefinition existingDef = this.registry.getBeanDefinition(beanName);
BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition();
if (originatingDef != null) {
existingDef = originatingDef;
}
//两个bean的定义是相同的,即相互兼容的
if (isCompatible(beanDefinition, existingDef)) {
return false;
}
//如果两个bean是同名的,但是bean定义不兼容,则会抛出异常,报告出现了一个beanName映射到两个不同bean上的错误
throw new ConflictingBeanDefinitionException("Annotation-specified bean name '" + beanName +
"' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " +
"non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]");
}
解决我们上面遇到的这个问题,有两个思路:
在@Bean方法上标注@Order注解影响的是Bean的加载顺序,不要搞错!
思路2就是让扫描器A扫描的路径和扫描器B扫描的路径不产生重叠:
同时注意关闭默认自动注入的扫描器,或者不在mapper接口上标注@Mapper注解即可
MapperScannerConfigurer本质是一个工厂Bean后置处理器,用于在BeanFactory prepare初始化完成后,向容器中额外添加一些BeanDefinition。
其核心方法是postProcessBeanDefinitionRegistry:
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
//根据我们向MapperScannerConfigurer设置的值,再设置到ClassPathMapperScanner 中
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);
scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
if (StringUtils.hasText(lazyInitialization)) {
scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
}
if (StringUtils.hasText(defaultScope)) {
scanner.setDefaultScope(defaultScope);
}
//注册Filter,然后启动包扫描,包扫描路径就是我们在@Bean方法中设置的
scanner.registerFilters();
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
MapperScan注解最大的作用就是向容器中导入了一个MapperScannerRegistrar,MapperScannerRegistrar继承了ImportBeanDefinitionRegistrar接口,负责向容器中注册额外的beanDefintion。
而MapperScannerRegistrar向容器中注册的bean类型不是别的,就是上面介绍的MapperScannerConfigurer,MapperScan注解中的属性和MapperScannerConfigurer配置类中的属性一一对应:
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//取出注解中所有属性
AnnotationAttributes mapperScanAttrs = AnnotationAttributes
.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
//注册MapperScannerConfigurer
if (mapperScanAttrs != null) {
registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
generateBaseBeanName(importingClassMetadata, 0));
}
}
void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
BeanDefinitionRegistry registry, String beanName) {
// 要注册的bean类型为MapperScannerConfigurer
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);
//如果指定了包扫描的同时按照注解进行过滤那就进行设置,默认是会将指定包下所有接口都搜集起来
Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
if (!Annotation.class.equals(annotationClass)) {
builder.addPropertyValue("annotationClass", annotationClass);
}
Class<?> markerInterface = annoAttrs.getClass("markerInterface");
if (!Class.class.equals(markerInterface)) {
builder.addPropertyValue("markerInterface", markerInterface);
}
Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
if (!BeanNameGenerator.class.equals(generatorClass)) {
builder.addPropertyValue("nameGenerator", BeanUtils.instantiateClass(generatorClass));
}
Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
builder.addPropertyValue("mapperFactoryBeanClass", mapperFactoryBeanClass);
}
String sqlSessionTemplateRef = annoAttrs.getString("sqlSessionTemplateRef");
if (StringUtils.hasText(sqlSessionTemplateRef)) {
builder.addPropertyValue("sqlSessionTemplateBeanName", annoAttrs.getString("sqlSessionTemplateRef"));
}
String sqlSessionFactoryRef = annoAttrs.getString("sqlSessionFactoryRef");
if (StringUtils.hasText(sqlSessionFactoryRef)) {
builder.addPropertyValue("sqlSessionFactoryBeanName", annoAttrs.getString("sqlSessionFactoryRef"));
}
//是否在注解中指定了包扫描路径
List<String> basePackages = new ArrayList<>();
basePackages.addAll(
Arrays.stream(annoAttrs.getStringArray("value")).filter(StringUtils::hasText).collect(Collectors.toList())); basePackages.addAll(Arrays.stream(annoAttrs.getStringArray("basePackages")).filter(StringUtils::hasText)
.collect(Collectors.toList()));
basePackages.addAll(Arrays.stream(annoAttrs.getClassArray("basePackageClasses")).map(ClassUtils::getPackageName)
.collect(Collectors.toList()));
//没有指定包扫描路径,则选取默认包扫描路径
if (basePackages.isEmpty()) {
basePackages.add(getDefaultBasePackage(annoMeta));
}
String lazyInitialization = annoAttrs.getString("lazyInitialization");
if (StringUtils.hasText(lazyInitialization)) {
builder.addPropertyValue("lazyInitialization", lazyInitialization);
}
String defaultScope = annoAttrs.getString("defaultScope");
if (!AbstractBeanDefinition.SCOPE_DEFAULT.equals(defaultScope)) {
builder.addPropertyValue("defaultScope", defaultScope);
}
builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));
// for spring-native
builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
private static String generateBaseBeanName(AnnotationMetadata importingClassMetadata, int index) {
return importingClassMetadata.getClassName() + "#" + MapperScannerRegistrar.class.getSimpleName() + "#" + index;
}
private static String getDefaultBasePackage(AnnotationMetadata importingClassMetadata) {
return ClassUtils.getPackageName(importingClassMetadata.getClassName());
}