Skip to content

devtools

isea533 edited this page Apr 25, 2018 · 2 revisions

Spring Boot Devtools 兼容性分析

最早在 Spring Boot 中使用 Devtools 时,使用通用 Mapper 会报错: can't cast x.y.Z to x.y.Z , 包名和类名完全一样,为什么会出现这个错误呢?

最早有人通过 issue 提出了一个解决方案,就是增加下面这个配置(spring-devtools.properties):

restart.include.mapper=/mapper-[\\w-\\.]+jar

在一段时间内,这个配置解决了前面遇到的问题,但是 4.0 版本发布后,又发现有人遇到了类似问题。

后来发现 core 下面并没有 spring-devtools.properties 配置文件,我还以为缺了,还在 4.0.2 中提了这个, 实际上这个文件一直都没少,在 spring 子模块中一直都有这个文件,而且放在 spring 子模块也是最合适的位置。

为什么一直都有这个配置,还会出错呢?

实际上在后来发生的下面两个症状很可能都是 Devtools 导致的:

  • Error invoking SqlProvider method (tk.mybatis.mapper.provider.SpecialProvider.dynamicSQL)
  • java.lang.NoSuchMethodException: tk.mybatis.mapper.provider.SpecialProvider.<init>()

在 faq 中专门介绍了这两个错误的解决办法,但是一直没有完全处理所有的情况。

并且由于 Devtools 导致通用 Mapper 无法正常启动,因此也很难出现 can't cast x.y.Z to x.y.Z

为什么会出现 can't cast x.y.Z to x.y.Z

首先 Devtools 是应用于开发时的,也就是在 IDE 中运行时,当使用 jar 方式运行时,就会自动禁用 Devtools 工具。

在 IDE 中运行时,Devtools 会通过独立的类加载器来加载会发生变化的资源,通常是 IDE 各个模块的 classes 目录。 对于通过依赖导入的 jar 包是不会自动更新的。

这两部分内容分别由 RestartClassLoader 和 AppClassLoader 加载的,并且前者的 parent 是后者, 也就是说 AppClassLoader 加载的类可以在 RestartClassLoader 加载的类中使用, 但是 RestartClassLoader 加载的类不能在 AppClassLoader 中使用。

当由不同的类加载器加载 x.y.Z 类时,他们算不同的类,无法互相转换。

由于我自始至终没有复现出这个问题,所以这里猜测下原因:

有可能是 Spring Boot 同时加载了 Jar 中的 x.y.Z,还加在了 IDE 中 classes 中的 x.y.Z。 当 Jvm 中处理类型时,classes 中 x.y.Z 产生的实例,在找类型时优先从父类加载器查找,因此产生了这个问题。

如果有人能够复现这个问题,希望可以给我发一份,或者谁能验证一下这个问题。

为什么前期增加 spring-devtools.properties 后解决了这个问题?

没有增加时,因为 IDE 运行时,通用 Mapper 的 mapper-x.x.x.jar 会被 AppClassLoader 加载, 因此会出现 can't cast x.y.Z to x.y.Z

当增加上面的配置后,mapper-x.x.x.jar 会被 RestartClassLoader 加载,此时他们就一致了, 表面上解决了这个问题。

为什么后来会导致通用 Mapper 无法初始化?

就是因为这个配置导致的。

前面使用时,有可能你所有的 XXMapper 都在当前的 IDE 中,因此这些 XXMapper 都会使用 RestartClassLoader 加载。

但是如果你依赖的项目(包含 XXMapper)不在当前的 IDE 中,那么该 XXMapper 就会使用 AppClassLoader 加载。

因此就会出现 AppClassLoader 加载的 XXMapper 获取 RestartClassLoader 加载的 @RegisterMapper 注解时, 就会找不到,因此该 XXMapper 就无法初始化,调用 XXMapper 的方法时就会报下面的错:

  • Error invoking SqlProvider method (tk.mybatis.mapper.provider.SpecialProvider.dynamicSQL)

同样 XXMapper 对应 XX 使用 Example(XX.class) 时,就报错找不到 XX 对应的表名。

合理的情况

最合理的情况就是没有 spring-devtools.properties 配置文件,就算有,也应该是下面的配置:

restart.exclude.mapper=/mapper-[\\w-\\.]+jar

不能让 RestartClassLoader 加载 mapper-x.x.x.jar,这才是应该有的样子。

对于遇到 can't cast x.y.Z to x.y.Z,这应该是依赖问题导致的,也有可能是低版本 Spring Boot 导致的。

由于对该问题的解决办法是基于前面的猜测,因此未必就是这个原因。

如何判断此问题?

如果你有幸遇到了 can't cast x.y.Z to x.y.Z,这里有一个办法可以印证上面的猜测

首先 DEBUG 启动项目,出现该错误后,在代码中可运行到的某个地方断点,操作进入该断点。

断点停止的时候,通过下面的操作来查找问题。

  1. 使用 IDEA 的 F8 或者 Watches,或者 Eclipse 的变量表达式进行下面的操作
  2. 输入 getClass().getClassLoader() 查看当前是哪个 ClassLoader
  3. 一般断点在 target/classes 下面存在的类中时,应该是 RestartClassLoader,建议在这种类断点。
  4. getClass().getClassLoader() 获取到 RestartClassLoader 后,继续输入 getClass().getClassLoader().findClass("x.y.Z") 获取