Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 5032|回复: 0

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

[复制链接]
  • TA的每日心情
    奋斗
    2024-4-6 11:05
  • 签到天数: 748 天

    [LV.9]以坛为家II

    2034

    主题

    2092

    帖子

    70万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    705612
    发表于 2021-4-18 17:27:54 | 显示全部楼层 |阅读模式

    通过反射解决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

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-5-18 06:09 , Processed in 0.067520 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表