与MyBatis缠斗的几个小时...
最近项目开了一个新的服务端,一个纯新的模块,使用的技术没有太特别的地方,Spring Boot 来快速搭建的SSM。本文就是在这次搭建的过程中,因为时间紧「求快」,结果各种折腾,反而费了更多时间。现整理出来记录下。
一、搭环境
使用 Spring Boot, 直接从 start.spring.io开始,添加各种依赖。一路还比较顺利。由于用到了 MyBatis, 直接把原来项目里通过 generator生成 Mapper的配置文件都拷了过来。这就是熬夜的开始呀。) _ ( ...
1. 拷过来之后,简单改了改配置,生成了Mapper.xml 和对应的 Mapper interface。
2. 项目里的「日志」配置,也是从原来的项目里拷过来的。
3. application.properties文件中增加关于 MyBatis mapper文件解析位置
4. 手写一个Controller 来验证整体的功能
此时请求可以正常到达 Controller (这是最基本的嘛),在 dal 查库时,Mapper的 查询方法总是会报错,提示(org.apache.ibatis.binding.BindingException: Invalid bound statement (not found...)
看到异常,第一反应是MyBatis没有添加Binding成功,最直接原因应该是没找到 mapper.xml。此时,开始手工在datasource中添加mapperLocation。
重试一次,不成功。
使用注解形式的 Mapper,重试,成功。一脸懵... 后台没有错误日志。
因为generator生成的Mapper java文件和 xml文件,看着也都符合预期,这个时候,开始顺着 Mapper.xml 为啥没有被正确解析,后台日志没有输出没有考虑,被忽视了。
隐约记得几年前看过的MyBatis源码,对于 XML 配置的解析,mapper的注册这些内容。没有具体的总结,记忆不深刻。现如今具体问题在哪呢?只能大概跟一遍,尝试着在几个可能的地方加断点。
二、开始Debug
既然报错,但又没具体的异常信息。只能在调用Mapper的地方跟进去看了。
mapper的实际CRUD代码执行时,实际调用的是一个mapperProxy, 为什么mapper转成了Proxy, 我们后面再说。我们来看由于autowire的就是一个mapperProxy,调用mapper就直接进入proxy的 invoke 方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
这里默认的methodCache是空的,因此方法初次调用时会生成MapperMethod
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
而对应的MapperMethod,实际创建时,会生成一个SqlCommand,代表具体要执行的SQL命令。
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, method);
}
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) throws BindingException {
String statementName = mapperInterface.getName() + "." + method.getName();
MappedStatement ms = null;
if (configuration.hasStatement(statementName)) {
ms = configuration.getMappedStatement(statementName);
} else if (!mapperInterface.equals(method.getDeclaringClass().getName())) { // issue #35
String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
if (configuration.hasStatement(parentStatementName)) {
ms = configuration.getMappedStatement(parentStatementName);
}
}
if (ms == null) {
throw new BindingException("Invalid bound statement (not found): " + statementName);
}
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
此时看到这个异常是不是很惊喜
再向上看,为什么这里的MappedStatement会为空呢?我们生成的 XXXMapper 的 Java 文件里明明是有的。
在处理MappedStatement地方再跟,会发现这些内容,是由我们定义的注解,或者XML的Mapper来生成的。每个在Spring 中对应不同的Bean,我们来看在生成 Spring 的 Bean的时候, 是怎么处理XML 文件的,从而导致其没有成功。
三、什么时候变成Proxy的?
应用中的Service 一般会 AutoWire 具体的Mapper, 此时在 CreateBean的过程中,Spring 会getMapper这个Bean,没有时会创建Bean,此时对于Bean的添加,实际上是添加到了一个名为「MapperRegistry」的注册处,后续对于Mapper的添加,获取都从注册处来了解。
对于 Mapper,在add的时候,添加到已知的Mapper里的,是一个MapperProxyFactory,所以在获取的时候,直接是通过ProxyFactory的newInstance生成了一个MapperProxy的实例返回了。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
四、问题出在哪里呢?
在 Spring 创建这些Mapper的时候,对于 XML配置的 Mapper,就会通过XMLMapperBuilder来进行解析,重点的解析代码如下:
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
解析出来的Mapper内容以一个XNode传了进来。
那这个时候,如果XML里的内容「有问题」,导致添加失败,理论上是会打印日志出来的,但巧的是,我们的日志由于是拷过来的,没有正确配置,所以日志也没打出来,所以每次请求到Mapper里对应的方法时,都会提示错误。
那XML又为什么有问题呢? 也是因为拷原项目,然后把其中生成Entity的地方的packageName改了,但是对于Mapper.xml遗漏了,这个时候问题就出现了。
所以,最终发现并解决问题,是通过跟到源码中来查看解决,大费周折。
如果日志配置没问题,也可以通过日志更快的定位问题。
如果能仔细的处理generatorConfig的配置,也不会有这个问题。
如果我们在执行过程中出现了Bind Error, 一般都是在这里由于配置原因,没在注册处报到过导致。
另外,对于使用XML方式的配置,如果在 Mapper interface里增加了方法,在XML里没有同步包含,也是会报这个错的。
我们前面说每个Mapper的调用,实际是请求了一个MapperProxy。而这个MapperProxy, 则代理了真实的 Mapper interface里的方法。
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
实际创建的MapperProxy
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
问题解决了,过程还是太迂回了。
总结起来,应用开发中,有几点还是要谨记,
日志要尽早配置好
发车别太快了,开稳点
出问题了从源头出发,仔细分析
不好使的时候,看源码
三天不练手生,多总结
你有一个问题解决经验,我有一个问题处理经验,我们一分享,就每人有两个问题解决办法了。
为此,我创建了一个「Java工作实战群」,在群里分享工作中各种踩过的坑,走的弯路,欢迎大家加入一起分享。
如果看到时二维码已过期,添加个人微信:chainhou,拉你进群
相关阅读:
觉得本文对你有帮助?请分享给更多人支持一下吧,谢谢
关注『 Tomcat那些事儿 』 ,发现更多精彩文章!了解各种常见问题背后的原理与答案。深入源码,分析细节,内容原创,欢迎关注。
加入知识星球,一起进步
更多精彩内容:
一台机器上安装多个Tomcat 的原理(回复001)
监控Tomcat中的各种数据 (回复002)
启动Tomcat的安全机制(回复003)
乱码问题的原理及解决方式(回复007)
Tomcat 日志工作原理及配置(回复011)
web.xml 解析实现(回复 012)
线程池的原理( 回复 014)
Tomcat 的集群搭建原理与实现 (回复 015)
类加载器的原理 (回复 016)
类找不到等问题 (回复 017)
代码的热替换实现(回复 018)
Tomcat 进程自动退出问题 (回复 019)
为什么总是返回404? (回复 020)
...
PS: 对于一些 Tomcat常见问题,在公众号的【常见问题】菜单中,有需要的朋友欢迎关注查看。