代码改变世界

spring boot mybatis 报错Invalid bound statement (not found)解决过程

2020-05-09 16:35  全me村的希望  阅读(7913)  评论(1编辑  收藏  举报

  通过解决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 ,也同时失效。