如何在Java Filter中更改HTTP响应内容长度标头

我编写了一个Java HTTP响应filter,我在其中修改HTTP响应主体。 由于我正在更改HTTP响应主体,因此我必须根据新内容更新响应的http内容长度标头。 我是按照以下方式做的。

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

听下一个string

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

 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.

请注意,如果在调用filter之前某些内容写入响应正文,那么将忽略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 contentLengths = new HashMap(); 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 (eg 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(); } } 

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

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

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

 /* * 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 Content-Length * 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 true if the user set Content-Length * 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 } }