spring-session自定義序列化方式
spring-session自定義序列化
spring-session默認采用jdk序列化方法,該方法效率低下、內存占用大,且需要額外修改代碼。故需要自定義序列化方法
自定義序列方法使用jackson庫
首先需要一個類作為序列化的工具,需要實現
RedisSerializer
該接口在反序列化時沒有提供對應的class對象,因此使用jackson反序列化時,都會返回成Object對象
因此我的解決思路是,在序列化時,獲取對應bean的class,將其和bean序列化後的結果一並返回,存入redis
反序列化時,首先將byte數組轉化成字符串,在從中截取存入的class字符串,作為參數傳入jackson反序列化方法中
問題:對於帶有泛型的bean,無法將其轉化成真正適合的類型
解決方案:對於list,map,set等集合類,獲取其第一個元素的class,存入redis
缺點:要求集合類元素必須是同一個子類,不能來自於同一個父類
問題:spring-session在刪除attribute時,並沒有真正從redis中刪除,隻是將value置為null,此時也會調用該類做序列化
解決方案:查看spring-session源碼得知,對於null值得處理方法為直接返回一個個數為0的byte數組,反序列化時直接返回null即可
import cn.nsu.edu.web.four.config.BaseStatic; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.TypeFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import org.springframework.data.redis.serializer.SerializationUtils; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; public class SessionSerializer implements RedisSerializer<Object> { @Autowired private ObjectMapper mapper; private Logger logger = LoggerFactory.getLogger(getClass()); private final String separator = "="; private final String classPrefix = "<"; private final String classSuffix = ">"; private final String classSeparator = ","; private Pattern pattern; public SessionSerializer() { pattern = Pattern.compile("<(.*)>"); } /** * 獲取class,包含集合類的泛型 * <p>暫隻支持最多兩個泛型,同時集合內數據必須為同一個實現類,不可將泛型聲明成父類</p> * * @param obj 將要序列化的對象 * @return 沒有泛型,形式為java.lang.String<> * <p>一個泛型,形式為java.lang.String<java.lang.String></p> * <p>兩個個泛型,形式為java.lang.String<java.lang.String,java.lang.String></p> */ private String getBegin(Object obj) { StringBuilder builder = new StringBuilder(obj.getClass().toString().substring(6) + classPrefix); if (obj instanceof List) { List list = ((List) obj); if (!list.isEmpty()) { Object temp = list.get(0); builder.append(temp.getClass().toString().substring(6)); } } else if (obj instanceof Map) { Map map = ((Map) obj); Iterator iterator = map.keySet().iterator(); if (iterator.hasNext()) { Object key = iterator.next(); Object value = map.get(key); builder.append(key.getClass().toString().substring(6)).append(classSeparator).append(value.getClass().toString().substring(6)); } } else if (obj instanceof Set) { Set set = ((Set) obj); Iterator iterator = set.iterator(); if (iterator.hasNext()) { Object value = iterator.next(); builder.append(value.getClass().toString().substring(6)); } } builder.append(classSuffix); return builder.toString(); } @Override public byte[] serialize(Object o) throws SerializationException { if (o == null) return new byte[0]; try { String builder = getBegin(o) + separator + mapper.writeValueAsString(o); return builder.getBytes(BaseStatic.CHARSET); } catch (UnsupportedEncodingException | JsonProcessingException e) { e.printStackTrace(); } return null; } @Override public Object deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length == 0) return null;//已被刪除的session try { String temp = new String(bytes, BaseStatic.CHARSET); String cl[] = getClass(temp); if (cl == null) { throw new RuntimeException("錯誤的序列化結果=" + temp); } if (cl.length == 1) { return mapper.readValue(temp.substring(temp.indexOf(separator) + 1), Class.forName(cl[0])); } else if (cl.length == 2) { TypeFactory factory = mapper.getTypeFactory(); JavaType type = factory.constructParametricType(Class.forName(cl[0]), Class.forName(cl[1])); return mapper.readValue(temp.substring(temp.indexOf(separator) + 1), type); } else if (cl.length == 3) { TypeFactory factory = mapper.getTypeFactory(); JavaType type = factory.constructParametricType(Class.forName(cl[0]), Class.forName(cl[1]), Class.forName(cl[2])); return mapper.readValue(temp.substring(temp.indexOf(separator) + 1), type); } } catch (ClassNotFoundException | IOException e) { e.printStackTrace(); } return null; } /** * 解析字符串,獲取class * <p>一個類型,java.lang.String<>={}</p> * <p>兩個類型,後面為泛型,java.lang.String<java.lang.String>={}</p> * <p>三個類型,後面為泛型,java.lang.String<java.lang.String,java.lang.String>={}</p> * * @param value 包含class的字符串 * @return 返回所有類的數組 */ private String[] getClass(String value) { int index = value.indexOf(classPrefix); if (index != -1) { Matcher matcher = pattern.matcher(value.subSequence(index, value.indexOf(classSuffix) + 1)); if (matcher.find()) { String temp = matcher.group(1); if (temp.isEmpty()) {//沒有泛型 return new String[]{value.substring(0, index)}; } else if (temp.contains(classSeparator)) {//兩個泛型 int nextIndex = temp.indexOf(classSeparator); return new String[]{ value.substring(0, index), temp.substring(0, nextIndex), temp.substring(nextIndex + 1) }; } else {//一個泛型 return new String[]{ value.substring(0, index), temp }; } } } return null; } }
配置spring-session序列化
在之前的配置文件上進行修改
<bean id="springSessionDefaultRedisSerializer" class="cn.nsu.edu.web.four.session.serializer.SessionSerializer"/> <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy"> <property name="hostName" value="${redis.host}"/> <property name="port" value="${redis.port}"/> <property name="timeout" value="${redis.timeout}"/> <property name="password" value="${redis.password}"/> <property name="database" value="${redis.database}"/> <property name="usePool" value="true"/> <property name="poolConfig" ref="redisPoolConfig"/> </bean> <bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate"> <property name="connectionFactory" ref="jedisConnectionFactory"/> <property name="defaultSerializer" ref="springSessionDefaultRedisSerializer"/> <!--指定序列化類--> <property name="valueSerializer" ref="springSessionDefaultRedisSerializer"/> <property name="hashValueSerializer" ref="springSessionDefaultRedisSerializer"/> </bean>
spring-session序列化問題排查
嚴重: Servlet.service() for servlet [spring] in context with path [/] threw exception
org.springframework.data.redis.serializer.SerializationException: Cannot serialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.mogoroom.service.vo.criteria.QueryBSPromotionListVO]
at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.serialize(JdkSerializationRedisSerializer.java:52)
at org.springframework.data.redis.core.AbstractOperations.rawHashValue(AbstractOperations.java:146)
at org.springframework.data.redis.core.DefaultHashOperations.putAll(DefaultHashOperations.java:128)
at org.springframework.data.redis.core.DefaultBoundHashOperations.putAll(DefaultBoundHashOperations.java:85)
at org.springframework.session.data.redis.RedisOperationsSessionRepository$RedisSession.saveDelta(RedisOperationsSessionRepository.java:778)
at org.springframework.session.data.redis.RedisOperationsSessionRepository$RedisSession.access$000(RedisOperationsSessionRepository.java:670)
at org.springframework.session.data.redis.RedisOperationsSessionRepository.save(RedisOperationsSessionRepository.java:388)
at org.springframework.session.data.redis.RedisOperationsSessionRepository.save(RedisOperationsSessionRepository.java:245)
at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.commitSession(SessionRepositoryFilter.java:245)
at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.access$100(SessionRepositoryFilter.java:217)
at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:170)
at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:80)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:344)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:261)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:100)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:953)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1041)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:603)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:310)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Caused by: org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.mogoroom.service.vo.criteria.QueryBSPromotionListVO]
at org.springframework.core.serializer.support.SerializingConverter.convert(SerializingConverter.java:67)
at org.springframework.core.serializer.support.SerializingConverter.convert(SerializingConverter.java:34)
at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.serialize(JdkSerializationRedisSerializer.java:50)
… 29 more
Caused by: java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.mogoroom.service.vo.criteria.QueryBSPromotionListVO]
at org.springframework.core.serializer.DefaultSerializer.serialize(DefaultSerializer.java:41)
at org.springframework.core.serializer.support.SerializingConverter.convert(SerializingConverter.java:62)
… 31 more
問題
spring session 異常信息沒有打印到日志中 用是默認jdk序列化。由於實體沒有序列話,導致異常,但是沒有輸入到日志,導致定位到問題。
在代碼中 request.getSession().setAttribute()是不會出現異常的 spring session 一次請求返回的時候,才會commit,才會觸發spring session提交。
代碼如下:onResponseCommitted
/** * Allows ensuring that the session is saved if the response is committed. * * @author Rob Winch * @since 1.0 */ private final class SessionRepositoryResponseWrapper extends OnCommittedResponseWrapper { private final SessionRepositoryRequestWrapper request; /** * Create a new {@link SessionRepositoryResponseWrapper}. * @param request the request to be wrapped * @param response the response to be wrapped */ SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request, HttpServletResponse response) { super(response); if (request == null) { throw new IllegalArgumentException("request cannot be null"); } this.request = request; } @Override protected void onResponseCommitted() { this.request.commitSession(); } } OnCommittedResponseWrapper abstract class OnCommittedResponseWrapper extends HttpServletResponseWrapper { /** * Calls <code>onResponseCommmitted()</code> with the current contents as long as * {@link #disableOnResponseCommitted()} was not invoked. */ private void doOnResponseCommitted() { if (!this.disableOnCommitted) { onResponseCommitted(); disableOnResponseCommitted(); } } }
doOnResponseCommitted相關依賴出發方法
解決方法
filter抓下日志
chain.doFilter(wrappedRequest, response); }catch (Exception ex){ logger.error("xxf",ex); }
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- SpringBoot 整合 Spring-Session 實現分佈式會話項目實戰
- Redis之RedisTemplate配置方式(序列和反序列化)
- 深入理解 Redis Template及4種序列化方式
- Redis快速實現分佈式session的方法詳解
- springboot2.5.0和redis整合配置詳解