通过解决Invalid bound statement (not found),剖析mybatis加载Mapper接口、Mapper.xml以及将两者绑定的过程。
项目刚开始使用了spring boot mybatis:
1.配置扫描mapper接口
@MapperScan({"com.hbfec.encrypt.mbg.mapper","com.hbfec.encrypt.admin.dao"})
2.在application.yml中配置Mapper.xml的扫描路径
mybatis: mapper-locations: - classpath:dao/**/*.xml - classpath*:com/**/mapper/*.xml
一切接口正常访问。
因为需要使用双数据源,自定义了DataSource SqlSessionFactory SqlSessionTemplate的bean,不再使用MybatisAutoConfiguration.class中默认的Bean
@Resource(name = "dsTwo") DataSource dsTwo;//DataSource 也为自定义 @Bean("sqlSessionFactory2") @Qualifier("sqlSessionFactory2") SqlSessionFactory sqlSessionFactory2() { SqlSessionFactory sessionFactory = null; try { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dsTwo); sessionFactory = bean.getObject(); } catch (Exception e) { e.printStackTrace(); } return sessionFactory; } @Bean("sqlSessionTemplate2") @Qualifier("sqlSessionTemplate2") SqlSessionTemplate sqlSessionTemplate2() { return new SqlSessionTemplate(sqlSessionFactory2()); }
项目启动后调用Mapper接口,部分接口正常访问,部分接口比如:TestDao.findAll,访问报一下错误:
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.hbfec.encrypt.admin.dao.ocr.TestDao.findAll at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:227) at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:49) at org.apache.ibatis.binding.MapperProxy.cachedMapperMethod(MapperProxy.java:65) at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:58) at com.sun.proxy.$Proxy118.findAll(Unknown Source) at com.hbfec.encrypt.admin.controller.TestController.findAll(TestController.java:24)
在网上试了各种手段都无法解决,只能去源码DEBUG了。
1.根据第一行错误信息:at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:227) 定位到MapperMethod的SqlCommand方法
发现是 MappedStatement ms == null的情况下抛出的异常,那么MappedStatement是什么呢?
MappedStatement对象对应Mapper.xml配置文件中的一个select/update/insert/delete节点,描述的就是一条SQL语句,所以可以猜测为对应的Mapper.xml文件没有找到。
ms对象从resolveMappedStatement方法中解析
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,configuration);
继续观察resolveMappedStatement为什么会返回null值?
发现configuration 对象(全局配置对象)里面的mappedStatements为空,没有加载到任何的Mapper,以下是Configuration类中定义的mappedStatements
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
找到这里就知道为什么会出现如上错误,TestDao接口对应的xml文件没有被正确加载或者没有找到接口对应的xml文件。
2.为什么mappedStatements值为空呢?configuration 对象什么时候进行初始化mappedStatements的?
我们知道一个MappedStatement对象对应一个mapper.xml中的一个SQL节点,而Mapper.xml文件是初始化Configuration对象的时候进行解析加载的,则说明MappedStatement对象就是在初始化Configuration对象的时候创建的。
所以找到初Configuration对象初始化MappedStatement的地方进行DEBUG;
Configuration对象中添加Mapper使用MapperRegistry类
public <T> void addMapper(Class<T> type) { mapperRegistry.addMapper(type); }
继续往下
public <T> void addMapper(Class<T> type) { if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { knownMappers.put(type, new MapperProxyFactory<T>(type)); // It's important that the type is added before the parser is run // otherwise the binding may automatically be attempted by the // mapper parser. If the type is already known, it won't try. MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }
红色部分为解析xml并初始化MappedStatement对象的代码。继续看看MapperAnnotationBuilder类的parse()方法:
public void parse() { String resource = type.toString(); if (!configuration.isResourceLoaded(resource)) { loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); parseCache(); parseCacheRef(); Method[] methods = type.getMethods(); for (Method method : methods) { try { // issue #237 if (!method.isBridge()) { parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods(); }
红色部分为加载Xml资源,继续看
private void loadXmlResource() { // Spring may not know the real resource name so we check a flag // to prevent loading again a resource twice // this flag is set at XMLMapperBuilder#bindMapperForNamespace if (!configuration.isResourceLoaded("namespace:" + type.getName())) { String xmlResource = type.getName().replace('.', '/') + ".xml"; InputStream inputStream = null; try { inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource); } catch (IOException e) { // ignore, resource is not required } if (inputStream != null) { XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName()); xmlParser.parse(); } } }
在这里寻找mapper接口对应的xml的资源路径的方式如下:
String xmlResource = type.getName().replace('.', '/') + ".xml";
替换接口包名中的.为/ 并在接口添加.xml后缀。
比如:Mapper接口com.hbfec.encrypt.admin.dao.ocr.TestDao的对应的xml资源路径会解析为com/hbfec/encrypt/admin/dao/ocr/TestDao.xml。
TestDao.xml在我的项目中的路径是classpath:dao/ocr/TestDao.xml,路径与上面解析出来的不一致,mybatis无法找到TestDao.xml,导致以上错误。所以项目采用使用这种方式绑定Mapper接口和Mapper.xml的话,其路径和名称都要一致。
解决办法有以下两种:
1.在resource下创建和TestDao接口所在目录一样的包路径,使TestDao.xml和TestDao接口的包路径一致。
2.在自定义的SqlSessionFactory中添加 bean.setMapperLocations(mybatisProperties.resolveMapperLocations()),使yml中的配置信息mapper-locations信息生效。
改造自定义的SqlSessionFactory如下:
@Configuration @ConditionalOnClass(SqlSessionFactoryBean.class) @MapperScan(basePackages = "com.hbfec.encrypt.admin.dao.ocr",sqlSessionFactoryRef = "sqlSessionFactory2",sqlSessionTemplateRef = "sqlSessionTemplate2") public class MyBatisConfigOcr { @Resource(name = "dsTwo") DataSource dsTwo; @Autowired MybatisProperties mybatisProperties; @Bean("sqlSessionFactory2") @Qualifier("sqlSessionFactory2") SqlSessionFactory sqlSessionFactory2() { SqlSessionFactory sessionFactory = null; try { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dsTwo); bean.setMapperLocations(mybatisProperties.resolveMapperLocations());//将mapper-locations的配置信息注入 sessionFactory = bean.getObject(); } catch (Exception e) { e.printStackTrace(); } return sessionFactory; } @Bean("sqlSessionTemplate2") @Qualifier("sqlSessionTemplate2") SqlSessionTemplate sqlSessionTemplate2() { return new SqlSessionTemplate(sqlSessionFactory2()); } }
问题1:为什么单数据源的时候classpath:dao 下面的Mapper.xml可以被正确加载
答:自定义的SqlSessionFactory导致MybatisAutoConfiguration中的SqlSessionFactory bean失效,该bean中使用了MybatisProperties从配置文件application.yml中加载的mybatis配置信息,比如mapperLocations ,也同时失效。