最近项目开了一个新的服务端,一个纯新的模块,使用的技术没有太特别的地方,Spring Boot 来快速搭建的SSM。本文就是在这次搭建的过程中,因为时间紧「求快」,结果各种折腾,反而费了更多时间。现整理出来记录下。
一、搭环境
使用 Spring Boot, 直接从 start.spring.io开始,添加各种依赖。一路还比较顺利。由于用到了 MyBatis, 直接把原来项目里通过 generator生成 Mapper的配置文件都拷了过来。这就是熬夜的开始呀。) _ ( …
-
拷过来之后,简单改了改配置,生成了Mapper.xml 和对应的 Mapper interface。
-
项目里的「日志」配置,也是从原来的项目里拷过来的。
-
application.properties文件中增加关于 MyBatis mapper文件解析位置
-
手写一个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 getMapper(Class 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 mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
问题解决了,过程还是太迂回了。
总结起来,应用开发中,有几点还是要谨记,
日志要尽早配置好
发车别太快了,开稳点
出问题了从源头出发,仔细分析
不好使的时候,看源码
三天不练手生,多总结
原文链接
https://mp.weixin.qq.com/s/_gqyBICXUcJJueDIyKTzmA