如何在 Java 过滤器中更改 HTTP 响应内容长度标头 [英] How to change the HTTP response content length header in Java Filter

查看:34
本文介绍了如何在 Java 过滤器中更改 HTTP 响应内容长度标头的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我编写了一个 Java HTTP 响应过滤器,我正在其中修改 HTTP 响应正文.由于我正在更改 HTTP 响应正文,因此我必须根据新内容更新响应中提交的 http content-length 标头.我正在按以下方式进行操作.

I have written a Java HTTP Response filter in which I am modifying the HTTP response body. Since I am changing the HTTP responce body, I have to update the http content-length header filed in response in accordance with new content. I am doing it in the following way.

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

听到接下来是一个string

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

However, this method is unable to set the new content length of the HTTP response. Could somebody advice me whats the correct way to get it done in Java filter

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() {}

    }

上面给出的是过滤器类,但您可能不需要阅读整个类.以下是我正在修改 http 正文并设置内容长度的 doFilter 代码.

given above is the Filter class, but you may not need to read the whole class. Following is the doFilter code where I am modifying the http body and setting the content length filed.

  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.

Here's a Java sample that does this. It stores the response in a temporary file, which is deleted when the response completes. It is intended for serving static files only at this point, as it temporarily caches files by url path. Note that it stores the length of the file in memory, by url path, and uses that on subsequent requests to avoid I/O.

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

Note that if something writes to the response body before your filter gets called, then your Content-Length header will be ignored. This header needs to be set BEFORE any content is written out, so if you're finding it's not getting added, that's why.

像这样使用它:

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();
    }
}

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

Another great sample filter for doing this, that can be used standalone from the project, is this ContentLengthFilter.java from the Carrot2 project on github. Note that it works great, but stores each file in memory when writing it out, so if you have large files, you'll need to consider a different approach.

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

This uses a response wrapper with a byte stream to solve the issue, so this also ensures that Transfer-Encoding: Chunked doesn't get set by some other Filter/code in the filter chain, and override your Content-Length header when it's set. You can verify that by testing this with larger files, as they'd normally be chunked in the response.

我也将在这里复制文件的内容,以确保它不会成为断开的链接.

I'm going to copy the contents of the file here as well, to ensure it doesn't become a broken link.

/*
 * 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 过滤器中更改 HTTP 响应内容长度标头的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆