java - 如何更改Java Filter中的HTTP响应内容长度 header

标签 java http servlets filter content-length

我编写了一个 Java HTTP 响应过滤器,在其中修改 HTTP 响应正文。由于我正在更改 HTTP 响应正文,因此我必须根据新内容更新响应中提交的 http 内容长度 header 。我按照以下方式进行。

response.setContentLength( next.getBytes().length );

听到下一个是一个字符串

但是,此方法无法设置 HTTP 响应的新内容长度。有人可以建议我在 Java 过滤器中完成它的正确方法是什么

package com.test;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.CharArrayWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;



public class DumpFilter implements Filter {

      private static class ByteArrayServletStream extends ServletOutputStream {

        ByteArrayOutputStream baos;

        ByteArrayServletStream(ByteArrayOutputStream baos) {
          this.baos = baos;
        }

        public void write(int param) throws IOException {
          baos.write(param);
        }
      }

      private static class ByteArrayPrintWriter {

        private ByteArrayOutputStream baos = new ByteArrayOutputStream();

        private PrintWriter pw = new PrintWriter(baos);

        private ServletOutputStream sos = new ByteArrayServletStream(baos);

        public PrintWriter getWriter() {
          return pw;
        }

        public ServletOutputStream getStream() {
          return sos;
        }

        byte[] toByteArray() {
          return baos.toByteArray();
        }
      }

      private class BufferedServletInputStream extends ServletInputStream {

        ByteArrayInputStream bais;

        public BufferedServletInputStream(ByteArrayInputStream bais) {
          this.bais = bais;
        }

        public int available() {
          return bais.available();
        }

        public int read() {
          return bais.read();
        }

        public int read(byte[] buf, int off, int len) {
          return bais.read(buf, off, len);
        }

      }

      private class BufferedRequestWrapper extends HttpServletRequestWrapper {

        ByteArrayInputStream bais;

        ByteArrayOutputStream baos;

        BufferedServletInputStream bsis;

        byte[] buffer;

        public BufferedRequestWrapper(HttpServletRequest req) throws IOException {
          super(req);
          InputStream is = req.getInputStream();
          baos = new ByteArrayOutputStream();
          byte buf[] = new byte[1024];
          int letti;
          while ((letti = is.read(buf)) > 0) {
            baos.write(buf, 0, letti);
          }
          buffer = baos.toByteArray();
        }

        public ServletInputStream getInputStream() {
          try {
            bais = new ByteArrayInputStream(buffer);
            bsis = new BufferedServletInputStream(bais);
          } catch (Exception ex) {
            ex.printStackTrace();
          }

          return bsis;
        }

        public byte[] getBuffer() {
          return buffer;
        }

      }

      private boolean dumpRequest;
      private boolean dumpResponse;

      public void init(FilterConfig filterConfig) throws ServletException {
        dumpRequest = Boolean.valueOf(filterConfig.getInitParameter("dumpRequest"));
        dumpResponse = Boolean.valueOf(filterConfig.getInitParameter("dumpResponse"));
      }

      public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
          FilterChain filterChain) throws IOException, ServletException {

        final HttpServletRequest httpRequest = (HttpServletRequest)servletRequest;
        BufferedRequestWrapper bufferedRequest= new BufferedRequestWrapper(httpRequest);

        if (dumpRequest) {
            System.out.println("REQUEST -> " + new String(bufferedRequest.getBuffer()));
        }

        final HttpServletResponse response = (HttpServletResponse) servletResponse;

        final ByteArrayPrintWriter pw = new ByteArrayPrintWriter();
        HttpServletResponse wrappedResp = new HttpServletResponseWrapper(response) {
          public PrintWriter getWriter() {
            return pw.getWriter();
          }

          public ServletOutputStream getOutputStream() {
            return pw.getStream();
          }

        };

        filterChain.doFilter(bufferedRequest, wrappedResp);

        byte[] bytes = pw.toByteArray();

        String s = new String(bytes);

        String next = "test message";

        response.getOutputStream().write(next.getBytes());
        ///response.setHeader("Content-Length", String.valueOf(next.length()));
        response.setContentLength( next.getBytes().length );
       // if (dumpResponse) System.out.println("RESPONSE -> " + s);
      }

      public void destroy() {}

    }

上面给出的是Filter类,但你可能不需要阅读整个类。以下是 doFilter 代码,我在其中修改 http 正文并设置字段的内容长度。

  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
              FilterChain filterChain) throws IOException, ServletException {

            final HttpServletRequest httpRequest = (HttpServletRequest)servletRequest;
            BufferedRequestWrapper bufferedRequest= new BufferedRequestWrapper(httpRequest);

            if (dumpRequest) {
                System.out.println("REQUEST -> " + new String(bufferedRequest.getBuffer()));
            }

            final HttpServletResponse response = (HttpServletResponse) servletResponse;

            final ByteArrayPrintWriter pw = new ByteArrayPrintWriter();
            HttpServletResponse wrappedResp = new HttpServletResponseWrapper(response) {
              public PrintWriter getWriter() {
                return pw.getWriter();
              }

              public ServletOutputStream getOutputStream() {
                return pw.getStream();
              }

            };

            filterChain.doFilter(bufferedRequest, wrappedResp);

            byte[] bytes = pw.toByteArray();

            String s = new String(bytes);

            String next = "test message";

            response.getOutputStream().write(next.getBytes());
            ///response.setHeader("Content-Length", String.valueOf(next.length()));
            response.setContentLength( next.getBytes().length );
           // if (dumpResponse) System.out.println("RESPONSE -> " + s);
          }

最佳答案

这是执行此操作的 Java 示例。它将响应存储在临时文件中,响应完成后该文件将被删除。此时它仅用于提供静态文件,因为它通过 url 路径临时缓存文件。请注意,它通过 url 路径将文件的长度存储在内存中,并在后续请求中使用它以避免 I/O。

请注意,如果在调用过滤器之前向响应正文写入了某些内容,则您的 Content-Length header 将被忽略。需要在写出任何内容之前设置此 header ,因此如果您发现它没有被添加,这就是原因。

像这样使用它:

new ContentLengthFilter("contentLengthFilter_", new File("/tmp/fileCache"))

ContentLengthFilter.java

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

import org.apache.commons.io.IOUtils;

/*
 * This filter adds a "Content-Length" header to all responses.
 * It does this by caching the response to a temporary file, which
 * is deleted immediately after the response completes.
 * 
 * It caches the size of the file to a hashmap, and uses that for
 * any matching requests that it encounters in the future, to decrease
 * the amount of I/O required. So the first request to a file is the
 * only one that does file I/O, the rest use the cache.
 * 
 * Note that it ignores queryString params when comparing responses.
 * If this is important to you, then you should override the getFilenameForUrl
 * method as required.
 */
public class ContentLengthFilter implements Filter
{
    protected ServletContext servletContext;
    protected final File tempDir;
    protected final Map<String, Long> contentLengths = new HashMap<String, Long>();
    protected final String filenamePrefix;

    public static final String CONTENT_LENGTH = "Content-Length";

    public ContentLengthFilter(String filenamePrefix, File tempDir)
    {
        this.filenamePrefix = filenamePrefix;

        this.tempDir = tempDir;
        this.tempDir.mkdirs();
    }

    private final static class BufferingOutputStreamFile extends ServletOutputStream
    {
        private FileOutputStream baos;

        public BufferingOutputStreamFile(File file)
        {
            try
            {
                baos = new FileOutputStream(file);
            }
            catch (FileNotFoundException e)
            {
                baos = null;
            }
        }

        @Override
        public void write(int b) throws IOException
        {
            baos.write(b);
        }

        @Override
        public void write(byte[] b) throws IOException
        {
            baos.write(b);
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException
        {
            baos.write(b, off, len);
        }
    }

    private final static class BufferingHttpServletResponse extends HttpServletResponseWrapper
    {
        private enum StreamType
        {
            OUTPUT_STREAM, WRITER
        }

        private final HttpServletResponse httpResponse;

        private StreamType acquired;
        private PrintWriter writer;
        private ServletOutputStream outputStream;
        private boolean savedResponseToTmpFile;
        private File file;

        public BufferingHttpServletResponse(HttpServletResponse response, File file)
        {
            super(response);
            this.file = file;
            httpResponse = response;
        }

        @Override
        public ServletOutputStream getOutputStream() throws IOException
        {
            if (acquired == StreamType.WRITER)
                throw new IllegalStateException("Character stream already acquired.");

            if (outputStream != null)
                return outputStream;

            if (alreadyHasContentLength())
            {
                outputStream = super.getOutputStream();
            }
            else
            {
                outputStream = new BufferingOutputStreamFile(file);
                savedResponseToTmpFile = true;
            }

            acquired = StreamType.OUTPUT_STREAM;
            return outputStream;
        }

        @Override
        public PrintWriter getWriter() throws IOException
        {
            if (acquired == StreamType.OUTPUT_STREAM)
                throw new IllegalStateException("Binary stream already acquired.");

            if (writer != null)
                return writer;

            if (alreadyHasContentLength())
            {
                writer = super.getWriter();
            }
            else
            {
                writer = new PrintWriter(new OutputStreamWriter(getOutputStream(), getCharacterEncoding()), false);
            }

            acquired = StreamType.WRITER;

            return writer;
        }

        private boolean alreadyHasContentLength()
        {
            return super.containsHeader(CONTENT_LENGTH);
        }

        public void copyTmpFileToOutput() throws IOException
        {
            if (!savedResponseToTmpFile)
                throw new IllegalStateException("Not saving response to temporary file.");

            // Get the file, and write it to the output stream
            FileInputStream fis = new FileInputStream(file);
            ServletOutputStream sos;
            try
            {
                long contentLength = file.length();
                httpResponse.setHeader(CONTENT_LENGTH, contentLength + "");

                sos = httpResponse.getOutputStream();
                IOUtils.copy(fis, sos);
            }
            finally
            {
                IOUtils.closeQuietly(fis);
                fis.close();
            }
        }
    }

    protected String getFilenameForUrl(HttpServletRequest request)
    {
        String result = filenamePrefix + request.getRequestURI();

        result = hashString(result);

        return result;
    }

    // Simple way to make a unique filename for an url. Note that
    // there could be collisions of course using this approach,
    // so use something better (e.g. MD5) if you want to avoid
    // collisions entirely. This approach is more readable, and
    // is why it's used.
    protected String hashString(String input)
    {
        String result = input.replaceAll("[^0-9A-Za-z]", "_");

        return result;
    }

    public void log(Object o)
    {
        System.out.println(o);
    }

    protected boolean setContentLengthUsingMap(String key, FilterChain chain, HttpServletResponse response) throws IOException, ServletException
    {
        Long contentLength = contentLengths.get(key);

        if (contentLength == null)
            return false;

        response.setHeader(CONTENT_LENGTH, contentLength + "");
        log("content-length from map:" + key + ", length:" + contentLength + ", entries:" + contentLengths.size());

        return true;
    }

    protected void writeFileToResponse(String filenameFromUrl, HttpServletRequest request, File file, BufferingHttpServletResponse wrappedResponse) throws IOException
    {
        Long contentLength = file.length();

        if (contentLength > 0)
        {
            log("Response written to temporary_file=" + filenameFromUrl + ", contentLength=" + contentLength);
            contentLengths.put(filenameFromUrl, contentLength);
        }
        else
        {
            log("Skipping caching response for temporary_file=" + filenameFromUrl + ", contentLength=" + contentLength);
        }

        wrappedResponse.copyTmpFileToOutput();

        String contentType = servletContext.getMimeType(request.getRequestURI());
        wrappedResponse.setContentType(contentType);
    }

    protected void deleteTempFileIfExists(File file)
    {
        if (file.exists())
        {
            try
            {
                file.delete();
            }
            catch (Exception e)
            {
                log(e);
            }
        }
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException
    {
        final HttpServletResponse response = (HttpServletResponse) resp;
        final HttpServletRequest request = (HttpServletRequest) req;
        final String filenameFromUrl = getFilenameForUrl(request);

        // If we've downloaded this file before, we saved it's
        // size, so write that out and skip caching the file locally
        // as it's not required.
        if (setContentLengthUsingMap(filenameFromUrl, chain, response))
        {
            chain.doFilter(request, response);
            return;
        }

        // We've never seen this request before, so download the response
        // to a temporary file, then write that file and it's
        // file size to the response.
        final File file = new File(tempDir, filenameFromUrl + UUID.randomUUID());
        try
        {
            final BufferingHttpServletResponse wrappedResponse = new BufferingHttpServletResponse(response, file);

            chain.doFilter(req, wrappedResponse);

            if (wrappedResponse.savedResponseToTmpFile)
            {
                writeFileToResponse(filenameFromUrl, request, file, wrappedResponse);
            }
        }
        finally
        {
            deleteTempFileIfExists(file);
        }
    }

    public void destroy()
    {
        this.servletContext = null;
    }

    public void init(FilterConfig config) throws ServletException
    {
        this.servletContext = config.getServletContext();
    }
}

用于执行此操作的另一个很棒的示例过滤器,可以在项目中独立使用,是 this ContentLengthFilter.java来自Carrot2 github上的项目。请注意,它工作得很好,但在写出每个文件时将其存储在内存中,因此如果您有大文件,则需要考虑不同的方法。

这使用带有字节流的响应包装器来解决问题,因此这也确保了 Transfer-Encoding: Chunked 不会被过滤器链中的其他过滤器/代码设置,并在设置后覆盖您的 Content-Length header 。您可以通过使用较大的文件进行测试来验证这一点,因为它们通常会在响应中分块。

我还将在此处复制文件的内容,以确保它不会成为损坏的链接。

/*
 * Carrot2 project.
 *
 * Copyright (C) 2002-2010, Dawid Weiss, Stanisław Osiński.
 * All rights reserved.
 *
 * Refer to the full license file "carrot2.LICENSE"
 * in the root folder of the repository checkout or at:
 * http://www.carrot2.org/carrot2.LICENSE
 */

package org.carrot2.webapp;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

/**
 * Buffer the output from filters below and set accurate <code>Content-Length</code>
 * header. This header is required by flash, among others, to display progress
 * information.
 */
public class ContentLengthFilter implements Filter
{
    private final static class BufferingOutputStream extends ServletOutputStream
    {
        private final ByteArrayOutputStream baos = new ByteArrayOutputStream();

        @Override
        public void write(int b) throws IOException
        {
            baos.write(b);
        }

        @Override
        public void write(byte [] b) throws IOException
        {
            baos.write(b);
        }

        @Override
        public void write(byte [] b, int off, int len) throws IOException
        {
            baos.write(b, off, len);
        }
    }

    private final static class BufferingHttpServletResponse extends
        HttpServletResponseWrapper
    {
        private enum StreamType
        {
            OUTPUT_STREAM,
            WRITER
        }

        private final HttpServletResponse httpResponse;

        private StreamType acquired;
        private PrintWriter writer;
        private ServletOutputStream outputStream;
        private boolean buffering;

        public BufferingHttpServletResponse(HttpServletResponse response)
        {
            super(response);
            httpResponse = response;
        }

        @Override
        public ServletOutputStream getOutputStream() throws IOException
        {
            if (acquired == StreamType.WRITER)
                throw new IllegalStateException("Character stream already acquired.");

            if (outputStream != null)
                return outputStream;

            if (hasContentLength())
            {
                outputStream = super.getOutputStream();
            }
            else
            {
                outputStream = new BufferingOutputStream();
                buffering = true;
            }

            acquired = StreamType.OUTPUT_STREAM;
            return outputStream;
        }

        @Override
        public PrintWriter getWriter() throws IOException
        {
            if (acquired == StreamType.OUTPUT_STREAM)
                throw new IllegalStateException("Binary stream already acquired.");

            if (writer != null)
                return writer;

            if (hasContentLength())
            {
                writer = super.getWriter();
            }
            else
            {
                writer = new PrintWriter(new OutputStreamWriter(
                    getOutputStream(), getCharacterEncoding()), false);
            }

            acquired = StreamType.WRITER;
            return writer;
        }

        /**
         * Returns <code>true</code> if the user set <code>Content-Length</code>
         * explicitly.
         */
        private boolean hasContentLength()
        {
            return super.containsHeader("Content-Length");
        }

        /**
         * Push out the buffered data.
         */
        public void pushBuffer() throws IOException
        {
            if (!buffering)
                throw new IllegalStateException("Not buffering.");

            BufferingOutputStream bufferedStream = 
                (BufferingOutputStream) outputStream;

            byte [] buffer = bufferedStream.baos.toByteArray();
            httpResponse.setContentLength(buffer.length);
            httpResponse.getOutputStream().write(buffer);
        }
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
        throws IOException, ServletException
    {
        final HttpServletResponse response = (HttpServletResponse) resp;
        final BufferingHttpServletResponse wrapped = 
            new BufferingHttpServletResponse(response);

        chain.doFilter(req, wrapped);

        if (wrapped.buffering)
        {
            wrapped.pushBuffer();
        }
    }

    public void destroy()
    {
        // Empty
    }

    public void init(FilterConfig config) throws ServletException
    {
        // Empty
    }
}

关于java - 如何更改Java Filter中的HTTP响应内容长度 header ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39767239/

相关文章:

angular - 如何使用 HttpTestingController 刷新 header ?

google-chrome - 为什么IE和Chrome显示的内容不一样?

java - 防止 Java 中的跨站请求伪造 (CSRF) 攻击

java - 如何在 Spring WebFlux Security(Reactive Spring Security) 配置中将多个用户角色添加到单个 pathMatcher/Route?

java - 无法在Java中重命名和删除文件

ruby-on-rails - 在我的 Rails 答案中删除不必要的 HTTP header

java - 我的代码的 For 循环出错?

java - Java EE 应用程序和过滤器中的编写器和输出流

java - 尝试使用 Tab 在 JTextField 之间垂直跳转(使用 Custom FocusTraversalPolicy)

java - Hadoop 中的 HashMap