對於過濾器中使用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());
    }

寫到這裏我對項目接收參數的認識更加清晰了,不知道對各位有沒有幫助,如果有什麼問題 歡迎聯繫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章