问题分析
在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 finallyin = 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 finallyout = 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>