描述一下因为spring-boot-devtools自定义ClassLoader导致的ClassCastException问题

一、现象

本地开发环境在开发新功能的过程中突然出现了诡异的ClassCastException,之所以称之为诡异,是因为出现了对象强转自身所属类异常。
发生的场景:项目首次接入memcache,在通过泛型方法取值时,虽然取到的值和接收的值是同一个类,但是却出现强转异常。(自己无法强转自己 ⊙﹏⊙∥∣°)
抛出异常:java.lang.ClassCastException: com.zhqy.bean.Person cannot be cast to com.zhqy.bean.Person

  1. 环境如下:
  2. macos mojave
  3. idea 2018.2.1
  4. springbootmemcachespring-boot-devtools

二、问题排查

1、怀疑泛型方法有问题,但是方法简单到无法怀疑

  1. // 代码功能简述:将memcache的对象反序列化成对应的实体类
  2. // 简单代码如下
  3. public <T> T get(String key) {
  4. try {
  5. return (T) mc.get(key);
  6. } catch (Exception e) {
  7. log.error(EXCEPTION, e);
  8. }
  9. return null;
  10. }

2、怀疑是工具方法中的泛型有问题,导致无法正确转换
使用Junit和main方法运行具体的工具方法,发现结果一直是正确的,未出现一次ClassCastException。

三、问题解决

一般的排查手段已经无法解决问题,最后还是同事@张帆提出,可能是因为spring-boot-devtools热部署功能使用了自定义ClassLoader导致的问题。

验证理论的过程如下
1、先简单准备自定义classLoader的实例代码,代码如下:

  1. // 调用方法:getPersonUseMyClassLoader
  2. Person person = test.getPersonUseMyClassLoader();
  3. // 测试类中的getPersonUseMyClassLoader方法
  4. // 当项目中使用了自定义CLassLoader的情况,一些泛型方法就会出问题
  5. // 例如:项目中使用了spring-devtool,memcache的get方法如果用了泛型方法,就会出问题
  6. // 此方法模拟memcache的get泛型方法
  7. public <T> T getPersonUseMyClassLoader() {
  8. MyClassLoader loader = new MyClassLoader();
  9. Class<?> aClass = loader.findClass(Person.class.getName());
  10. try {
  11. Object obj = aClass.newInstance();
  12. return (T) obj;
  13. } catch (Exception e) {
  14. logger.error(String.format("错误:%s", e));
  15. }
  16. return null;
  17. }
  18. // MyClassLoader类
  19. public class MyClassLoader extends ClassLoader {
  20. private Logger logger = Logger.getLogger(MyClassLoader.class);
  21. @Override
  22. public Class<?> findClass(String name) {
  23. String myPath = "file://" + MyClassLoader.class.getResource("/").getPath().replace("test-classes", "classes") + name.replaceAll("\\.", "/") +".class";
  24. logger.debug(String.format("class file path:%s", myPath));
  25. byte[] cLassBytes = null;
  26. Path path = null;
  27. try {
  28. path = Paths.get(new URI(myPath));
  29. cLassBytes = Files.readAllBytes(path);
  30. return defineClass(name, cLassBytes, 0, cLassBytes.length);
  31. } catch (IOException | URISyntaxException e) {
  32. logger.error(String.format("错误:%s", e.getMessage()));
  33. }
  34. return null;
  35. }
  36. }

2、根据抛出的强转异常,将示例代码的第一句进行如下修改:

  1. Person person = test.getPersonUseMyClassLoader();
  2. 改为
  3. Object obj = test.getPersonUseMyClassLoader();
  4. logger.info(String.format("obj.equals(Person.class):%s", obj.getClass().equals(Person.class)));
  5. // 输出false
  6. logger.info(String.format("obj instanceof Person:%s", obj instanceof Person));
  7. // 输出false
  8. logger.info(String.format("obj 的 classLoader:%s", obj.getClass().getClassLoader()));
  9. // 输出com.zhqy.classLoad.MyClassLoader
  10. logger.info(String.format("Person 的 classLoader:%s", Person.class.getClassLoader()));
  11. // 输出sun.misc.Launcher$AppClassLoader

根据修改后obj instanceof Person的结果可知,obj不是Person的对象。(很蛋疼的结果,因为理论上obj确实是Person类的对象)
在这种情况下,分析示例代码的字节码得知,instanceof结果为false和泛型方法出现强转异常,是因为泛型方法仅仅只是省去人为指定强转类型而已,并不是没有强转的那一步。详见字节码文件内容:

  1. // getPersonUseMyClassLoader() 方法返回一个 Object 类型的对象
  2. INVOKEVIRTUAL com/zhqy/bean/Person.getPersonUseMyClassLoader ()Ljava/lang/Object;
  3. // 检查是否可以转换成Person对象,如果不可以转换则会抛出异常
  4. CHECKCAST com/zhqy/bean/Person

四、结论

在示例代码中,虽然被强转对象和接收的对象是同一个,但是因为classLoader不是同一个,在CHECKCAST时就会抛出ClassCastException异常。
解决办法

  1. 1、创建META-INF/spring-devtools.properties文件
  2. 2、将memcache也配置为使用spring-boot-devtoolsClassLoader
  3. #spring-devtools.properties文件示例
  4. restart.include.memcachedclient=/com.zhqy.memcache-.*jar

参考文档: INSTANCEOF、CHECKCAST关键字解释 [张振阳] spring-boot-devtools 不同ClassLoader引起的问题 spring-boot-devtools文档,Customizing the Restart Classloader部分