Springboot应用中过滤器chain.doFilter后设置header无效&filterChain.doFilter后使用response对象引起的问题&Filter过滤器执行流程

本文是在使用过滤器添加动态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还是先经过过滤器才能到达我们的客户端,这里我们进行一个代码的演示,看看具体执行流程。首先给出一个图。

img

这里我们通过实现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>

imgimg

可以看出,但我们客户端发起请求的时候,首先是经过了Filter过滤器,处理了request请求,然后去执行了我们的servlet/jsp,当执行完毕后,我们的response响应也经过了过滤器,这里经过过滤器的时候是从放行后面开始执行的,也就是处理了response响应(注意并没有再次处理request请求)。

猜你喜欢

转载自blog.csdn.net/qq_43842093/article/details/135142585