Как изменить заголовок длины содержимого ответа HTTP в фильтре Java

Я написал фильтр ответа Java HTTP, в котором я изменяю тело ответа HTTP. Поскольку я меняю тело ответа HTTP, мне нужно обновить заголовок длины содержимого http, отправленный в ответ, в соответствии с новым содержимым. Я делаю это следующим образом.

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

услышать дальше будет string

Однако этот метод не может установить новую длину содержимого ответа 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);
          }

person KItis    schedule 29.09.2016    source источник
comment
Заголовок ответа длины содержимого описывает длину тела ответа. Какую ошибку вы получаете? Пожалуйста, опубликуйте код сервлета.   -  person Ankit Tripathi    schedule 29.09.2016
comment
У Writer, полученного от PrintWriter, нет возможности установить тип кодировки, и он использует кодировку по умолчанию, связанную с платформой. На мобильных платформах кодировка по умолчанию - UTF-16, поэтому next.getBytes().length вернет 2 раза длины следующего .   -  person Ankit Tripathi    schedule 30.09.2016


Ответы (1)


Вот пример Java, который делает это. Он сохраняет ответ во временном файле, который удаляется после завершения ответа. Он предназначен для обслуживания статических файлов только в этот момент, так как временно кэширует файлы по URL-адресу. Обратите внимание, что он сохраняет длину файла в памяти по URL-адресу и использует его при последующих запросах, чтобы избежать ввода-вывода.

Обратите внимание: если что-то запишется в тело ответа до того, как будет вызван ваш фильтр, ваш заголовок Content-Length будет проигнорирован. Этот заголовок должен быть установлен ДО того, как какой-либо контент будет записан, поэтому, если вы обнаружите, что он не добавляется, вот почему.

Используйте это так:

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. Обратите внимание, что он отлично работает, но сохраняет каждый файл в памяти при его записи, поэтому, если у вас есть большие файлы, вам нужно будет рассмотреть другой подход.

Это использует оболочку ответа с потоком байтов для решения проблемы, поэтому это также гарантирует, что Transfer-Encoding: Chunked не будет установлен каким-либо другим фильтром/кодом в цепочке фильтров и переопределит ваш заголовок Content-Length, когда он установлен. Вы можете убедиться в этом, протестировав это с файлами большего размера, поскольку они обычно разбиваются на части в ответе.

Я также скопирую сюда содержимое файла, чтобы убедиться, что ссылка не станет битой.

/*
 * 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
    }
}
person Brad Parks    schedule 08.09.2017
comment
Я успешно использовал второй вариант (Carrot2) - спасибо. - person Devology Ltd; 09.01.2019