Request的包装类HttpServletRequestWrapper的使用说明
作者:SuperPurse
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
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
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”时,我们也是可以通过重写对应的方法对请求的头部信息进行修改的,代码如下:
/** * 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读取完数据后无法重新读取数据
@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等数据
这个跟网上的教程有关,大多网上的教程是这样的如下代码:
//先定义一个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中的代码如下修改
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中写入数据了
这两个类的实现如下:
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中的一个。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。