狗血的Mybatis文件路径问题org.apache.ibatis.binding.BindingException: Invalid bound statement not found
1、问题描述
今天学习Mybatis
运行程序的时候不出意外,出现异常了。
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.njust.mybatisplug.dao.CountryMapper.selectAll
at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:223)
at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:48)
at org.apache.ibatis.binding.MapperProxy.cachedMapperMethod(MapperProxy.java:59)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:52)
at com.sun.proxy.$Proxy5.selectAll(Unknown Source)
at com.njust.mybatisplug.dao.CountryMapperTest.testSelectAll(CountryMapperTest.java:47)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
嗯,错误显示statement
里面没有com.njust.mybatisplug.dao.CountryMapper.selectAll
。好的下面贴出我的源码和调试过程。
2、问题分析
首先总体看一下项目的结构吧。
很简单,就是Mybatis
的基本主键。下面将源码贴出。
Country .java
package com.njust.mybatisplug.model;
/**
* @author Chen
* @version 1.0
* @date 2020/4/18 9:52
* @description:
*/
public class Country {
private Long id;
private String countryname;
private String countrycode;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCountryname() {
return countryname;
}
public void setCountryname(String countryname) {
this.countryname = countryname;
}
public String getCountrycode() {
return countrycode;
}
public void setCountrycode(String countrycode) {
this.countrycode = countrycode;
}
@Override
public String toString() {
return "Country{" +
"id=" + id +
", countryname='" + countryname + '\'' +
", countrycode='" + countrycode + '\'' +
'}';
}
}
CountryMapper .java
package com.njust.mybatisplug.dao;
import com.njust.mybatisplug.model.Country;
import java.util.List;
public interface CountryMapper {
List<Country> selectAll();
}
CountryMapper .xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.njust.mybatisplug.dao.CountryMapper">
<select id="selectAll" resultType="com.njust.mybatisplug.model.Country">
select id, countryname, countrycode
from country
</select>
</mapper>
log4j.properties
log4j.rootLogger=ERROR, stdout
log4j.logger.tk.mybatis.simple.mapper=TRACE
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="logImpl" value="LOG4J"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<package name="com.njust.mybatisplug.model"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="" value=""/>
</transactionManager>
<dataSource type="UNPOOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.njust.mybatisplug.dao"/>
</mappers>
</configuration>
CountryMapperTest.java
package com.njust.mybatisplug.dao;
import com.njust.mybatisplug.model.Country;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.io.Reader;
import java.util.List;
/**
* @author Chen
* @version 1.0
* @date 2020/4/18 9:58
* @description:
*/
public class CountryMapperTest {
private static SqlSessionFactory sqlSessionFactory;
@BeforeClass
public static void init() {
try {
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
reader.close();
} catch (IOException ignore) {
ignore.printStackTrace();
}
}
public SqlSession getSqlSession() {
return sqlSessionFactory.openSession();
}
@Test
public void testSelectAll() {
SqlSession sqlSession = getSqlSession();
try {
CountryMapper countryMapper = sqlSession.getMapper(CountryMapper.class);
//调用 selectAll 方法查询所有用户
List<Country> userList = countryMapper.selectAll();
userList.forEach(System.out::println);
} finally {
//不要忘记关闭 sqlSession
sqlSession.close();
}
}
private void printCountryList(List<Country> countryList) {
for (Country country : countryList) {
System.out.printf("%-4d%4s%4s\n", country.getId(), country.getCountryname(), country.getCountrycode());
}
}
}
上述代码的逻辑很简单,只是将Country
的数据全部查询并打印出来。
3、问题解决
接下来,我们一步一步的debug
。老司机开车,上车请系好安全带!首先调试sqlSession
是否有问题。OK,在CountryMapper countryMapper = sqlSession.getMapper(CountryMapper.class);
这一行打个断点,便于查看。然后运行。如下图所示:
由于错误信息报的是statement
没有绑定,所以我们直接查找statement
相关信息即可。
根据debug
信息。我们发现sqlSession.configuration.mappedStatements
确实是size =0
。哎?不对啊,我们这些路径不是配的好好的吗?怎么没有呢?那我们就查看mappedStatements
的初始化实现,然后debug
到此处即可。实现源码如下:
public void parseStatementNode() {
//...省略其他代码
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
我们只要一步步debug
进来,查看十分初始化即可。OK,继续debug
。我们首先找到configuration
的初始化实现方法。我们这里主要是在init()
方法里面的new SqlSessionFactoryBuilder().build(reader);
实现configuration
的初始化,源码如下:
public static void init() {
try {
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
reader.close();
} catch (IOException ignore) {
ignore.printStackTrace();
}
}
直接进入build
方法。
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
这里调用了另一个build
函数,信息不大,继续进去。
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
嗯有点意思了,这里的初始化主要是在parser.parse()
方法里面,继续进入该方法。
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
这里的parseConfiguration(parser.evalNode("/configuration"));
方法实现了初始化,但是一眼也看不出来啊,继续进去。
private void parseConfiguration(XNode root) {
try {
Properties settings = settingsAsPropertiess(root.evalNode("settings"));
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
这里的mapperElement(root.evalNode("mappers"));
方法实现了初始化,但是一眼也看不出来啊,继续进去。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
这里的configuration.addMappers(mapperPackage);
方法实现了初始化,但是一眼也看不出来啊,继续进去。
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
这里的mapperRegistry.addMappers(packageName);
方法实现了初始化,但是一眼也看不出来啊,继续进去。
/**
* @since 3.2.2
*/
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
这里的addMappers(packageName, Object.class);
方法实现了初始化,但是一眼也看不出来啊,继续进去。
/**
* @since 3.2.2
*/
public void addMappers(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
这里的addMapper(mapperClass);
方法实现了初始化,但是一眼也看不出来啊,继续进去。
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);
}
}
}
}
这里的parser.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();
}
这里的loadXmlResource();
方法实现了初始化,但是一眼也看不出来啊,继续进去。
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();
}
}
}
这里已经到了事发地点了。什么鬼inputStream
怎么是null
呢?这里相当于没有读取到资源文件,再谈设置 mappedStatements
就没有意义了,肯定设置不了啊!看一下xmlResource
的路径。
在看一下我们项目中的路径。
咋一看确实是没有问题的。但是问题就出现在这个咋一看上面。我们进入项目的win
文件夹下面看看。
是不是有点奇怪,idea
中.
在win
中不是一个个文件吗?这里怎么是一个文件。是的。
错误就在这里,我们在idea中以.分隔的文件,在win中其实是一个文件夹,这里和java中的package不同。
我们修改一下文件。
再次运行项目如下:
这里的inputStream
就不是空了,有数值了。继续运行代码。我们发现可以给statement
赋值了。
再次查看mappedStatements
里面有值了。
最后程序运行结果如下:
Country{id=1, countryname='中国', countrycode='CN'}
Country{id=2, countryname='美国', countrycode='US'}
Country{id=3, countryname='俄罗斯', countrycode='RU'}
Country{id=4, countryname='英国', countrycode='GB'}
Country{id=5, countryname='法国', countrycode='FR'}
Disconnected from the target VM, address: '127.0.0.1:44522', transport: 'socket'
Process finished with exit code 0
成功输出结果。
最后我们看一下idea
显示的目录结构。
注意这是我改过win
下面的路径之后截的图,尼玛,这和我们上面报错的有什么区别呢?废了老子大半天时间。强行学一波Mybatis
源码。
4、总结
书上的代码直接运行绝大部分是对的,但是总有一些软件的更新使得作者无能为力。之前的API是对的,但是之后就废弃了或修改了是常有的事。所以我们需要跟踪源代码。这只是一个小小的问题,如果没有前辈的无私奉献,很难想象我们自己一天能学到多少内容。感谢各位前辈的辛勤付出,让我们少走了很多的弯路!
点个赞再走呗!欢迎留言哦!