与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 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

服务推荐

猜你喜欢

转载自blog.csdn.net/weixin_44476888/article/details/89604208