SpringMVC 實現自定義的session 共享(同步)機制

SpringMVC 實現自定義的session 共享(同步)機制java

思路

這個問題是針對線上多臺服務器(例如多個tomcat,集羣)負載均衡而言,若是隻有一個服務器運行(提供服務),則不存在這個問題,請直接略過.nginx

爲何多個tomcat服務會存在session 不一樣步的問題

使用nginx 負載均衡 假設咱們使用服務器端的session記錄登陸鑑權信息,(沒有使用redis), 好比用戶登陸時,登陸接口命中的是服務A,那麼服務A中就會記錄用戶的登陸信息, 接着用戶修改資料(好比上傳圖片,修改暱稱等),保存資料接口命中的是服務器B, 服務B中並無記錄登陸信息,因此直接報錯:未登陸,會跳轉到登陸頁面.git

用戶明明已經登陸過了,但是莫名其妙地又讓用戶去登陸. 這就是問題, 用戶的登陸信息 只存儲到了一臺服務器上, 而用戶的各類操做(接口訪問)可能負載到任意一臺服務上. 而http session是內存級別的,各tomcat服務是不會共享的.web

流程

如何讓全部服務都能讀取到用戶的登陸信息呢? 咱們須要把登陸信息存儲到一個全部服務器都能訪問的地方,這裏咱們使用redis, 使用其餘分佈式的緩存,Memcached ,zookeeper 也能夠.redis

方案

實現 HttpServletRequest , 重寫它的 getSession(boolean),getSession()方法.spring

具體方案

  1. 實現 javax.servlet.http.HttpServletRequestWrapper ,重寫它的 getSession(boolean),getSession()
  2. 實現HttpSession ,重寫HttpSession的三個核心方法: a. getAttribute; b. setAttribute; c. removeAttribute
  3. 在這三個方法中,除了對原始的HttpSession 操做外,還會同時對redis進行操做.
    看下 setAttribute 的重寫實現:
/**
     * 須要重寫
     *
     * @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);
    }

注意

  1. 存儲到redis 中的時候,redis 的key必定要有原始sessionId,這樣才能區分是哪一個會話;
  2. redis 中的value 實際都是String,因此在setAttribute 中存儲到redis 時,要對存儲的值進行序列化, 同理 getAttribute中,對從redis中獲取的value,要反序列化

代碼

CustomSharedHttpSession 實現HttpSessionapache

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;
    }
}

HttpPutFormContentRequestWrapper重寫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

相關文章
相關標籤/搜索