通过反射解决springboot环境下从redis取缓存进行转换时出现ClassCastException异常问题

关键字

  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热部署,或通过反射强转类对象,从而解决问题

参考文献

1- https://www.jianshu.com/p/e6d5a3969343

2- https://www.cnblogs.com/ldy-blogs/p/8671863.html

项目总结10:通过反射解决springboot环境下从redis取缓存进行转换时出现ClassCastException异常问题的更多相关文章

  1. Windows环境下启动Redis报错:Could not create server TCP listening socket 127.0.0.1:6379: bind: 操作成功完成。(已解决)

    问题描述: 今天在windows环境下启动Redis时启动失败报错: 解决方案: ①运行命令:redis-cli.exe ②退出Redis ③运行命令:redis-server.exe redis.w ...

  2. centos / Linux 服务环境下安装 Redis 5.0.3

    原文:centos / Linux 服务环境下安装 Redis 5.0.3 1.首先进入你要安装的目录 cd /usr/local 2.下载目前最新稳定版本 Redis 5.0.3 wget http ...

  3. Ubuntu环境下的Redis 配置与C++使用入门

      Redis是一个高性能的key-value数据库. Redisedis的出现,非常大程度补偿了memcached这类key/value存储的不足,在部分场合能够对关系数据库起到非常好的补充作用.它 ...

  4. 在windows环境下安装redis和phpredis的扩展

    在windows环境下安装redis和phpredis的扩展 1.首先配置php: 需要在windows的集成环境中找到php的扩展文件夹,ext,然后在网上寻找自己的php对应的.dll文件 比如说 ...

  5. 在linux环境下安装redis并且搭建自己的redis集群

    此文档主要介绍在linux环境下安装redis并且搭建自己的redis集群 搭建环境: ubuntun 16.04 + redis-3.0.6 本文章分为三个部分:redis安装.搭建redis集群 ...

  6. Linux环境下安装Redis

    记录一下Linux环境下安装Redis,按顺序执行即可,这里下载的是Redis5,大家可根据自己的需求,修改版本号就好了,亲测可行. 1.下载Redis安装包cd /usr/local/wget ht ...

  7. Springboot使用Shiro-整合Redis作为缓存 解决定时刷新问题

    说在前面 (原文链接: https://blog.csdn.net/qq_34021712/article/details/80774649)本来的整合过程是顺着博客的顺序来的,越往下,集成的越多,由 ...

  8. 解决中文环境下zabbix监控图形注释乱码

    zabbix监控的图形界面能够更直观的查看监控状态,当我们把zabbix的语言切换为中文的时候,会发现监控图形中一些中文参数会乱码,例如下面的效果 但是图形界面在原生的英文环境下完全没有乱码问题.为了 ...

  9. 解决win10环境下python Selenuim调用Chrome时提示data 及Chrome正在受自动软件控制的方法

    用python自动访问谷歌浏览器时会出现data界面,很是烦人.在网上搜索,有说是因为webdriver和google版本不匹配导致的,就下过各种版本,结果都一样. 后来明白了,出现data的原因只是 ...

随机推荐

  1. 【版本发布】JAVA微服务开发框架,Jeecg-P3 1.0.0 重构版本发布

    1.项目介绍 Jeecg-P3是一个微服务框架,采用插件式模式开发:业务插件以JAR方式提供,松耦合可插拔支持独立部署,也可无缝集成Jeecg平台中,目前jeecg已经提供了在线聊天,我的邮箱等一系列 ...

  2. JDBC使用步骤分哪几步?

    (1) 加载JDBC驱动程序: Cllass.forName(" 驱动程序" );   //你要连接的数据库对象 (2) 建立连接 Connection conn=DriverMa ...

  3. vscode vue 格式化 和emmet 提示

    ctrl+shift+p打开用户默认设置 设置vetur插件 "vetur.validation.template": false, "vetur.format.defa ...

  4. Hibernate学习笔记2.4(Hibernate的Id生成策略)

    通过设置告诉id该怎么设置. 1.通过xml方式 1.assigned 主键由外部程序负责生成,在 save() 之前必须指定一个.Hibernate不负责维护主键生成.与Hibernate和底层数据 ...

  5. C++ VC实现对话框窗口任意分割

    最近写MFC的程序,想在对话框里实现窗口的任意分割.现在网络资料一大抄,找个东西实在麻烦.总算这个很简单,很快就搞定了,写下来做个笔记.    个人认为简单问题最好就是直接贴源代码,一看就明白,说来说 ...

  6. Docker虚拟化平台

    1.虚拟化技术的概念 1)虚拟化就是把物理资源转变为逻辑上可以管理的资源,以打破物理结构间的壁垒,让计算机的元件运行在虚拟的基础上,而不是真实的物理设备: 2)虚拟化技术可以将物理机硬件资源虚拟生成单 ...

  7. platform 系统是windows还是liunx

    import platform # 判断当前代码运行的系统是windows还是liunx print(platform.architecture()) print(platform.platform( ...

  8. Python基础学习Day7 基础数据类型的扩展 集合 深浅copy

    一.基础数据类型的扩展 1.1GBK ---> UTF - 8 # str --->bytes s1 = '太白' # 字符串是unicode编码 b1 = s1.encode('gbk' ...

  9. Repeater - 重复器

    Repeater - 重复器,用来展示泛型集合中的数据 五大模板:1.HeaderTemplate - 头模板,加载时会在开始执行一次2.FooterTemplate - 脚模板,加载时会在最后执行一 ...

  10. windows 下 redis安装

    在D盘新建文件夹[redis],右键解压Redis ZIP包,把所有文件解压到redis文件夹中.(其他盘符也可以滴^_^) 文件介绍: redis-benchmark.exe         #基准 ...