Spring Boot Devtools 兼容性分析

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

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

  1. 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

详细分析看这里:https://github.com/abel533/mapper-cast-exception

为什么前期增加 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 配置文件。

七、常见问题

1. mappers 参数导致的各种错误

主要针对 Mapper 3.x 版本,升级到 4.x 能解决常见的 mappers 参数问题

常见症状:

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

出现这个问题的原因是因为你用到了 tk.mybatis.mapper.common.Mapper<T> 之外的其他接口,例如:

  • MySqlMapper
  • SqlServerMapper
  • InsertListMapper
  • InsertUseGeneratedKeysMapper

当你没有配置 mappers 参数的时候,通用 Mapper 只会解析 tk.mybatis.mapper.common.Mapper<T> 中提供的方法,所以你使用不包含在内的其他方法时就会报错。

通常在使用接口时,都会创建自己的 BaseMapper<T> 接口,例如:

  1. package x.y.z;
  2. //import ...
  3. public interface BaseMapper<T> extends Mapper<T>, MySqlMapper<T> {
  4. }

这种情况下,如果你不正确配置 mappers 参数,你就无法使用 MySqlMapper<T> 中提供的几个方法。

配置方法如下

在使用 Spring XML 方式配置时,配置如下:

  1. <bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
  2. <property name="basePackage" value="tk.mybatis.mapper.xml"/>
  3. <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
  4. <property name="properties">
  5. <value>
  6. mappers=x.y.z.BaseMapper
  7. </value>
  8. </property>
  9. </bean>

想要配置其他属性时,一行配置一个 key=value 即可。

在 Spring Boot 中,可以直接在配置文件中按照下面方式指定:

  1. mapper.mappers=x.y.z.BaseMapper

1.集成通用 Mapper 文档中有很多种集成方式,文档中详细介绍了如何进行配置。

1.1 使用 Devtools 时也会引发上面的问题

由于 Devtools 使用不同的类加载器实现的动态加载,经过测试发现 Spring Boot 使用 Devtools 的时候,@RegisterMapper 等类会使用 RestarClassLoader 进行加载,而 AppClassLoader 会加载不需要重启的类,
而前者的 parent 是后者,这就导致 AppClassLoader 加载的 XXMapper ,在初始化时,由于获取不到另一个类加载器加载的注解类,导致获取失败,因而导致了初始化失败,后续方法都无法调用(包括Example创建)。

目前的解决版本就是先禁用 Devtools,后续更新会尝试解决该问题。

该问题影响的版本为:4.0.0~4.0.2。

2. Example 无法获取实体类xxx.Yyy对应的表名!

常见异常:

  • java.lang.RuntimeException: 无法获取实体类xxx.Yyy对应的表名!`

出现这个问题的原因有两种。

第一种情况通常是通用 Mapper 没有配置好,没有对 MyBatis 方法初始化造成的。常见的判断方法就是看看其他非 Example 的通用方法能否执行,如果不能执行就是配置的问题。如果能正常执行,可能就是第二种情况。

第二种情况就是调用方法的位置或者时机早于通用 Mapper 初始化,这种情况下可以通过升级到 4.x 版本解决,4.x 版本的所有配置方式理论上都是在 MyBatis 初始化的同时进行的,也就是在你能使用 MyBatis 的时候,通用 Mapper 就已经准备好了。如果还存在这种问题,就可以使用 XML 配置方式中的 Configuration 方式,参考这里XML 配置使用 Configuration。Spring Boot 参考下面的代码:

https://github.com/abel533/Mapper/blob/819ad48389d1766a6a8304aae282a58be9132708/spring/src/test/java/tk/mybatis/mapper/annotation/SpringAnnotationTest.java#L191-L197

3. Spring Boot 中注解注意事项

如果你使用了 @tk.xxx.MapperScan 注解(包名必填),通用 Mapper 就会自动处理所有通用方法。

如果不使用该注解,你没有别的办法设置包名,所以通用 Mapper 就无法判断哪些接口属于 DAO 层,因此你需要给所有的 Mapper 接口增加 @org.xxx.Mapper 注解,否则 MyBatis 就扫描不到任何接口。

4. 集成 Activiti BPM 出错

通过查找依赖发现 activiti 中存在下面的依赖:

  1. <dependency>
  2. <groupId>org.hibernate.javax.persistence</groupId>
  3. <artifactId>hibernate-jpa-2.1-api</artifactId>
  4. <version>x.x.x</version>
  5. </dependency>

这个依赖和 Mapper 中的 JPA 依赖有冲突:

  1. <dependency>
  2. <groupId>javax.persistence</groupId>
  3. <artifactId>persistence-api</artifactId>
  4. <version>x.x.x</version>
  5. </dependency>

按照类加载顺序,persistence-api-1.0.jar 会更早的加载,因此会出错。

解决办法就是排除 Mapper 自带的这个依赖,使用 hibernate 这个 JPA 就能解决。

排除方式如下:

  1. <dependency>
  2. <groupId>tk.mybatis</groupId>
  3. <artifactId>mapper</artifactId>
  4. <version>x.x.x</version>
  5. <exclusions>
  6. <exclusion>
  7. <artifactId>persistence-api</artifactId>
  8. <groupId>javax.persistence</groupId>
  9. </exclusion>
  10. </exclusions>
  11. </dependency>

5. mysql的驱动版本导致的mapper-generator生成实体类的@Id问题

参考:https://github.com/abel533/Mapper/issues/463