Request的包裝類HttpServletRequestWrapper使用
在使用zuul進行鑒權的時候,我們希望從請求Request中獲取輸入流,解析里面的內容,奈何InputStream只能被讀取一次。為啥呢?源碼里是這樣說的:
public int read(byte[] b,int off, int len)
Reads up to len bytes of data into an array of bytes from this input stream. Ifpos equals count, then -1 is returned to indicate end of file. Otherwise, the number k of bytes read is equal to the smaller of len and count-pos.If k is positive, then bytes buf[pos] through buf[pos+k-1] are copied into b[off] through b[off+k-1] in the manner performed by System.arraycopy. The value k is added into pos and k is returned.
大致的意思是:
在InputStream讀取的時候,會有一個pos指針,它指示每次讀取之后下一次要讀取的起始位置。在每次讀取后會更新pos的值,當你下次再來讀取的時候是從pos的位置開始的,而不是從頭開始,所以第二次獲取String中的值的時候是不全的,API中提供了一個解決辦法:reset()。但我發現在inputStream和servlet中根本不起作用。提示 mark/reset not supported 。意思是只有重寫過markSupported()方法的IO流才可以用。所以一般我們使用inputStream,最好在一次內處理完所有邏輯。
那么就沒法在中途獲取請求流中的數據么?當然有辦法了,我可是PPZ,只需要重寫Request緩存一下流中的數據就好了,實現代碼如下:
BodyReaderHttpServletRequestWrapper.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
package com.neo.authUtils; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.Charset; import java.util.Enumeration; import java.util.NoSuchElementException; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper { // private final byte[] body; -----》 private byte [] body;《------- public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException { super (request); System.out.println( "-------------------打印請求的頭信息------------------------------" ); Enumeration<?> e = request.getHeaderNames() ; while (e.hasMoreElements()){ String name = (String) e.nextElement(); String value = request.getHeader(name); // System.out.println(name+" = "+value); } -----》獲取流中的數據緩存到字節數組中,以后要讀數據就用這里的《------ body = HttpHelper.getBodyString(request).getBytes(Charset.forName( "UTF-8" )); } /** * 從請求的頭部獲取用戶的身份識別id; * @param request * @return */ public String getJsessionidFromHeader(HttpServletRequest request) { String jsessionid = null ; //識別用戶身份的id; Enumeration<?> e = request.getHeaderNames() ; while (e.hasMoreElements()){ String name = (String) e.nextElement(); String value = request.getHeader(name); //cookie = JSESSIONID=B926F6024438D4C693A5E5881595160C; SESSION=458e80dc-e354-4af3-a501-74504a873e70 if ( "cookie" .equals(name)) { jsessionid = value.split( ";" )[ 0 ].split( "=" )[ 1 ]; } System.out.println(name+ "=" +value); } // System.out.println("======jsessionid========>"+jsessionid); return jsessionid; } @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() { public int read() throws IOException { return bais.read(); } @Override public boolean isFinished() { // TODO Auto-generated method stub return false ; } @Override public boolean isReady() { // TODO Auto-generated method stub return false ; } @Override public void setReadListener(ReadListener listener) { // TODO Auto-generated method stub } }; } @Override public String getHeader(String name) { return super .getHeader(name); } @Override public Enumeration<String> getHeaderNames() { return super .getHeaderNames(); } /* @Override public Enumeration<String> getHeaders(String name) { return super.getHeaders(name); } */ /** * content-type=text/plain;charset=UTF-8 * 重寫getHeaders方法,實現自定義Content-Type; */ @Override public Enumeration<String> getHeaders(String name) { if ((null != name && name.equals("Content-Type"))||(null != name && name.equals("content-type"))) { return new Enumeration<String>() { private boolean hasGetted = false; @Override public String nextElement() { if (hasGetted) { throw new NoSuchElementException(); } else { hasGetted = true; return "application/json;charset=utf-8"; } } @Override public boolean hasMoreElements() { return !hasGetted; } }; } return super.getHeaders(name); } /** * 添加自定義信息到請求體; * @param customMsg:自定義的添加到請求體中的信息; */ public void appendCustomMsgToReqBody(String customMsg) { String oldBodyString = HttpHelper.getBodyString( this ); //oldBodyString一定是通過當前對象的輸入流解析得來的,否則接收時會報EOFException; String appendMsg = HttpHelper.appendCustomMsgToReqBody(customMsg); String requestBodyAfterAppend = appendMsg + "," +oldBodyString; //this.body = HttpHelper.appendCustomMsgToReqBody(HttpHelper.appendCustomMsgToReqBody(customMsg)+(HttpHelper.getBodyString(this))).getBytes(Charset.forName("UTF-8")); //this.body = HttpHelper.appendCustomMsgToReqBody((HttpHelper.getBodyString(this))).getBytes(Charset.forName("UTF-8")); this .body = HttpHelper.appendCustomMsgToReqBody(requestBodyAfterAppend).getBytes(Charset.forName( "UTF-8" )); } } |
HttpHelper.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
package com.neo.authUtils; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import javax.servlet.ServletRequest; public class HttpHelper { /** * 獲取post請求中的Body * * @param request * @return */ public static String getBodyString(ServletRequest request) { StringBuilder sb = new StringBuilder(); InputStream inputStream = null ; BufferedReader reader = null ; try { inputStream = 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(); } //添加自定義的信息到請求體中; public static String appendCustomMsgToReqBody(String newReqBodyStr) { StringBuilder sb = new StringBuilder(); InputStream inputStream = null ; BufferedReader reader = null ; String newReqBody = null ; try { //通過字符串構造輸入流; inputStream = String2InputStream(newReqBodyStr); 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(); } } } //返回字符串; newReqBody = sb.toString(); return newReqBody; } //將字符串轉化為輸入流; public static InputStream String2InputStream(String str) { ByteArrayInputStream stream = null ; try { stream = new ByteArrayInputStream(str.getBytes( "UTF-8" )); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return stream; } } |
上述方案解決了
使用request.getInpuStream()方法讀取流中的數據只能讀取一次的問題,其實當我們在使用第三方接口時,如果請求頭信息和我們的服務所需不一致,例如第三方接口中頭部信息為:content-type=text/plain;charset=UTF-8
而我們需要的是:”application/json;charset=utf-8”時,我們也是可以通過重寫對應的方法對請求的頭部信息進行修改的,代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
/** * content-type=text/plain;charset=UTF-8 * 重寫getHeaders方法,實現自定義Content-Type; */ @Override public Enumeration<String> getHeaders(String name) { if (( null != name && name.equals( "Content-Type" ))||( null != name && name.equals( "content-type" ))) { return new Enumeration<String>() { private boolean hasGetted = false ; @Override public String nextElement() { if (hasGetted) { throw new NoSuchElementException(); } else { hasGetted = true ; return "application/json;charset=utf-8" ; } } @Override public boolean hasMoreElements() { return !hasGetted; } }; } return super .getHeaders(name); } |
當我們在后端設置了頭部信息后,如果不出意外,前端發送的請求將變為簡單請求,這樣,服務器的處理機制將簡單很多。
HttpServletRequestWrapper和HttpServletResponseWrapper使用時的坑
WrapperRequest和WrapperResponse的使用
在做JavaWeb開發過程中如果想拿到請求參數和返回數據的話我們就會使用到這兩個類,從類名上就可以看出是包裝類,通過這兩個類的包裝我們可以使用移花接木的方式獲取到對應的參數數據。
這里涉及到的坑
坑1
如果請求參數在Body內時取出參數后,后端程序就無法再次取出數據
這個和InputStream不能重復讀有關 ,這里需要將Request中的數據自己保存一份然后在使用的時候給出新的InputStream,這樣就可以避免使用同一個InputStream讀取完數據后無法重新讀取數據
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@Override public ServletInputStream getInputStream() throws IOException { //這里每次都重新創建了一個InputStream final ByteArrayInputStream inputStream = new ByteArrayInputStream(bodyData); return new ServletInputStream() { @Override public int read() throws IOException { return inputStream.read(); } @Override public boolean isFinished() { return false ; } @Override public boolean isReady() { return false ; } @Override public void setReadListener(ReadListener readListener) { } }; } |
坑2
使用HttpServletResponseWrapper包裝Response后無法返回數據或者無法返回html,css等數據
這個跟網上的教程有關,大多網上的教程是這樣的如下代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
//先定義一個Filter類包裝對應的request和response public class WrapperFilter extends OncePerRequestFilter { private static Logger logger = LoggerFactory.getLogger(WrapperFilter. class ); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { logger.debug( " request mapping {} {}" , request.getRequestURL(), request.getRequestURI()); RequestWrapper requestWrapper = new RequestWrapper(request); ResponseWrapper responseWrapper = new ResponseWrapper(response); filterChain.doFilter(requestWrapper, responseWrapper); } //response的包裝類 public class ResponseWrapper extends HttpServletResponseWrapper { private static Logger logger = LoggerFactory.getLogger(ResponseWrapper. class ); private final ByteArrayOutputStream buffer; private final ServletOutputStream out; private final PrintWriter writer; public ResponseWrapper(HttpServletResponse response) throws IOException { super (response); buffer = new ByteArrayOutputStream( 2048 ); out = new WrapperOutputStream(buffer); writer = new PrintWriter(buffer); } @Override public ServletOutputStream getOutputStream() throws IOException { return out; } /** * 當獲取字符輸出流時,實際獲取的是我們自己包裝的字符輸出流 */ @Override public PrintWriter getWriter() { return writer; } /** * 獲取返回的數據內容,這個是截獲的數據 */ public byte [] getResponseData() throws IOException { flushBuffer(); return buffer.toByteArray(); } public String getContent() throws IOException { flushBuffer(); return buffer.toString(); } } |
上面的代碼雖然是可以獲取到數據的但是,數據就無法返回到前端頁面了,那么為什么會出現這樣的問題呢,咱們來分析一下。
1、包裝類對response進行了包裝,并且重寫了getWriter和getOutputStream 這樣就可以保證后端使用response向前端寫數據時寫到我們定義的buffer中
2、這個包裝類是不范圍的,也就是只在WrapperFilter 之后有效,但是瀏覽器從response讀取數據明顯是在WrapperFilter的范圍之外的
也就是說瀏覽器從reponse讀取數據時無法使用ResponseWrapper而只能使用response 這個默認是ResponseFacade
3、那么問題來了咱們上面有往response中寫入數據嗎,顯然是沒有的也就是寫數據只在ResponseWrapper中有而ResponseFacade 是沒有數據的所以瀏覽器了就無法讀取到返回的數據啦。
清楚以上問題后問題就變得簡單得多了,那么我們只需要往ResponseFacade 中也定入一份數據就可以了
解決問題
Filter的內容不變
ResponseWrapper中的代碼如下修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public class ResponseWrapper extends HttpServletResponseWrapper { private static Logger logger = LoggerFactory.getLogger(ResponseWrapper. class ); private final ByteArrayOutputStream buffer; private final ServletOutputStream out; private final PrintWriter writer; public ResponseWrapper(HttpServletResponse response) throws IOException { super (response); buffer = new ByteArrayOutputStream( 2048 ); //這里將response也傳入了WrapperOutputStream 和 WrapperWriter out = new WrapperOutputStream(buffer, response); writer = new WrapperWriter(buffer, response); } @Override public ServletOutputStream getOutputStream() throws IOException { return out; } /** * 當獲取字符輸出流時,實際獲取的是我們自己包裝的字符輸出流 */ @Override public PrintWriter getWriter() { return writer; } public byte [] getResponseData() throws IOException { flushBuffer(); return buffer.toByteArray(); } public String getContent() throws IOException { flushBuffer(); return buffer.toString(); } } |
這里將response也傳入了WrapperOutputStream 和 WrapperWriter 這樣我們在做數據寫入的時候就可以同時向reponse中寫入數據了
這兩個類的實現如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
public class WrapperOutputStream extends ServletOutputStream { private OutputStream innerOut; private HttpServletResponse response; public WrapperOutputStream(OutputStream innerOut, HttpServletResponse response) { super (); this .response = response; this .innerOut = innerOut; } @Override public boolean isReady() { if (response == null ){ return false ; } try { return response.getOutputStream().isReady(); } catch (IOException e) { e.printStackTrace(); } return false ; } @Override public void setWriteListener(WriteListener listener) { if (response != null ){ try { ((CoyoteOutputStream)response.getOutputStream()).setWriteListener(listener); } catch (IOException e) { e.printStackTrace(); } } } //關鍵在這里 @Override public void write( int b) throws IOException { if (response != null ){ response.getOutputStream().write(b); } innerOut.write(b); } } //另一個類 public class WrapperWriter extends PrintWriter { private HttpServletResponse response; ByteArrayOutputStream output; public WrapperWriter(ByteArrayOutputStream out, HttpServletResponse response) { super (out); this .response = response; this .output = out; } //關鍵在這里 @Override public void write( int b){ super .write(b); try { response.getWriter().write(b); } catch (IOException e) { e.printStackTrace(); this .setError(); } } //關鍵在這里 @Override public void write(String s, int off, int len) { super .write(s,off,len); try { response.getWriter().write(s,off,len); } catch (IOException e) { e.printStackTrace(); this .setError(); } } } |
以上可以看到數據的寫入變成了寫兩份一份寫到了自定義的對象中一份寫到了response中這樣返回到前端的responnse就不會沒有數據了
到此問題完全解決,這里還需要注意的就是PrintWriter 有多個writer重載需要都進行重寫才行
問題延伸
有人會問能不能直接將response中的OutputStream和Writer獲取到分配給對應的WrapperOutputStream 和WrapperWriter而不是直接傳入response本身,答案是不可以的,response是不能同時獲取OutputStream和Writer的因為他們操作的是同一個數據,所以ResponseFacade 實現時對其進行了保護——同時只能獲取OutputStream和Writer中的一個。
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持服務器之家。
原文鏈接:https://blog.csdn.net/zj20142213/article/details/80012221