问题分析
在SpringBoot中使用 org.apache.commons.lang.SerializationUtils.clone
方法时,发现克隆出来的类强转对应类时发生类型不一致的错误,经过检测发现两个看似相同的类的类加载器不一致
场景
报错信息
java.lang.ClassCastException: com.tianqiauto.tis.pc.dingdanyupai.po.PrePoint cannot be cast to com.tianqiauto.tis.pc.dingdanyupai.po.PrePoint
检测信息
解决方法分析
既然发现类加载器不一致,那么需要找到类反序列化时的类加载器是如何指定得
深入SerializationUtils.clone
方法时发现内部是通过jdk的反序列化类ObjectInputStream
将字节码转为对象得
发现返回对象是由cons
创建的,cons
是一个Constructor
,那么需要判断cons
是在哪里生成的,从而推断出类加载器的生成依据,而cons
存在于ObjectStreamClass
,进入ObjectStreamClass desc = readClassDesc(false);
判断cons
何时赋值
发现执行完desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
这段代码时,cons
有值,进入initNonProxy
方法中
发现最终指向Caches.localDescs
一个map中
localDescs
是一个全局静态变量,所以需要知道在什么地方添加值的
打断点debug发现在序列化的时候会new ObjectStreamClass(cl)
并放在localDescs
里,所以进入new ObjectStreamClass(cl)
的方法里,找cons
的来源
发现cons
是由Class<?> cl
生成,也就是说cons
的类加载器信息是由Class<?> cl
的类加载器决定的,反向查找cl
的加载
发现cl
的类加载信息由latestUserDefinedLoader()
,查阅资料发现,latestUserDefinedLoader()
会根据栈帧信息查找第一个非根类加载器或扩展类加载器,而SerializationUtils
属于ApplicationClassLoader
加载的范围,所以SerializationUtils.clone(point)
返回的对象是由ApplicationClassLoader
加载
解决方案
方案一(推荐)
将SerializationUtils.clone
中的方法复制到项目中
public class TestClassLoaderController {
private static PrePoint point = new PrePoint();
@GetMapping("/testClassLoader")
public AjaxResult testClassLoader() {
PrePoint deserialize = (PrePoint) cloneObject(point);
return null;
}
private static Object cloneObject(PrePoint point) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
serialize(point, baos);
byte[] bytes = baos.toByteArray();
return deserialize(bytes);
}
public static Object deserialize(byte[] objectData) {
if (objectData == null) {
throw new IllegalArgumentException("The byte[] must not be null");
}
ByteArrayInputStream bais = new ByteArrayInputStream(objectData);
return deserialize(bais);
}
public static Object deserialize(InputStream inputStream) {
if (inputStream == null) {
throw new IllegalArgumentException("The InputStream must not be null");
}
ObjectInputStream in = null;
try {
// stream closed in the finally
in = new ObjectInputStream(inputStream);
return in.readObject();
} catch (ClassNotFoundException ex) {
throw new SerializationException(ex);
} catch (IOException ex) {
throw new SerializationException(ex);
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException ex) {
// ignore close exception
}
}
}
public static void serialize(Serializable obj, OutputStream outputStream) {
if (outputStream == null) {
throw new IllegalArgumentException("The OutputStream must not be null");
}
ObjectOutputStream out = null;
try {
// stream closed in the finally
out = new ObjectOutputStream(outputStream);
out.writeObject(obj);
} catch (IOException ex) {
throw new SerializationException(ex);
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException ex) {
// ignore close exception
}
}
}
}
方案二
关闭热加载
spring:
devtools:
restart:
enabled: false
或者
@SpringBootApplication
public class SSMPApplication {
public static void main(String[] args) {
System.setProperty("spring.devtools.restart.enabled","false");
SpringApplication.run(SSMPApplication.class);
}
}
方案三
移除热加载的jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional> <!-- 这个需要为 true 热部署才有效 -->
</dependency>