1. 程式人生 > >對於過濾器中使用getInputStream()、getParameter()接收引數接收不到的一些知識,以及解決方法。

對於過濾器中使用getInputStream()、getParameter()接收引數接收不到的一些知識,以及解決方法。

昨天,我需要做一個從主專案分離出來的專案對主專案的功能的呼叫,但是在寫Http傳送Post請求時,遇到了主專案接收不到引數的情況,從而引起了我對專案接收引數的一些探討。

我們知道,對於spring專案接收引數用的最多的方式應該是request.getParameter(“xx”),這種方式了把,不論在過濾器Interceptor的preHandle()做攔截是獲取引數處理,還是controller用各種註解獲取引數比如@RequestParam,@RequestParam(這個註解是獲取url後面的引數,下面的post的請求形式上是引數是放在URL後面的,所以能夠使用該註解獲取)等等。

我們主專案中使用的就在過濾器中使用request.getParameter(“xx”),在controller中使用@RequestParam,獲取的引數,今天我在子專案中要呼叫主專案的一個介面時,需要傳一些引數,我就按照平時的傳送Http請求寫了,程式碼如下(注意,一些涉及到私密的 我給遮蔽了 ):

/**
     * 傳送https請求
     * 
     * @param requestUrl 請求地址
     * @param requestMethod 請求方法(get,post)
     * @param outputStr 請求引數
     * @return JSONObject 返回一個json物件
     */
    public static JSONObject httpsRequest(String requestUrl, String requestMethod, String outputStr){
        JSONObject jsonObject = null
; System.out.println("----請求引數"+outputStr); try { URL url = new URL(requestUrl); if (url.toString().startsWith("https")){//https請求路徑 HttpsURLConnection conn = (HttpsURLConnection)url.openConnection(); //建立SSLContext物件,並使用我們指定的信任管理器初始化
TrustManager[] tm = { new MyX509TrustManager() }; SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE"); sslContext.init(null, tm, new java.security.SecureRandom()); //從上述的SSLContext物件中得到SSLSocketFactory SSLSocketFactory ssf = sslContext.getSocketFactory(); conn.setSSLSocketFactory(ssf); conn.setDoInput(true); conn.setDoOutput(true); conn.setUseCaches(false); //設定請求方式 conn.setRequestMethod(requestMethod); //當outputStr不為null的時候,向輸出流寫資料 if(outputStr != null){ OutputStream outputStream = conn.getOutputStream(); outputStream.write(outputStr.getBytes("UTF-8")); outputStream.close(); } HttpsURLConnection httpConn = conn; //從輸入流獲取資料 InputStream inputStream = null; if (httpConn.getResponseCode() >= 400) {//如果報錯,將錯誤資訊寫入到輸入流中 inputStream = httpConn.getErrorStream(); } else { inputStream = httpConn.getInputStream(); } InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8"); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String str = null; StringBuffer buffer = new StringBuffer(); while((str = bufferedReader.readLine()) != null){ buffer.append(str); } //釋放資源 bufferedReader.close(); inputStreamReader.close(); inputStream.close(); httpConn.disconnect(); conn.disconnect(); System.out.println("HTTP請求返回資訊:"+buffer.toString()); jsonObject = JSON.parseObject(buffer.toString()); }else{//http請求 HttpURLConnection conn = (HttpURLConnection)url.openConnection(); conn.setDoInput(true); conn.setDoOutput(true); conn.setUseCaches(false); //設定請求方式 conn.setRequestMethod(requestMethod); //當outputStr不為null的時候,向輸出流寫資料 if(outputStr != null){ OutputStream outputStream = conn.getOutputStream(); outputStream.write(outputStr.getBytes("UTF-8")); outputStream.close(); } HttpURLConnection httpConn = conn; //從輸入流獲取資料 InputStream inputStream = null; if (httpConn.getResponseCode() >= 400) {//如果報錯,將錯誤資訊寫入到輸入流中 inputStream = httpConn.getErrorStream(); } else { inputStream = httpConn.getInputStream(); } InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8"); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String str = null; StringBuffer buffer = new StringBuffer(); while((str = bufferedReader.readLine()) != null){ buffer.append(str); } //釋放資源 bufferedReader.close(); inputStreamReader.close(); inputStream.close(); httpConn.disconnect(); conn.disconnect(); System.out.println("HTTP請求返回資訊:" + buffer.toString()); jsonObject = JSON.parseObject(buffer.toString()); } } catch (ConnectException ce) { ce.printStackTrace(); log.error("連線超時:{}",ce); } catch (Exception e) { e.printStackTrace(); log.error("https請求異常:{}", e); } return jsonObject; }

上面的請求引數是一個json字串資料,用於請求引數,

但是單元測試的時候,這個http請求總是返回說引數不存在的400錯誤。

從上面的程式碼可以看出,我明明是把請求引數寫入到了輸出流當中了。然後我從主專案的過濾器中使用request.getParameter(“xx”),獲取 是一個null值。

剛開始我以為是我的資料沒有寫進來,但是後來我在主專案中使用流讀取引數,確實是能夠讀取到引數的。這就是問題所在,說明我是把請求引數寫入進來了。但是獲取不到。

所以我就開始尋找相關的資訊,後來從別的部落格以及資料中,瞭解到好像request.getParameter(“xx”)這種獲取引數的方法,僅僅對於form表單提交的請求有效,並且form表單還需要設定enctype=”application/x-www-form-urlencoded”是編碼方式,這個是form的預設編碼方式,所以如果不是設定的其他的編碼格式就能夠獲取到。

知道了這個,我就開始在我的http方法中添加了:

conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

設定了請求編碼型別,然後再請求,發現一點用沒有,還是報引數不存在的錯誤。但是我從公司的swagger-ui上測試介面是能夠測試通的。。

後來我使用Fiddler工具監聽了swagger-ui呼叫介面的請求,發現,介面傳輸的引數不是在request的body區域內,而是拼接到了URL上面,也就是說雖然這是一個POST請求,但是引數的傳輸還是在URL上面。然後我就查詢各種資料,然後問了公司的前端工程師、安卓工程師,他們呼叫也沒有問題,我看了一下他們的呼叫程式碼,,前端工程師他也是先將引數處理成一個URL後面的字串,不過他不是將這個字串拼接到URL後面,而是把這個字串寫入到Http的body裡面。安卓的就是把這個json寫入到body屬性中。

然後我就開始修改我的http請求,模擬form表單提交的方式傳送Http請求(從這個可以看出來,java模擬form表單提交與普通的http請求的區別,就是下面這個 還有一個請求頭的content-type的設定問題),在請求前對引數進行處理:


        // 構建請求引數
        StringBuffer sb = new StringBuffer();
        if (outputStr != null) {
            Map params = JSONObject.parseObject(outputStr);
            for (Object e : params.keySet()) {
                sb.append("&");
                sb.append(e);
                sb.append("=");
                sb.append(params.get(e));
            }
            sb.substring(0, sb.length() - 1);
        }

將原來的請求引數拼接成以下格式的請求引數:
&key1=xxx&key2=xxx&key3=xxx

然後在將拼接好的請求引數寫入到request中。單元測試發現主專案中使用request.getParameter能夠獲取到引數了。

雖然這個問題解決了,但是對於專案接收引數還是有很多疑問,比如說在過濾器中如何使用流接收引數,以及為什麼在過濾器或者其他地方或去過引數之後controller裡面就再也獲取不到引數了。。

首先說第二個問題,為什麼在過濾器或者其他地方或去過引數之後controller裡面就再也獲取不到引數了。。

這個問題主要是一個HttpServletRequest的一個不知道是不是bug的問題,就是對於一個request請求來說,它的引數輸入流只能讀取一次,讀取之後流中的資料便沒有了,而無論我們從過濾器中也好還是三方的一些功能裡面也好還是controller,只要它需要使用到request中的引數,他就只能從流中讀取。所以如果在controller之前,有物件都去過request中的流,那麼controller中就再也讀取不到引數了。。

那麼這種問題如何處理呢,現在使用最多的一種方式便是我們現將流讀出來,然後在寫進去。這樣後面的方法在讀取的時候就能夠讀取了。

這也就是第一個問題過濾器中如何使用流接收引數

我們在過濾器中使用流讀取引數,我們需要考慮我們讀完之後,後面的是不是也能讀到。我們不能做那種我們自己讀完了一時爽,然後讓後面的人懵逼去吧的事情。。。

下面是解決方法:

既然原生的ServletRequest有這樣的問題,那麼我們可以自己寫一個ServletRequest,能夠提供重複對取請求引數流的方法,這個就需要繼承HttpServletRequestWrapper方法。

package ***.***.***.***.common;

/**
 * Created by yefuliang on 2017/10/25.
 */

import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;

/**
 * 儲存流
 *
 * @author yefuliang 2017年10月25日
 */
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] body;//儲存流的位元組陣列

    public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        String sessionStream = getBodyString(request);//讀取流中的引數
        body = sessionStream.getBytes(Charset.forName("UTF-8"));
    }

    /**
     * 獲取請求Body
     *
     * @param request
     * @return
     */
    public String getBodyString(final ServletRequest request) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        try {
            inputStream = cloneInputStream(request.getInputStream());
            reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
            String line = "";
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }

    /**
     * Description: 複製輸入流</br>
     *
     * @param inputStream
     * @return</br>
     */
    public InputStream cloneInputStream(ServletInputStream inputStream) {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        try {
            while ((len = inputStream.read(buffer)) > -1) {
                byteArrayOutputStream.write(buffer, 0, len);
            }
            byteArrayOutputStream.flush();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
        InputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        return byteArrayInputStream;
    }
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {

        final ByteArrayInputStream bais = new ByteArrayInputStream(body);

        return new ServletInputStream() {

            @Override
            public int read() throws IOException {
                return bais.read();
            }

        };
    }
}

上面繼承HttpServletRequestWrapper的方法寫好了,我們就可以使用了,具體的使用方法如下:
1.首先在攔截器中將原來的ServletRequest替換掉:

// 防止流讀取一次後就沒有了, 所以需要將流繼續寫出去
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
ServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(httpServletRequest);

HttpServletResponse resp = (HttpServletResponse) servletResponse;
ResponseWrapper mResp = new ResponseWrapper(resp); // 包裝響應物件 resp 並快取響應資料

filterChain.doFilter(requestWrapper, mResp);

可以對比下以前的doFilter()方法:filterChain.doFilter(request, response);
可以發現我上面的程式碼對request和response都進行了封裝,封裝response主要是我這個專案還需要對返回的引數進行處理,因為別的地方都讀取不到reponse中的值,所以在這裡重新封裝了response用與讀取返回值,具體的怎麼封裝可以看我的另一篇部落格過濾器通過HttpServletResponseWrapper包裝HttpServletResponse實現獲取response中的返回資料,以及對資料進行gzip壓縮

2.在過濾器中如果需要讀取引數:

JSONObject parameterMap = JSON.parseObject(new BodyReaderHttpServletRequestWrapper(request).getBodyString(request));
String dataFrom = String.valueOf(parameterMap.get("dataFrom"));

parameterMap 就是請求的引數json。

3.如何在controller中獲取q請求的json資料
可以參考下我下面的方法,使用@RequestBody 將請求引數轉換成後面的型別的引數,後面的可以是一個Bean,也可以是一個json,也可以是一個string,看你傳輸的資料了:

    @RequestMapping("/***/manageUserGag")
    public String manageUserGag(@RequestBody JSONObject request){
        return ***Impl.manageUserGag(request.toJSONString());
    }

寫到這裡我對專案接收引數的認識更加清晰了,不知道對各位有沒有幫助,如果有什麼問題 歡迎聯絡。