本文是在使用过滤器添加动态header过程中遇到设置header无效,经过研究源码而产生。
因为特殊需求,自定义的header必须在经过Controller处理之后,才能确定,所以不能在请求处理之前设置,必须在请求处理之后。于是出现了这个坑。
问题分析
在springboot中添加过滤器后,如果需要在过滤器中给response对象添加header,那么一定要在chain.doFilter(request, httpServletResponse);
之前添加,在这个一句后面添加将无效。这和过滤器的处理流程以及对header的处理时机有关。
首先过滤器链的处理流程是:进入到一个过滤器的doFitler方法中,处理一些逻辑,然后调用chain.doFilter(request, httpServletResponse);进入到过滤器链的下一个过滤器的doFilter方法中.当在过滤器链上最后一个过滤器的doFilter方法中调用chain.doFilter(request, httpServletResponse);时,将会把请求转发到Servlet中,再分配到对应的Controller的方法中。当从Controller的方法中退出,再回到最后一个过滤器的doFilter方法中之前,就将会把respone对象上的header写入到headerBuffer中。
所以,在chain.doFilter()方法之后,一方面是给response对象设置header不会成功,因为发现response对象的状态已经是commited状态,就不会再写入到headers里,另一方面,即便通过反射的方式写入了,也不会输出给客户端,因为headers已经处理过了。
导致 response 状态变为 committed 的原因:
send***这类方法:向客户端发送状态码或重定向会直接提交响应。
刷新缓存:当response对象缓存区满时,或者使用response对象的flushbuffer方法会刷新response对象的缓存导致响应提交。
转发:将未提交的response通过forward转发可能会在转发目标的处理流程内被提交(include转发不会)。
forward指令和include指令很相似,它们都采用方法来导入目标。
执行forward指令时,response必须未提交,目标获得的response与原Servlet中是同一个(ResponseFacade对象)。原先存放在response对象中的内容将会自动被清除,目标可以直接发出响应,之后程序流程回到原Servlet转发处继续执行,但是原Servlet似乎连页面内容都不可输出了。
而执行include指令时,目标获得的response与原Servlet中不是同一个(被换成了一个ApplicationHttpResponse对象,让目标无法对源请求做出实质响应,但是该对象进行提交操作后会导致原Servlet中的response对象也变为已提交,但仍然可以进行页面输出)。原Servlet把目标产生的响应的内容部分包含到自身响应的内容中,目标改变响应消息的状态码和响应头的语句执行结果将被忽略(即被调用的Servlet的响应只有内容部分会并入原Servlet的响应的内容部分中)。
关于forward和include的详细分析不在此处深究。
对于当前页面中已经committed(提交)的response:
就不能再使用这个response向缓冲区写任何东西 。(原文这里可能有错误)
不可以再进行send***这类发送响应内容的操作(因为响应已经提交给客户端),
可以使用set***这类设置响应内容的函数(设置后无效,因为响应已经提交给客户端),
实测可以继续进行页面内容的输出(–此处存疑–不能理解–实测(执行response.getWriter().close()后会导致后续输出无效,但不会爆异常)),
实测可以进行request.getRequestDispatcher(“”).include(request, response);,
实测不可以进行request.getRequestDispatcher(“”).forward(request, response);(会抛出IllegalStateException异常Cannot forward after response has been committed),
(注:以为JSP中,response是一个JSP页面的内置对象,所以同一个页面中的response.XXX()是同一个response的不同方法,只要其中一个已经导致了committed,那么其它类似方式的调用都会导致 IllegalStateException异常)。
源码展示
逻辑起始入口在org.apache.coyote.Response的sendHeaders()方法:
1 public void sendHeaders() {
2 action(ActionCode.COMMIT, this);
3 setCommitted(true);
4 }
参考:
https://blog.csdn.net/woslx/article/details/100540958
https://www.cnblogs.com/Leroscox/p/8305141.html
filterChain.doFilter后使用response对象引起的问题
1、问题出现的现象:
在旧版接口平台产品中发现原来的计费通过过滤器实现,在计费检查通过后,请求发送到servlet进行处理,然后根据servlet处理结果判断是否需要计费,如果计费失败则抛出异常(暂不考虑计费流程设计的瑕疵,这里仅讨论问题及解决方案)
public class FeesFilter extends OncePerRequestFilter {
// 之前有一些计费检查的逻辑
filterChain.doFilter(request, response);
// 此处进行扣费,如果费用不足向客户端输出错误响应
if (!consumeFeignResponse.success()) {
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getOutputStream().println(new String(JacksonUtil.toJson(responseBody).getBytes(), StandardCharsets.ISO_8859_1));
}
}
如果代码按照现有的逻辑走,会出现以下现象
{
"code": "OK",
"msg": "成功",
"request_id": "1a7decfbe849ea47",
"data": "xxxx"
}
{
"code": "fees.INSUFFICIENT_FEES",
"msg": "费用不足",
"sign_type": "md5",
"request_id": "f604fd519a7f35fc"
}
出现这样问题的原因是因为在servletresponse接口中有这么一个属性
/**
* Returns a boolean indicating if the response has been committed. A
* committed response has already had its status code and headers written.
*
* @return a boolean indicating if the response has been committed
* @see #setBufferSize
* @see #getBufferSize
* @see #flushBuffer
* @see #reset
*/
public boolean isCommitted();
当isCommitted=true时,响应已经提供给客户端,所以无法调用reset()来重置response中的内容,再调用输出流只会追加输出内容。
response.reset();
2、解决方案
该方案也是摘抄自网络,非原创
问题的来源在于在filterChain中传入的response已经写入了不该写入的数据,所以可以通过包装类,使用自定义的字节流代替原有的字节流,再根据逻辑判断该输出包装类里面的字节流还是另输出异常信息。
// 之前过滤链可以修改成这样
ResponseWrapper responseWrapper = new ResponseWrapper(response);
filterChain.doFilter(request, responseWrapper);
// 如果计费失败使用response输出异常信息
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getOutputStream().println(new String(JacksonUtil.toJson(responseBody).getBytes(), StandardCharsets.ISO_8859_1));
// 如果计费成功,输出包装类里的接口数据
response.getOutputStream().write(responseWrapper.getBuffer());
public class ResponseWrapper extends HttpServletResponseWrapper {
private final HttpServletResponse response;
private final ByteArrayOutputStream bout = new ByteArrayOutputStream();// 字节流
public ResponseWrapper(HttpServletResponse response) {
super(response);
this.response = response;
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new ResponseWrapperServletOutputStream(bout);
}
public byte[] getBuffer() {
try {
if (pw != null) {
// 如果pw不为空,则我们需要关闭一下,让其将数据从缓存写到底层流中去
pw.close();
}
bout.flush();
return bout.toByteArray();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
class ResponseWrapperServletOutputStream extends ServletOutputStream {
private final ByteArrayOutputStream bout;
public ResponseWrapperServletOutputStream(ByteArrayOutputStream bout) {
this.bout = bout;
}
@Override
public void write(int arg0) throws IOException {
this.bout.write(arg0);
}
}
Filter过滤器执行流程
我们讲解Filter的执行流程,从下图可以大致了解到,当客户端发送请求的时候,会经过过滤器,然后才能到我们的servlet,当我们的servlet处理完请求之后,我们的response还是先经过过滤器才能到达我们的客户端,这里我们进行一个代码的演示,看看具体执行流程。首先给出一个图。
这里我们通过实现Filter接口,来进行定义过滤器类,通过注解来配置该过滤器拦截的路径。
package com.zhiying.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/*")
public class FilterDemo2 implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 对request对象的请求进行处理
System.out.println("处理了request请求");
// 放行
chain.doFilter(request,response);
// 对response对象的响应进行处理
System.out.println("处理了response响应");
}
}
然后给出我们的index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
index.jsp...
<%
System.out.println("index.jsp...");
%>
</body>
</html>
可以看出,但我们客户端发起请求的时候,首先是经过了Filter过滤器,处理了request请求,然后去执行了我们的servlet/jsp,当执行完毕后,我们的response响应也经过了过滤器,这里经过过滤器的时候是从放行后面开始执行的,也就是处理了response响应(注意并没有再次处理request请求)。