1. 程式人生 > >HttpServletRequestWrapper使用技巧(自定義session和快取InputStream)

HttpServletRequestWrapper使用技巧(自定義session和快取InputStream)

一、前言

  javax.servlet.http.HttpServletRequestWrapper 是一個開發者可以繼承的類,我們可以重寫相應的方法來實現session的自定義以及快取InputStream,在程式中可以多次獲取request body的內容。

二、自定義seesion

import javax.servlet.http.*;

public class CustomizeHttpServletRequest extends HttpServletRequestWrapper {

    public CustomizeHttpServletRequest(HttpServletRequest request) {
        
super(request); this.response = response; } @Override public HttpSession getSession() { //return super.getSession(); // 預設使用的是servlet容器session管理 return this.getSession(true); } @Override public HttpSession getSession(boolean create) { Cookie[] cookies
= this.getCookies(); String sessionId = ""; //這裡編寫自己獲取session的邏輯 //既然是自定義邏輯,可以從記憶體中取,也可以從快取中取。 } }

  也許大家都用過shiro的session管理或者spring-session,其實想要自己去實現,也是很簡單的。

三、快取InputStream

  自定義工具類 ContentCachingRequestWrapper 

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.InputStreamReader; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import org.apache.commons.io.IOUtils; public class ContentCachingRequestWrapper extends HttpServletRequestWrapper{ private byte[] body; private BufferedReader reader; private ServletInputStream inputStream; public ContentCachingRequestWrapper(HttpServletRequest request) throws IOException{ super(request); body = IOUtils.toByteArray(request.getInputStream()); inputStream = new RequestCachingInputStream(body); } public byte[] getBody() { return body; } @Override public ServletInputStream getInputStream() throws IOException { if (inputStream != null) { return inputStream; } return super.getInputStream(); } @Override public BufferedReader getReader() throws IOException { if (reader == null) { reader = new BufferedReader(new InputStreamReader(inputStream, getCharacterEncoding())); } return reader; } private static class RequestCachingInputStream extends ServletInputStream { private final ByteArrayInputStream inputStream; public RequestCachingInputStream(byte[] bytes) { inputStream = new ByteArrayInputStream(bytes); } @Override public int read() throws IOException { return inputStream.read(); } @Override public boolean isFinished() { return inputStream.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener readlistener) { } } }

  spring工具類 ContentCachingRequestWrapper 

package org.springframework.web.util;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.springframework.http.HttpMethod;

public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {

    private static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";


    private final ByteArrayOutputStream cachedContent;

    private ServletInputStream inputStream;

    private BufferedReader reader;


    /**
     * Create a new ContentCachingRequestWrapper for the given servlet request.
     * @param request the original servlet request
     */
    public ContentCachingRequestWrapper(HttpServletRequest request) {
        super(request);
        int contentLength = request.getContentLength();
        this.cachedContent = new ByteArrayOutputStream(contentLength >= 0 ? contentLength : 1024);
    }


    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (this.inputStream == null) {
            this.inputStream = new ContentCachingInputStream(getRequest().getInputStream());
        }
        return this.inputStream;
    }

    @Override
    public String getCharacterEncoding() {
        String enc = super.getCharacterEncoding();
        return (enc != null ? enc : WebUtils.DEFAULT_CHARACTER_ENCODING);
    }

    @Override
    public BufferedReader getReader() throws IOException {
        if (this.reader == null) {
            this.reader = new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
        }
        return this.reader;
    }

    @Override
    public String getParameter(String name) {
        if (this.cachedContent.size() == 0 && isFormPost()) {
            writeRequestParametersToCachedContent();
        }
        return super.getParameter(name);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        if (this.cachedContent.size() == 0 && isFormPost()) {
            writeRequestParametersToCachedContent();
        }
        return super.getParameterMap();
    }

    @Override
    public Enumeration<String> getParameterNames() {
        if (this.cachedContent.size() == 0 && isFormPost()) {
            writeRequestParametersToCachedContent();
        }
        return super.getParameterNames();
    }

    @Override
    public String[] getParameterValues(String name) {
        if (this.cachedContent.size() == 0 && isFormPost()) {
            writeRequestParametersToCachedContent();
        }
        return super.getParameterValues(name);
    }


    private boolean isFormPost() {
        String contentType = getContentType();
        return (contentType != null && contentType.contains(FORM_CONTENT_TYPE) &&
                HttpMethod.POST.matches(getMethod()));
    }

    private void writeRequestParametersToCachedContent() {
        try {
            if (this.cachedContent.size() == 0) {
                String requestEncoding = getCharacterEncoding();
                Map<String, String[]> form = super.getParameterMap();
                for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext(); ) {
                    String name = nameIterator.next();
                    List<String> values = Arrays.asList(form.get(name));
                    for (Iterator<String> valueIterator = values.iterator(); valueIterator.hasNext(); ) {
                        String value = valueIterator.next();
                        this.cachedContent.write(URLEncoder.encode(name, requestEncoding).getBytes());
                        if (value != null) {
                            this.cachedContent.write('=');
                            this.cachedContent.write(URLEncoder.encode(value, requestEncoding).getBytes());
                            if (valueIterator.hasNext()) {
                                this.cachedContent.write('&');
                            }
                        }
                    }
                    if (nameIterator.hasNext()) {
                        this.cachedContent.write('&');
                    }
                }
            }
        }
        catch (IOException ex) {
            throw new IllegalStateException("Failed to write request parameters to cached content", ex);
        }
    }

    /**
     * Return the cached request content as a byte array.
     */
    public byte[] getContentAsByteArray() {
        return this.cachedContent.toByteArray();
    }


    private class ContentCachingInputStream extends ServletInputStream {

        private final ServletInputStream is;

        public ContentCachingInputStream(ServletInputStream is) {
            this.is = is;
        }

        @Override
        public int read() throws IOException {
            int ch = this.is.read();
            if (ch != -1) {
                cachedContent.write(ch);
            }
            return ch;
        }


        @Override
        public boolean isFinished() {
            return is.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener readlistener) {
        }

    }

}

  獲取InputStream

  1、使用自定義工具類的時候呼叫方法 getBody

  2、使用spring工具類的時候呼叫方法 getContentAsByteArray

  列印request中的所有請求資訊,詳細程式碼如下。

private void printRequest(HttpServletRequest request) {
    String body = StringUtils.EMPTY;
    try {
        if (request instanceof ContentCachingRequestWrapper) {
            body = new String(((ContentCachingRequestWrapper) request).getContentAsByteArray(), "UTF-8");
            LOGGER.info("Request-Inputstream: " + body);
        }
    } catch (IOException e) {
        LOGGER.error("printRequest 獲取body異常...", e);
    }

    JSONObject requestJ = new JSONObject();
    JSONObject headers = new JSONObject();
    Collections.list(request.getHeaderNames())
            .stream()
            .forEach(name -> headers.put(name, request.getHeader(name)));
    requestJ.put("headers", headers);
    requestJ.put("parameters", request.getParameterMap());
    requestJ.put("body", body);
    requestJ.put("remote-user", request.getRemoteUser());
    requestJ.put("remote-addr", request.getRemoteAddr());
    requestJ.put("remote-host", request.getRemoteHost());
    requestJ.put("remote-port", request.getRemotePort());
    requestJ.put("uri", request.getRequestURI());
    requestJ.put("url", request.getRequestURL());
    requestJ.put("servlet-path", request.getServletPath());
    requestJ.put("method", request.getMethod());
    requestJ.put("query", request.getQueryString());
    requestJ.put("path-info", request.getPathInfo());
    requestJ.put("context-path", request.getContextPath());

    LOGGER.info("Request-Info: " + JSON.toJSONString(requestJ, SerializerFeature.PrettyFormat));
}

  request中的所有請求資訊示例

 Request-Inputstream: {
        "timestamp":1539155028668,
        "appId":"cmos10086e36ipz2otyy8gfqh",
        "nonce":691879,
        "telephone":"18736085778",
        "signature":"226e734a49d513b3b1e364a06fc6f4eb5e2c425c6446ce6a7a950f1d8d6af06c"
}

Request-Info: {
        "headers":{
                "x-real-ip":"211.138.20.171",
                "content-length":"183",
                "content-encoding":"UTF-8",
                "host":"221.176.66.251",
                "connection":"close",
                "content-type":"application/json",
                "accept-encoding":"gzip,deflate",
                "user-agent":"Apache-HttpClient/4.5.3 (Java/1.7.0_76)"
        },
        "remote-host":"172.17.20.92",
        "method":"POST",
        "body":"{\"timestamp\":1539155028668,\"appId\":\"cmos10086e36ipz2otyy8gfqh\",\"nonce\":691879,\"telephone\":\"18736085778\",\"signature\":\"226e734a49d513b3b1e364a06fc6f4eb5e2c425c6446ce6a7a950f1d8d6af06c\"}",
        "uri":"/wmhopenapi/hevb-api/total",
        "url":"http://221.176.66.251/wmhopenapi/hevb-api/total",
        "servlet-path":"/hevb-api/total",
        "remote-addr":"172.17.20.92",
        "context-path":"/wmhopenapi",
        "remote-port":49174,
        "parameters":{}
}

四、在Filter中替換掉預設的Request

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.util.ContentCachingRequestWrapper;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean wmhFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new WmhFilter());
        registration.addUrlPatterns("/*");
        registration.setName("MyFilter");
        registration.setOrder(1);
        return registration;
    }

    private static class WmhFilter implements Filter {

        @Override
        public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {

        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            chain.doFilter(new ContentCachingRequestWrapper((HttpServletRequest) request), response);
        }

        @Override
        public void destroy() {

        }
    }
}

 五、使用場景

  個人認為,在做許可權管理、使用者管理、登入等場景,尤其是多系統的情況下,常常需要藉助第三方的工具比如shiro,spring-session,完成許可權、角色、使用者、登入的管理邏輯。之前我自己也嘗試過使用spring-session+redis快取實現共享session和單點登入的邏輯。如果時間充分的話,完全可以自己去寫一套session管理的工具,並應用到專案中去。

  最近在配合其他組的同時聯調介面的時候,遇到了這樣的一種情況:他說request body的內容是按照我的協議來的,我後端的實現是通過@RequestBody註解拿到的一個java 物件,有一個欄位值為null,很是詭異。於是我倆就糾結是誰的問題,我說他引數傳的不對,他說我欄位定義不對並讓我列印一下Request InputStream,於是就開始尋找解決方案。我們都知道Input Sream只能獲取一次,再次獲取一定會丟擲異常。通過尋找HttpServletRequest的子類,發現了spring提供了這樣一個快取Input Stream內容的工具類,問題迎刃而解。