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

关键字

  springboot热部署  ClassCastException异常 反射 redis

前言

  最近项目出现一个很有意思的问题,用户信息(token)储存在redis中;在获取token,反序列化的类型转换的时候,明明是同一个类却总是抛出ClassCastException的异常;

正文

1-问题

异常日志

  1. java.lang.ClassCastException: com.hs.web.common.token.AccessToken cannot be cast to com.hs.web.common.token.AccessToken
  2. at com.hs.web.common.token.AccessTokenManager.getToken(AccessTokenManager.java:31) ~[classes/:na]
  3. at com.hs.web.controller.base.AppBaseController.getTokenUser(AppBaseController.java:35) ~[classes/:na]
  4. at com.hs.web.app.controller.AppShopCartController.listShopcart(AppShopCartController.java:66) ~[classes/:na]
  5. at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_102]
  6. at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) ~[na:1.8.0_102]
  7. at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) ~[na:1.8.0_102]
  8. at java.lang.reflect.Method.invoke(Unknown Source) ~[na:1.8.0_102]
  9. at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-4.3.16.RELEASE.jar:4.3.16.RELEASE]
  10. at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133) ~[spring-web-4.3.16.RELEASE.jar:4.3.16.RELEASE]

对应代码

  1. public class AccessTokenManager {
  2.  
  3. private static AccessTokenManager instance = new AccessTokenManager();
  4.  
  5. private AccessTokenManager(){
  6. }
  7.  
  8. public static AccessTokenManager getInstance(){
  9. return instance;
  10. }
  11.  
  12. public AccessToken getToken(String token){
  13. if(!StringUtils.isBlank(token) &&
  14. RedisUtil.exists(RedisKeySuffixEnum.USER_TOKEN.getKey() + token)){
  15. AccessToken accessToken = (AccessToken) RedisUtil.get(RedisKeySuffixEnum.USER_TOKEN.getKey() + token);//类转换异常出现在这里
  16. //AccessToken accessToken = convertAccessToken(RedisUtil.get(RedisKeySuffixEnum.USER_TOKEN.getKey() + token));
  17. return accessToken;
  18. }
  19. return null;
  20. }
  21.  
  22. public String putToken(String userId){
  23. AccessToken token = new AccessToken(userId);
  24. RedisUtil.set(RedisKeySuffixEnum.USER_TOKEN.getKey() + token.getToken(),
  25. token, RedisKeySuffixEnum.USER_TOKEN.getExpireTime());
  26.  
  27. return token.getToken();
  28. }
  29.  
  30. public void updateToken(String token){
  31. if(!StringUtils.isBlank(token) &&
  32. RedisUtil.exists(RedisKeySuffixEnum.USER_TOKEN.getKey() + token)){
  33. AccessToken assessToken = (AccessToken) RedisUtil.get(RedisKeySuffixEnum.USER_TOKEN.getKey() + token);
  34. if(assessToken == null){
  35. return;
  36. }
  37. RedisUtil.set(RedisKeySuffixEnum.USER_TOKEN.getKey() + token, token,
  38. RedisKeySuffixEnum.USER_TOKEN.getExpireTime());
  39. }
  40. }
  41.  
  42. }

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)

  1. <!-- spring boot 的调试模块 -->
  2. <!-- <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-devtools</artifactId>
  5. <optional>true</optional>
  6. </dependency> -->

方案1,简单快速有效,但本质是回避了问题,如果想用springboot热部署,这样做就无法实现热部署,如果想继续用springboot热部署,可以参考方案2。

方案2:通过反射,手动转换对应的类对象

直接上源码解决方案

  1. package com.hs.web.common.token;
  2.  
  3. import java.lang.reflect.Field;
  4.  
  5. import org.apache.commons.lang.StringUtils;
  6. import org.apache.commons.lang3.reflect.FieldUtils;
  7.  
  8. import com.hs.common.util.redis.RedisUtil;
  9. import com.hs.web.model.RedisKeySuffixEnum;
  10.  
  11. /**
  12. * 用户Token管理工具
  13. *
  14. * @comment
  15. * @update
  16. */
  17. public class AccessTokenManager {
  18.  
  19. private static AccessTokenManager instance = new AccessTokenManager();
  20.  
  21. private AccessTokenManager(){
  22. }
  23.  
  24. public static AccessTokenManager getInstance(){
  25. return instance;
  26. }
  27.  
  28. public AccessToken getToken(String token){
  29. if(!StringUtils.isBlank(token) &&
  30. RedisUtil.exists(RedisKeySuffixEnum.USER_TOKEN.getKey() + token)){
  31. //AccessToken accessToken = (AccessToken) RedisUtil.get(RedisKeySuffixEnum.USER_TOKEN.getKey() + token);
  32. AccessToken accessToken = convertAccessToken(RedisUtil.get(RedisKeySuffixEnum.USER_TOKEN.getKey() + token));//使用反射,进行对象转换(方法在下面)
  33. return accessToken;
  34. }
  35. return null;
  36. }
  37.  
  38. public String putToken(String userId){
  39. AccessToken token = new AccessToken(userId);
  40. RedisUtil.set(RedisKeySuffixEnum.USER_TOKEN.getKey() + token.getToken(),
  41. token, RedisKeySuffixEnum.USER_TOKEN.getExpireTime());
  42.  
  43. return token.getToken();
  44. }
  45.  
  46. public void updateToken(String token){
  47. if(!StringUtils.isBlank(token) &&
  48. RedisUtil.exists(RedisKeySuffixEnum.USER_TOKEN.getKey() + token)){
  49. AccessToken assessToken = (AccessToken) RedisUtil.get(RedisKeySuffixEnum.USER_TOKEN.getKey() + token);
  50. if(assessToken == null){
  51. return;
  52. }
  53. RedisUtil.set(RedisKeySuffixEnum.USER_TOKEN.getKey() + token, token,
  54. RedisKeySuffixEnum.USER_TOKEN.getExpireTime());
  55. }
  56. }
  57.  
  58. /**
  59. * 反射转换:解决因类加载器不同导致的转换异常
  60. * com.hs.web.common.token.AccessToken cannot be cast to com.hs.web.common.token.AccessToken
  61. *
  62. */
  63. private AccessToken convertAccessToken(Object redisObject){
  64. AccessToken at = new AccessToken();
  65. at.setToken(ReflectUtils.getFieldValue(redisObject,"token")+"");
  66. at.setUserId(ReflectUtils.getFieldValue(redisObject,"userId")+"");
  67. return at;
  68. }
  69.  
  70. }
  71. //本类私用反射方法
  72. class ReflectUtils{
  73. public static Object getFieldValue(Object obj, String fieldName){
  74. if(obj == null){
  75. return null ;
  76. }
  77. Field targetField = getTargetField(obj.getClass(), fieldName);
  78. try {
  79. return FieldUtils.readField(targetField, obj, true ) ;
  80. } catch (IllegalAccessException e) {
  81. e.printStackTrace();
  82. }
  83. return null ;
  84. }
  85. public static Field getTargetField(Class<?> targetClass, String fieldName) {
  86. Field field = null;
  87. try {
  88. if (targetClass == null) {
  89. return field;
  90. }
  91. if (Object.class.equals(targetClass)) {
  92. return field;
  93. }
  94. field = FieldUtils.getDeclaredField(targetClass, fieldName, true);
  95. if (field == null) {
  96. field = getTargetField(targetClass.getSuperclass(), fieldName);
  97. }
  98. } catch (Exception e) {
  99. }
  100. return field;
  101. }
  102. }

相关非核心源码

  1. /**
  2. * Token WMS管理实体
  3. *
  4. * @comment
  5. * @update
  6. */
  7. public class AccessToken implements Serializable {
  8.  
  9. /**
  10. *
  11. */
  12. private static final long serialVersionUID = 4759692267927548118L;
  13.  
  14. private String token;// AccessToken字符串
  15.  
  16. private String userId;
  17.  
  18. public AccessToken(){
  19. }
  20.  
  21. public AccessToken(String userId){
  22. this.userId = userId;
  23. // this.token = EncryptUtil.encrypt(userId, System.currentTimeMillis() + "");
  24. this.token = EncryptUtil.encrypt(userId);
  25. }
  26.  
  27. public String getToken() {
  28. return token;
  29. }
  30.  
  31. public void setToken(String token) {
  32. this.token = token;
  33. }
  34.  
  35. public String getUserId() {
  36. return userId;
  37. }
  38.  
  39. public void setUserId(String userId) {
  40. this.userId = userId;
  41. }
  42.  
  43. }

方案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. 在Ubuntu下利用Eclipse调试FFmpeg《转》

    参考原贴,其中编译命令有略微改动. 第一步:准备编译环境 #sudoapt-get update #-dev libspeex-dev libtheora-dev libtool libva-dev ...

  2. DJango之视图函数

    一)Django WEB框架 2) request.path和request.get_full_path() 是请求的路径 3)render:页面渲染 4)redirect:页面跳转 3)模板语法: ...

  3. Apache tica详述

    Tika是一个内容抽取的工具集合(a toolkit for text extracting).它集成了POI, Pdfbox 并且为文本抽取工作提供了一个统一的界面.其次,Tika也提供了便利的扩展 ...

  4. 自定义 mapper的实现

    json格式,要想好看直接百度,json,将字符放进去就可 一步:将mapper复制一份,名字加一个Custom自定义 二步:mpper.xml也是一样,设置里面的namespace映射关系 自定义m ...

  5. HTML框架、列表、表格

    本章内容一.列表1.有序列表ol <ol> <li></li> </ol>type的值有3个 默认为1(阿拉伯数字), 还有A/a(大小写字母),I/i ...

  6. 认识bash和shell

    各个 shell 的功能都差不多, Linux 默认使用 bash ,所以我们主要学习bash的使用. 1.bash命令格式 命令 [-options] [参数],如:tar  zxvf  demo. ...

  7. python小数据池概念以及具体范围

    =   赋值符号:        ==  比较值是否相等:   is  比较,比较的是内存地址      ID(内容) 数字,字符串的小数据池 小数据池现象产生的原因,作用: 为了节省内存空间. &l ...

  8. 2018面向对象程序设计(Java)第15周学习指导及要求

    2018面向对象程序设计(Java)第15周学习指导及要求 (2018.12.6-2018.12.9)   学习目标 (1) 掌握Java应用程序打包操作: (2) 了解应用程序存储配置信息的两种方法 ...

  9. “2017面向对象程序设计(Java)第就九周学习总结”存在问题的反馈

    对于“2017面向对象程序设计(Java)第就九周学习总结”存在问题的反馈 1.博文未写者:高树平 高俊梅 冯小丽 缪召召 王瑞强 宗鹏新 李向龙 马润韬 米奇辉 卯保云——不及时提交博客的同学人数出 ...

  10. Java将一个字符串的首位改为大写后边改为小写的实现,String

    Java将一个字符串的首位改为大写后边改为小写的实现,String 思路: 获取首字母, charAt(0) substring(0,1) 转成大写 toUpperCase() 转大写hellO=== ...