項目總結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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM