图片

图片



一、现象

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


环境如下:macos mojaveidea 2018.2.1springboot、memcache、spring-boot-devtools

二、问题排查

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

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

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和泛型方法出现强转异常,是因为泛型方法仅仅只是省去人为指定强转类型而已,并不是没有强转的那一步。详见字节码文件内容:

// getPersonUseMyClassLoader() 方法返回一个 Object 类型的对象INVOKEVIRTUAL com/zhqy/bean/Person.getPersonUseMyClassLoader ()Ljava/lang/Object;// 检查是否可以转换成Person对象,如果不可以转换则会抛出异常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.spat.memcachedclient-.*jar

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

(完)


更多相关文章

  1. 如何使用java语言求一个正整数的平方根?(自定义Sqrt方法)
  2. 设计模式之模板方法模式
  3. java创建对象的过程(内存角度分析)
  4. Java实现定时任务的三种方法
  5. 一个Java对象到底占多大内存?
  6. 用 globalThis 访问全局对象[每日前端夜话0xF6]
  7. 什么是 Java 对象分配率
  8. 使用 ThreadLocal 变量的时机和方法

随机推荐

  1. Android(安卓)Adapter适配器模板(笔记)
  2. android:gravity和android:layout_gravit
  3. Android 游戏设计教程
  4. android----UI组件
  5. Spinner的Android:prompt无法显示文本
  6. Android(安卓)Design Support Library(二)
  7. Application、Activity Stack 和 Task的
  8. Android:Gravity控制格式
  9. 关于progressbar进度条的显示风格及一些
  10. 源码解析Android中AsyncTask的工作原理