自定义过滤器中修改的HttpServletResponse被截断为原始响应长度

时间:2018-02-08 08:51:57

标签: java spring spring-mvc spring-boot servlets

我创建了一个自定义过滤器,用于修改http响应的内容。内容替换本身工作正常,但是当内容大小大于原始响应时,它会被截断为与原始响应相同的大小,同时缺少剩余的字符。

这是我的自定义过滤器:

public class MyCustomFilter extends OncePerRequestFilter{

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    String actionHeader = request.getHeader(RestApi.ACTION_HEADER_NAME);
    if (actionHeader != null) {
        String contentTypeValue = ContentTypeMap.getContentType(actionHeader);
        if (contentTypeValue != null) { 
            try {
                response.setHeader(RestApi.ACTION_HEADER_NAME, "TRUE");
                log.debug("New Action - Action request detected: " + request.getRequestURL());
                ActionRequestWrapper actionRequest = new ActionRequestWrapper(request, contentTypeValue);
                ActionResponseWrapper actionResponse = new ActionResponseWrapper(response);

                performActionRequest(actionRequest);

                filterChain.doFilter(actionRequest, actionResponse);

                byte[] originalResponseBody = actionResponse.getResponseData();
                byte[] updatedBody = performAction(originalResponseBody);
                OutputStream os = response.getOutputStream();
                os.write(updatedBody);
                os.flush();
                os.close();
            }
            catch (Exception e) {
                log.error("New Action - An error occurred while trying to perform action",e);
            }
        }
        else {
            filterChain.doFilter(request, response);
        }
    }
    else {
        filterChain.doFilter(request, response);
    }
}

这是我的ResponseWrapper:

public class ActionResponseWrapper extends HttpServletResponseWrapper {

private final ByteArrayOutputStream capture;
private ServletOutputStream output;
private PrintWriter writer;

public ActionResponseWrapper(HttpServletResponse response) throws  IOException {
    super(response);
    capture = new ByteArrayOutputStream(response.getBufferSize());
}

@Override
public ServletOutputStream getOutputStream() {
    if (writer != null) {
        throw new IllegalStateException("getWriter() has already been called on this response.");
    }

    if (output == null) {
        output = new ServletOutputStream() {
            @Override
            public void write(int b) throws IOException {
                capture.write(b);
            }

            @Override
            public void flush() throws IOException {
                capture.flush();
            }

            @Override
            public void close() throws IOException {
                capture.close();
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setWriteListener(WriteListener arg0) {
            }
        };
    }

    return output;
}

@Override
public PrintWriter getWriter() throws IOException {
    if (output != null) {
        throw new IllegalStateException("getOutputStream() has already been called on this response.");
    }

    if (writer == null) {
        writer = new PrintWriter(new OutputStreamWriter(capture, getCharacterEncoding()));
    }

    return writer;
}

@Override
public void flushBuffer() throws IOException {
    super.flushBuffer();

    if (writer != null) {
        writer.flush();
    } else if (output != null) {
        output.flush();
    }
}

public byte[] getResponseData() throws IOException {
    if (writer != null) {
        writer.close();
    } else if (output != null) {
        output.close();
    }
    return capture.toByteArray();
}
}

这是我的RequestWrapper:

public class ActionRequestWrapper extends HttpServletRequestWrapper {

private final String contentTypeValue;
private final String contentLengthValue;
private byte[] body;

public ActionRequestWrapper(HttpServletRequest request, String contentType) throws IOException{
    super(request);
    contentTypeValue = contentType;
    StringBuilder _body = new StringBuilder();
    try (BufferedReader bufferedReader = request.getReader()) {
        String line;
        while ((line = bufferedReader.readLine()) != null)
            _body.append(line);
    }
    this.body = _body.toString().getBytes();
    contentLengthValue = String.valueOf(body.length);
}

@Override
public String getHeader(String name) {
    if (name.equalsIgnoreCase(CONTENT_TYPE_HEADER_NAME)) {
        return contentTypeValue;
    }
    else if (name.equalsIgnoreCase(CONTENT_LENGTH_HEADER_NAME)) {
        return contentLengthValue;
    }
    return super.getHeader(name);
}

@Override
public Enumeration<String> getHeaders(String headerName) {
    if (headerName.equalsIgnoreCase(CONTENT_TYPE_HEADER_NAME)) {
        return Collections.enumeration(Collections.singletonList(contentTypeValue));
    }
    else if (headerName.equalsIgnoreCase(CONTENT_LENGTH_HEADER_NAME)) {
        return Collections.enumeration(Collections.singletonList(contentLengthValue));
    }
    return super.getHeaders(headerName);
}

@Override
public Enumeration<String> getHeaderNames(){
    Enumeration<String> original = super.getHeaderNames();
    List<String> newHeaders = new ArrayList<String>(){
        @Override
        public boolean contains(Object o){
            String paramStr = (String)o;
            for (String headerName : this){
                if (headerName.equalsIgnoreCase(paramStr)) return true;
            }
            return false;
        }
    };

    newHeaders.addAll(Collections.list(original));

    if (!newHeaders.contains(CONTENT_TYPE_HEADER_NAME)){
        newHeaders.add(CONTENT_TYPE_HEADER_NAME);
    }
    if (!newHeaders.contains(CONTENT_LENGTH_HEADER_NAME)){
        newHeaders.add(CONTENT_LENGTH_HEADER_NAME);
    }
    return Collections.enumeration(newHeaders);
}

@Override
public ServletInputStream getInputStream() throws IOException {
    final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
    return new ServletInputStream() {
        @Override
        public boolean isFinished() {
            return byteArrayInputStream.available() > 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener listener) {

        }

        @Override
        public int read() throws IOException {
            return byteArrayInputStream.read();
        }
    };
}

@Override
public BufferedReader getReader() throws IOException {
    return new BufferedReader(new InputStreamReader(this.getInputStream()));
}

public byte[] getBody() {
    return body;
}

public void setBody(byte[] body) {
    this.body = body;
}
}

我已经创建了一个过滤器注册类,我已经注册了我的过滤器bean并配置了它们的顺序。一切正常,除了我的响应的更新主体被截断为原始响应的字节数这一事实。

我能够使用ControllerAdviceResponseBodyAdvice来完成这项工作,但由于我更喜欢​​使用过滤器,我想知道的是为什么我更新的响应正文被截断了,是否可以用过滤器解决这个问题我觉得这与我从org.apache.tomcat.embed:tomcat-embed-*-8.5.4升级到org.apache.tomcat.embed:tomcat-embed-*-8.5.20有关,但如果确实如此,我想知道改变了什么? 我正在使用Spring Boot Version 1.5.7

感谢帮助!

更新 使用ControllerAdviceResponseBodyAdvice使我的解决方案变得更加复杂,因为我的身体不一定是String,并且将我的身体定义为Object将需要对我当前的现有代码进行过多修改。深入挖掘后,我发现Content-Length中来自Http11Processor类的package org.apache.coyote.http11标头设置为响应提交之前的原始响应的大小,这就是为什么我的新响应被截断了:

Http11Processor.java:

@Override
protected final void prepareResponse() throws IOException {

    boolean entityBody = true;
    contentDelimitation = false;

    OutputFilter[] outputFilters = outputBuffer.getFilters();

    if (http09 == true) {
        // HTTP/0.9
        outputBuffer.addActiveFilter(outputFilters[Constants.IDENTITY_FILTER]);
        outputBuffer.commit();
        return;
    }

    int statusCode = response.getStatus();
    if (statusCode < 200 || statusCode == 204 || statusCode == 205 ||
            statusCode == 304) {
        // No entity body
        outputBuffer.addActiveFilter
            (outputFilters[Constants.VOID_FILTER]);
        entityBody = false;
        contentDelimitation = true;
        if (statusCode == 205) {
            // RFC 7231 requires the server to explicitly signal an empty
            // response in this case
            response.setContentLength(0);
        } else {
            response.setContentLength(-1);
        }
    }

    MessageBytes methodMB = request.method();
    if (methodMB.equals("HEAD")) {
        // No entity body
        outputBuffer.addActiveFilter
            (outputFilters[Constants.VOID_FILTER]);
        contentDelimitation = true;
    }

    // Sendfile support
    if (endpoint.getUseSendfile()) {
        prepareSendfile(outputFilters);
    }

    // Check for compression
    boolean isCompressible = false;
    boolean useCompression = false;
    if (entityBody && (compressionLevel > 0) && sendfileData == null) {
        isCompressible = isCompressible();
        if (isCompressible) {
            useCompression = useCompression();
        }
        // Change content-length to -1 to force chunking
        if (useCompression) {
            response.setContentLength(-1);
        }
    }

    MimeHeaders headers = response.getMimeHeaders();
    // A SC_NO_CONTENT response may include entity headers
    if (entityBody || statusCode == HttpServletResponse.SC_NO_CONTENT) {
        String contentType = response.getContentType();
        if (contentType != null) {
            headers.setValue("Content-Type").setString(contentType);
        }
        String contentLanguage = response.getContentLanguage();
        if (contentLanguage != null) {
            headers.setValue("Content-Language")
                .setString(contentLanguage);
        }
    }

    long contentLength = response.getContentLengthLong();
    boolean connectionClosePresent = false;
    if (contentLength != -1) {
        headers.setValue("Content-Length").setLong(contentLength);
        outputBuffer.addActiveFilter
            (outputFilters[Constants.IDENTITY_FILTER]);
        contentDelimitation = true;
    } else {
        // If the response code supports an entity body and we're on
        // HTTP 1.1 then we chunk unless we have a Connection: close header
        connectionClosePresent = isConnectionClose(headers);
        if (entityBody && http11 && !connectionClosePresent) {
            outputBuffer.addActiveFilter
                (outputFilters[Constants.CHUNKED_FILTER]);
            contentDelimitation = true;
            headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED);
        } else {
            outputBuffer.addActiveFilter
                (outputFilters[Constants.IDENTITY_FILTER]);
        }
    }

    if (useCompression) {
        outputBuffer.addActiveFilter(outputFilters[Constants.GZIP_FILTER]);
        headers.setValue("Content-Encoding").setString("gzip");
    }
    // If it might be compressed, set the Vary header
    if (isCompressible) {
        // Make Proxies happy via Vary (from mod_deflate)
        MessageBytes vary = headers.getValue("Vary");
        if (vary == null) {
            // Add a new Vary header
            headers.setValue("Vary").setString("Accept-Encoding");
        } else if (vary.equals("*")) {
            // No action required
        } else {
            // Merge into current header
            headers.setValue("Vary").setString(
                    vary.getString() + ",Accept-Encoding");
        }
    }

    // Add date header unless application has already set one (e.g. in a
    // Caching Filter)
    if (headers.getValue("Date") == null) {
        headers.addValue("Date").setString(
                FastHttpDateFormat.getCurrentDate());
    }

    // FIXME: Add transfer encoding header

    if ((entityBody) && (!contentDelimitation)) {
        // Mark as close the connection after the request, and add the
        // connection: close header
        keepAlive = false;
    }

    // This may disabled keep-alive to check before working out the
    // Connection header.
    checkExpectationAndResponseStatus();

    // If we know that the request is bad this early, add the
    // Connection: close header.
    if (keepAlive && statusDropsConnection(statusCode)) {
        keepAlive = false;
    }
    if (!keepAlive) {
        // Avoid adding the close header twice
        if (!connectionClosePresent) {
            headers.addValue(Constants.CONNECTION).setString(
                    Constants.CLOSE);
        }
    } else if (!http11 && !getErrorState().isError()) {
        headers.addValue(Constants.CONNECTION).setString(Constants.KEEPALIVE);
    }

    // Add server header
    if (server == null) {
        if (serverRemoveAppProvidedValues) {
            headers.removeHeader("server");
        }
    } else {
        // server always overrides anything the app might set
        headers.setValue("Server").setString(server);
    }

    // Build the response header
    try {
        outputBuffer.sendStatus();

        int size = headers.size();
        for (int i = 0; i < size; i++) {
            outputBuffer.sendHeader(headers.getName(i), headers.getValue(i));
        }
        outputBuffer.endHeaders();
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        // If something goes wrong, reset the header buffer so the error
        // response can be written instead.
        outputBuffer.resetHeaderBuffer();
        throw t;
    }

    outputBuffer.commit();
}

有没有办法可以某种方式将Content-Length标题的值更改为新响应的长度?

2 个答案:

答案 0 :(得分:0)

ActionResponseWrapper更改以下行

capture = new ByteArrayOutputStream(response.getBufferSize());

capture = new ByteArrayOutputStream();

因为通过提供ByteArrayOutputStream的大小,您将其大小限制为原始响应内容长度。因此,ActionResponseWrapper.getResponseData将仅返回原始响应长度。

答案 1 :(得分:0)

尝试:

public ActionResult Index()
{
   var adminModel = new AdminModel();
   adminModel.Notifications = new List<Notification>();
   return View(adminModel);
}

然后它将不再截断。