How to change the HTTP response content length header in Java Filter

喜欢而已 提交于 2019-12-02 11:53:19

问题


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

hear next is a string

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

    }

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

回答1:


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.

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.

Use it like so:

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

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.

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
    }
}


来源:https://stackoverflow.com/questions/39767239/how-to-change-the-http-response-content-length-header-in-java-filter

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!