對於過濾器中使用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());
}
寫到這裡我對專案接收引數的認識更加清晰了,不知道對各位有沒有幫助,如果有什麼問題 歡迎聯絡。