通過解決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 ,也同時失效。