SpringMVC 實現自定義的session 共享(同步)機制java
這個問題是針對線上多臺服務器(例如多個tomcat,集羣)負載均衡而言,若是隻有一個服務器運行(提供服務),則不存在這個問題,請直接略過.nginx
假設咱們使用服務器端的session記錄登陸鑑權信息,(沒有使用redis), 好比用戶登陸時,登陸接口命中的是服務A,那麼服務A中就會記錄用戶的登陸信息, 接着用戶修改資料(好比上傳圖片,修改暱稱等),保存資料接口命中的是服務器B, 服務B中並無記錄登陸信息,因此直接報錯:未登陸,會跳轉到登陸頁面.git
用戶明明已經登陸過了,但是莫名其妙地又讓用戶去登陸. 這就是問題, 用戶的登陸信息 只存儲到了一臺服務器上, 而用戶的各類操做(接口訪問)可能負載到任意一臺服務上. 而http session是內存級別的,各tomcat服務是不會共享的.web
如何讓全部服務都能讀取到用戶的登陸信息呢? 咱們須要把登陸信息存儲到一個全部服務器都能訪問的地方,這裏咱們使用redis, 使用其餘分佈式的緩存,Memcached ,zookeeper 也能夠.redis
實現 HttpServletRequest , 重寫它的 getSession(boolean),getSession()方法.spring
javax.servlet.http.HttpServletRequestWrapper
,重寫它的 getSession(boolean),getSession()HttpSession
,重寫HttpSession的三個核心方法: a. getAttribute; b. setAttribute; c. removeAttributesetAttribute
的重寫實現:/** * 須要重寫 * * @param s * @param o */ @Override public void setAttribute(String s, Object o) { String sessionId = null; if (null == this.httpSession) { sessionId = this.JSESSIONID; } else { this.httpSession.setAttribute(s, o); sessionId = this.httpSession.getId(); } RedisCacheUtil.setSessionAttribute(sessionId + s, o); }
getAttribute
中,對從redis中獲取的value,要反序列化CustomSharedHttpSession 實現HttpSession
apache
package oa.web.responsibility.impl.custom; import com.common.util.RedisCacheUtil; import javax.servlet.ServletContext; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionContext; import java.util.Enumeration; /*** * http session 同步和共享<br /> * see oa/web/responsibility/impl/custom/HttpSessionSyncShareFilter.java */ public class CustomSharedHttpSession implements HttpSession { protected HttpSession httpSession; protected String JSESSIONID; public CustomSharedHttpSession() { super(); } public CustomSharedHttpSession(HttpSession httpSession, String JSESSIONID) { this.httpSession = httpSession; this.JSESSIONID = JSESSIONID; } @Override public long getCreationTime() { return this.httpSession.getCreationTime(); } @Override public String getId() { return this.httpSession.getId(); } @Override public long getLastAccessedTime() { return this.httpSession.getLastAccessedTime(); } @Override public ServletContext getServletContext() { return this.httpSession.getServletContext(); } @Override public void setMaxInactiveInterval(int i) { this.httpSession.setMaxInactiveInterval(i); } @Override public int getMaxInactiveInterval() { return this.httpSession.getMaxInactiveInterval(); } @Override public HttpSessionContext getSessionContext() { return this.httpSession.getSessionContext(); } /*** * 須要重寫 TODO * @param s * @return */ @Override public Object getAttribute(String s) { Object o1 = null; if (null == this.getHttpSession()) { o1 = RedisCacheUtil.getSessionAttribute(this.JSESSIONID + s); /*if (null != o1) { this.setAttribute(s,o1); }*/ return o1; } Object o = this.httpSession.getAttribute(s); if (o == null) { String currentSessionId = this.httpSession.getId(); o = RedisCacheUtil.getSessionAttribute(currentSessionId + s); if (null == o) { if ((!currentSessionId.equals(this.JSESSIONID))) { Object o2 = RedisCacheUtil.getSessionAttribute(this.JSESSIONID + s); if (null != o2) { this.httpSession.setAttribute(s, o2); o = o2; // RedisCacheUtil.setSessionAttribute(currentSessionId + s, o); } } } this.setAttribute(s, o); } return o; } @Override public Object getValue(String s) { return this.httpSession.getValue(s); } @Override public Enumeration<String> getAttributeNames() { return this.httpSession.getAttributeNames(); } @Override public String[] getValueNames() { return this.httpSession.getValueNames(); } /** * 須要重寫 * * @param s * @param o */ @Override public void setAttribute(String s, Object o) { String sessionId = null; if (null == this.httpSession) { sessionId = this.JSESSIONID; } else { this.httpSession.setAttribute(s, o); sessionId = this.httpSession.getId(); } RedisCacheUtil.setSessionAttribute(sessionId + s, o); } @Override public void putValue(String s, Object o) { this.httpSession.putValue(s, o); } @Override public void removeAttribute(String s) { if (null != this.httpSession) { this.httpSession.removeAttribute(s); String sessionId = this.httpSession.getId(); RedisCacheUtil.setSessionAttribute(sessionId + s, null); } RedisCacheUtil.setSessionAttribute(this.JSESSIONID + s, null); } @Override public void removeValue(String s) { this.httpSession.removeValue(s); } @Override public void invalidate() { this.httpSession.invalidate(); } @Override public boolean isNew() { return this.httpSession.isNew(); } /*** * 自定義方法 * @return */ public HttpSession getHttpSession() { return httpSession; } /*** * 自定義方法 * @param httpSession */ public void setHttpSession(HttpSession httpSession) { this.httpSession = httpSession; } }
HttpServletRequest
package oa.web.request; import com.common.util.RedisCacheUtil; import com.common.util.RequestUtil; import com.common.util.SystemHWUtil; import com.common.web.filter.CustomFormHttpMessageConverter; import com.file.hw.props.GenericReadPropsUtil; import com.io.hw.json.HWJacksonUtils; import com.string.widget.util.RegexUtil; import com.string.widget.util.ValueWidget; import oa.util.SpringMVCUtil; import org.apache.log4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import javax.servlet.FilterChain; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.*; import java.util.concurrent.ConcurrentHashMap; public class HttpPutFormContentRequestWrapper extends HttpServletRequestWrapper { protected final static Logger logger = Logger.getLogger(HttpPutFormContentRequestWrapper.class); public final static org.slf4j.Logger httpClientRestLogger = LoggerFactory.getLogger("rest_log"); protected MultiValueMap<String, String> formParameters; protected String requestBody; private ResettableServletInputStream servletStream; /*** * <實際不存在的接口路徑A,真實的接口路徑B> <br> * A映射到B<br> * 有兩個來源:(1)config/pathMapping.json;(2)redis,經過方法 RedisCacheUtil.getServletPathMap() */ private static Map<String, String> handlerMethodPathMap; /*** * 緩存應答題,<servletPath,responseText> */ private Map<String, Object> responseReturnResultMap = new ConcurrentHashMap<>(); private FilterChain chain; /*** * 是否須要改成成員變量 */ protected static final CustomFormHttpMessageConverter formConverter = new CustomFormHttpMessageConverter(); private ThreadLocal<Boolean> is404NotFound = new ThreadLocal<Boolean>() { @Override protected Boolean initialValue() { return Boolean.FALSE; } }; /*** * 解決 SpringMVC 進入接口慢的問題 <br /> * added 2018-06-28 中國標準時間 下午8:55:41 <br /> * see http://i.yhskyc.com/test/1384?testcase=SpringMVC%E8%BF%9B%E5%85%A5%E8%AF%B7%E6%B1%82%E5%B7%A8%E6%85%A2 */ protected Map<String, String> servletPathOriginAndTargetMap; public void set404NotFound(boolean bool) { this.is404NotFound.set(bool); } public boolean is404NotFound() { return this.is404NotFound.get(); } static {//由於每次請求都會new 一個HttpPutFormContentRequestWrapper,因此把initMapping 防止靜態代碼中,全局執行一次 initMapping(); } public void put(String servletPath, Object response) { if (null == servletPath) { servletPath = ""; } responseReturnResultMap.put(servletPath, response); } public String getResponseBodyBackup(String servletPath) { return (String) this.responseReturnResultMap.get(servletPath); } public String getResponseBodyBackup() { return this.getResponseBodyBackup(getServletPath()); } public boolean hasContains(String servletPath) { if (null == servletPath) { return false; } return this.responseReturnResultMap.containsKey(servletPath); } /*** * servlet 路徑映射,相似於nginx的轉發功能<br /> * see https://my.oschina.net/huangweiindex/blog/1789164 */ public static void initMapping() { handlerMethodPathMap = new ConcurrentHashMap<>(); //從本地文件"/config/pathMapping.json"中讀取 handlerMethodPathMap.put("/agent/afterbuy/list/json", "/agent/afterbuy/listfilter/json"); ClassLoader classLoader = SpringMVCUtil.getApplication().getClassLoader(); String resourcePath = "/config/pathMapping.json"; String json = GenericReadPropsUtil.getConfigTxt(classLoader, resourcePath); System.out.println("config/pathMapping.json :" + json); if (!ValueWidget.isNullOrEmpty(json)) { json = RegexUtil.sedDeleteComment(json);//刪除第一行的註釋 if (ValueWidget.isNullOrEmpty(json)) { return; } handlerMethodPathMap.putAll(HWJacksonUtils.deSerializeMap(json, String.class)); } } public HttpPutFormContentRequestWrapper(HttpServletRequest request/*, MultiValueMap<String, String> parameters, String requestBody*/) { super(request); servletStream = new ResettableServletInputStream(); MultiValueMap<String, String> parameters = RequestUtil.readFormParameters(request, formConverter); this.formParameters = (MultiValueMap) (parameters != null ? parameters : new LinkedMultiValueMap()); this.requestBody = formConverter.getRequestBody(); } /*** * see https://my.oschina.net/huangweiindex/blog/1789164<br > * 裏面有接口路徑的映射:handlerMethodPathMap * @return */ @Override public String getServletPath() { if (null != this.servletPathOriginAndTargetMap) { // System.out.println("servletPath :" + servletPath); String servletPath = super.getServletPath(); System.out.println("servletPath 2 :" + servletPath); String targetPath = this.servletPathOriginAndTargetMap.get(servletPath); if (null == targetPath) { targetPath = servletPath; } return targetPath; } String servletPath = super.getServletPath(); //映射 String lookupPath = null; if (!ValueWidget.isNullOrEmpty(handlerMethodPathMap)) { //<實際不存在的接口路徑A,真實的接口路徑B> if (handlerMethodPathMap.containsKey(servletPath)) { lookupPath = handlerMethodPathMap.get(servletPath); } else {//從 redis 獲取,see PreServletPathMapController Map servletPathMap = RedisCacheUtil.getServletPathMap(); if (!ValueWidget.isNullOrEmpty(servletPathMap)) { lookupPath = (String) servletPathMap.get(servletPath); handlerMethodPathMap.putAll(servletPathMap); RedisCacheUtil.clearServletPathMap(); } } } if (ValueWidget.isNullOrEmpty(lookupPath)) { lookupPath = servletPath; } else { String msg = "SpringMVC 層實現 Path Mapping,old:" + servletPath + "\tnew:" + lookupPath + " 將被真正調用"; logger.warn(msg); System.out.println(msg); httpClientRestLogger.error(msg); } //解決 SpringMVC 進入接口慢的問題 servletPathOriginAndTargetMap = new HashMap<>(); servletPathOriginAndTargetMap.put(super.getServletPath(), lookupPath); return lookupPath; } @Override public String getParameter(String name) { String queryStringValue = super.getParameter(name); String formValue = (String) this.formParameters.getFirst(name); return queryStringValue != null ? queryStringValue : formValue; } @Override public Map<String, String[]> getParameterMap() { Map<String, String[]> result = new LinkedHashMap(); Enumeration names = this.getParameterNames(); while (names.hasMoreElements()) { String name = (String) names.nextElement(); result.put(name, this.getParameterValues(name)); } return result; } @Override public Enumeration<String> getParameterNames() { Set<String> names = new LinkedHashSet(); names.addAll(Collections.list(super.getParameterNames())); names.addAll(this.formParameters.keySet()); return Collections.enumeration(names); } @Override public String[] getParameterValues(String name) { String[] queryStringValues = super.getParameterValues(name); List<String> formValues = (List) this.formParameters.get(name); if (formValues == null) { return queryStringValues; } else if (queryStringValues == null) { return (String[]) formValues.toArray(new String[formValues.size()]); } else { List<String> result = new ArrayList(); result.addAll(Arrays.asList(queryStringValues)); result.addAll(formValues); return (String[]) result.toArray(new String[result.size()]); } } @Override public ServletInputStream getInputStream() throws IOException { if (super.getInputStream().available() > 0) { return super.getInputStream(); } String requestCharset = getRequest().getCharacterEncoding(); if (ValueWidget.isNullOrEmpty(requestCharset)) { requestCharset = SystemHWUtil.CHARSET_ISO88591; } servletStream.stream = new ByteArrayInputStream(this.requestBody.getBytes(requestCharset)); return servletStream; } public MultiValueMap<String, String> getFormParameters() { return formParameters; } private static class ResettableServletInputStream extends ServletInputStream { private InputStream stream; @Override public int read() throws IOException { return stream.read(); } } public FilterChain getChain() { return chain; } public void setChain(FilterChain chain) { this.chain = chain; } public static CustomFormHttpMessageConverter getFormConverter() { return formConverter; } public void resetCustom() { this.servletPathOriginAndTargetMap = null; } }
個人其餘開源項目 用於服務器端API 的stub 測試
zookeeper的一個java 圖形客戶端json