描述一下因为spring-boot-devtools自定义ClassLoader导致的ClassCastException问题
一、现象
本地开发环境在开发新功能的过程中突然出现了诡异的ClassCastException,之所以称之为诡异,是因为出现了对象强转自身所属类异常。
发生的场景:项目首次接入memcache,在通过泛型方法取值时,虽然取到的值和接收的值是同一个类,但是却出现强转异常。(自己无法强转自己 ⊙﹏⊙∥∣°)
抛出异常:java.lang.ClassCastException: com.zhqy.bean.Person cannot be cast to com.zhqy.bean.Person
环境如下:
macos mojave
idea 2018.2.1
springboot、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的实例代码,代码如下:
// 调用方法:getPersonUseMyClassLoader
Person person = test.getPersonUseMyClassLoader();
// 测试类中的getPersonUseMyClassLoader方法
// 当项目中使用了自定义CLassLoader的情况,一些泛型方法就会出问题
// 例如:项目中使用了spring-devtool,memcache的get方法如果用了泛型方法,就会出问题
// 此方法模拟memcache的get泛型方法
public <T> T getPersonUseMyClassLoader() {
MyClassLoader loader = new MyClassLoader();
Class<?> aClass = loader.findClass(Person.class.getName());
try {
Object obj = aClass.newInstance();
return (T) obj;
} catch (Exception e) {
logger.error(String.format("错误:%s", e));
}
return null;
}
// MyClassLoader类
public class MyClassLoader extends ClassLoader {
private Logger logger = Logger.getLogger(MyClassLoader.class);
@Override
public Class<?> findClass(String name) {
String myPath = "file://" + MyClassLoader.class.getResource("/").getPath().replace("test-classes", "classes") + name.replaceAll("\\.", "/") +".class";
logger.debug(String.format("class file path:%s", myPath));
byte[] cLassBytes = null;
Path path = null;
try {
path = Paths.get(new URI(myPath));
cLassBytes = Files.readAllBytes(path);
return defineClass(name, cLassBytes, 0, cLassBytes.length);
} catch (IOException | URISyntaxException e) {
logger.error(String.format("错误:%s", e.getMessage()));
}
return null;
}
}
2、根据抛出的强转异常,将示例代码的第一句进行如下修改:
Person person = test.getPersonUseMyClassLoader();
改为
Object obj = test.getPersonUseMyClassLoader();
logger.info(String.format("obj.equals(Person.class):%s", obj.getClass().equals(Person.class)));
// 输出false
logger.info(String.format("obj instanceof Person:%s", obj instanceof Person));
// 输出false
logger.info(String.format("obj 的 classLoader:%s", obj.getClass().getClassLoader()));
// 输出com.zhqy.classLoad.MyClassLoader
logger.info(String.format("Person 的 classLoader:%s", Person.class.getClassLoader()));
// 输出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、创建META-INF/spring-devtools.properties文件
2、将memcache也配置为使用spring-boot-devtools的ClassLoader
#spring-devtools.properties文件示例
restart.include.memcachedclient=/com.zhqy.memcache-.*jar
参考文档: INSTANCEOF、CHECKCAST关键字解释 [张振阳] spring-boot-devtools 不同ClassLoader引起的问题 spring-boot-devtools文档,Customizing the Restart Classloader部分