最近项目组出现一个很奇怪的问题,为了实现用户的单点登录,项目通过redis进行session管理。而用户的相关身份信息是以字节数组的形式存储在redis中的。既然是字节数组,自然就要进行反序列化才能还原成相关对象。而在反序列化的类型转换的时候,明明是同一个类却总是抛出ClassCastException的异常让人摸不着头脑。现将具体的解决过程总结如下。
1. 查找异常
2. 通过异常链定位源码
为了更加方便的观察问题,源码改为如下形式:
3. 问题重现,观察打印信息:
4. 根据问题查找官方文档:
https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-devtools.html
5. 问题分析
1. JVM判断两个类对象是否相同的依据:一是类全称;一个是类加载器.(具体原理请自行百度,在此不再赘述)。
2. 大家都知道虚拟机的默认类加载机制是通过双亲委派实现的。springboot为了实现程序动态性(比如:代码热替换、模块热部署等,白话讲就是类文件修改后容器不重启),“破坏或牺牲” 了双亲委派模型。springboot通过强行干预-- “截获”了用户自定义类的加载(由jvm的加载器AppClassLoader变为springboot自定义的加载器RestartClassLoader,一旦发现类路径下有文件的修改,springboot中的spring-boot-devtools模块会立马丢弃原来的类文件及类加载器,重新生成新的类加载器来加载新的类文件,从而实现热部署。比较流行的OSGI也能实现热部署)。
6. 问题解决
既然源头因热部署而起,所以只要想办法关掉springboot的热部署即可。
<方案一> 通过卸掉springboot的热部署模块spring-boot-devtools来实现
在pom中注释掉springboot的spring-boot-devtools
<方案二>如果不想卸掉spring-boot-devtools模块也可禁用部署功能
读者也可以在application.properties设置禁用属性,但它的作用域只发生在当前模块,如果你的项目牵扯到多个模块,最好通过上面的方式在整个运行系统的级别禁用,以免出现多个模块之间实现类文件调用时类加载器不一致的问题。
<方案三>既然是类加载器的问题也可使用Spring的ConfigurableObjectInputStream配合Thread.currentThread().getContextClassLoader() 来使用。
7. 解决的结果
从上面的解决结果可以看到类加载机制又恢复了JVM本身的双亲委托机制,最终所有的类都是通过JVM中的类加载器AppClassLoader来加载。
8. 后续补充
8.1 spring-boot-devtools会检测类路径的变化,当类路径内容发生变化后会自动重启应用程序。Spring Boot的重启技术通过使用两个类加载器。由于使用的是双类加载机制重启会非常快,如果启动较慢也可使用JRebel重加载技术。
(1)base classloader (Base类加载器):加载不改变的Class,如第三方提供的jar包。
(2)restart classloader(Restart类加载器):加载正在开发的Class。
到这里相信大家知道了,为什么重启很快,因为重启的时候只是加载了在开发的Class,没有重新加载第三方的jar包。
8.2 因为类文件的修改(一般在IDE环境下修改)只会发生在开发阶段,所以就解释了ClassCastException为什么总是发生在开发阶段,而测试或生产环境却运行正常。
8.3 针对上面问题我们也能自然想到以后只要是在反序列化及热部署的场景下出现的类型转换异常基本上就是这个问题了。