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中的一个。