关键字
springboot热部署 ClassCastException异常 反射 redis
前言
最近项目出现一个很有意思的问题,用户信息(token)储存在redis中;在获取token,反序列化的类型转换的时候,明明是同一个类却总是抛出ClassCastException的异常;
正文
1-问题
异常日志
java.lang.ClassCastException: com.hs.web.common.token.AccessToken cannot be cast to com.hs.web.common.token.AccessToken
at com.hs.web.common.token.AccessTokenManager.getToken(AccessTokenManager.java:31) ~[classes/:na]
at com.hs.web.controller.base.AppBaseController.getTokenUser(AppBaseController.java:35) ~[classes/:na]
at com.hs.web.app.controller.AppShopCartController.listShopcart(AppShopCartController.java:66) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_102]
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) ~[na:1.8.0_102]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) ~[na:1.8.0_102]
at java.lang.reflect.Method.invoke(Unknown Source) ~[na:1.8.0_102]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133) ~[spring-web-4.3.16.RELEASE.jar:4.3.16.RELEASE]
对应代码
public class AccessTokenManager {
private static AccessTokenManager instance = new AccessTokenManager();
private AccessTokenManager(){
}
public static AccessTokenManager getInstance(){
return instance;
}
public AccessToken getToken(String token){
if(!StringUtils.isBlank(token) &&
RedisUtil.exists(RedisKeySuffixEnum.USER_TOKEN.getKey() + token)){
AccessToken accessToken = (AccessToken) RedisUtil.get(RedisKeySuffixEnum.USER_TOKEN.getKey() + token);//类转换异常出现在这里
//AccessToken accessToken = convertAccessToken(RedisUtil.get(RedisKeySuffixEnum.USER_TOKEN.getKey() + token));
return accessToken;
}
return null;
}
public String putToken(String userId){
AccessToken token = new AccessToken(userId);
RedisUtil.set(RedisKeySuffixEnum.USER_TOKEN.getKey() + token.getToken(),
token, RedisKeySuffixEnum.USER_TOKEN.getExpireTime());
return token.getToken();
}
public void updateToken(String token){
if(!StringUtils.isBlank(token) &&
RedisUtil.exists(RedisKeySuffixEnum.USER_TOKEN.getKey() + token)){
AccessToken assessToken = (AccessToken) RedisUtil.get(RedisKeySuffixEnum.USER_TOKEN.getKey() + token);
if(assessToken == null){
return;
}
RedisUtil.set(RedisKeySuffixEnum.USER_TOKEN.getKey() + token, token,
RedisKeySuffixEnum.USER_TOKEN.getExpireTime());
}
}
}
2-原因分析
简单来说:就是类加载机制出了问题
具体分析如下(参考:https://www.jianshu.com/p/e6d5a3969343)
1. JVM判断两个类对象是否相同的依据:一是类全称;一个是类加载器.(具体原理请自行百度,在此不再赘述)。
2. 大家都知道虚拟机的默认类加载机制是通过双亲委派实现的。springboot为了实现程序动态性(比如:代码热替换、模块热部署等,白话讲就是类文件修改后容器不重启),“破坏或牺牲” 了双亲委派模型。springboot通过强行干预-- “截获”了用户自定义类的加载(由jvm的加载器AppClassLoader变为springboot自定义的加载器RestartClassLoader,一旦发现类路径下有文件的修改,springboot中的spring-boot-devtools模块会立马丢弃原来的类文件及类加载器,重新生成新的类加载器来加载新的类文件,从而实现热部署。比较流行的OSGI也能实现热部署)。
3-解决方案
根据原因分析,问题处在springboot热部署,那么解决问题也是从这个方面入手
方案1:关掉springboot的热部署即可(在pom中注释掉springboot的spring-boot-devtools)
<!-- spring boot 的调试模块 -->
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>-->
方案1,简单快速有效,但本质是回避了问题,如果想用springboot热部署,这样做就无法实现热部署,如果想继续用springboot热部署,可以参考方案2。
方案2:通过反射,手动转换对应的类对象
直接上源码解决方案
package com.hs.web.common.token;
import java.lang.reflect.Field;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import com.hs.common.util.redis.RedisUtil;
import com.hs.web.model.RedisKeySuffixEnum;
/**
* 用户Token管理工具
*
* @comment
* @update
*/
public class AccessTokenManager {
private static AccessTokenManager instance = new AccessTokenManager();
private AccessTokenManager(){
}
public static AccessTokenManager getInstance(){
return instance;
}
public AccessToken getToken(String token){
if(!StringUtils.isBlank(token) &&
RedisUtil.exists(RedisKeySuffixEnum.USER_TOKEN.getKey() + token)){
//AccessToken accessToken = (AccessToken) RedisUtil.get(RedisKeySuffixEnum.USER_TOKEN.getKey() + token);
AccessToken accessToken = convertAccessToken(RedisUtil.get(RedisKeySuffixEnum.USER_TOKEN.getKey() + token));//使用反射,进行对象转换(方法在下面)
return accessToken;
}
return null;
}
public String putToken(String userId){
AccessToken token = new AccessToken(userId);
RedisUtil.set(RedisKeySuffixEnum.USER_TOKEN.getKey() + token.getToken(),
token, RedisKeySuffixEnum.USER_TOKEN.getExpireTime());
return token.getToken();
}
public void updateToken(String token){
if(!StringUtils.isBlank(token) &&
RedisUtil.exists(RedisKeySuffixEnum.USER_TOKEN.getKey() + token)){
AccessToken assessToken = (AccessToken) RedisUtil.get(RedisKeySuffixEnum.USER_TOKEN.getKey() + token);
if(assessToken == null){
return;
}
RedisUtil.set(RedisKeySuffixEnum.USER_TOKEN.getKey() + token, token,
RedisKeySuffixEnum.USER_TOKEN.getExpireTime());
}
}
/**
* 反射转换:解决因类加载器不同导致的转换异常
* com.hs.web.common.token.AccessToken cannot be cast to com.hs.web.common.token.AccessToken
*
*/
private AccessToken convertAccessToken(Object redisObject){
AccessToken at = new AccessToken();
at.setToken(ReflectUtils.getFieldValue(redisObject,"token")+"");
at.setUserId(ReflectUtils.getFieldValue(redisObject,"userId")+"");
return at;
}
}
//本类私用反射方法
class ReflectUtils{
public static Object getFieldValue(Object obj, String fieldName){
if(obj == null){
return null ;
}
Field targetField = getTargetField(obj.getClass(), fieldName);
try {
return FieldUtils.readField(targetField, obj, true ) ;
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null ;
}
public static Field getTargetField(Class<?> targetClass, String fieldName) {
Field field = null;
try {
if (targetClass == null) {
return field;
}
if (Object.class.equals(targetClass)) {
return field;
}
field = FieldUtils.getDeclaredField(targetClass, fieldName, true);
if (field == null) {
field = getTargetField(targetClass.getSuperclass(), fieldName);
}
} catch (Exception e) {
}
return field;
}
}
相关非核心源码
/**
* Token WMS管理实体
*
* @comment
* @update
*/
public class AccessToken implements Serializable {
/**
*
*/
private static final long serialVersionUID = 4759692267927548118L;
private String token;// AccessToken字符串
private String userId;
public AccessToken(){
}
public AccessToken(String userId){
this.userId = userId;
// this.token = EncryptUtil.encrypt(userId, System.currentTimeMillis() + "");
this.token = EncryptUtil.encrypt(userId);
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
}
方案3:
在resources目录下面创建META_INF文件夹,然后创建spring-devtools.properties文件,文件加上类似下面的配置:
restart.exclude.companycommonlibs=/mycorp-common-[\w-]+.jar
restart.include.projectcommon=/mycorp-myproj-[\w-]+.jar
但是这种方法没有凑效(目前原因不明)
总结
因项目发现springboot环境下相同类进行转换出现ClassCastException异常问题,分析原因,并提出两种解决方案:卸载springboot热部署,或通过反射强转类对象,从而解决问题