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

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

  • 相关阅读:
    LintCode Python 简单级题目 488.快乐数
    LintCode Python 简单级题目 100.删除排序数组中的重复数字 101.删除排序数组中的重复数字II
    LintCode Python 简单级题目 373.奇偶分割数组
    LintCode Python 简单级题目 39.恢复旋转排序数组
    LintCode Python 简单级题目 35.翻转链表
    LintCode Python 简单级题目 451.两两交换链表中的节点
    LintCode Python 简单级题目 174.删除链表中倒数第n个节点
    aws查看官方centos镜像imageid
    linux shell脚本查找重复行/查找非重复行/去除重复行/重复行统计
    php配置优化-生产环境应用版
  • 原文地址:https://www.cnblogs.com/wobuchifanqie/p/9908243.html
Copyright © 2011-2022 走看看