在Java Web開發中,Filter(過濾器)可攔截請求/響應流程,修改HTTP響應 是其核心應用場景之一(如統一添加響應頭、修改響應內容、處理字符編碼等)。由於HTTP響應流默認是單向、不可重複讀取的,修改響應內容需藉助 HttpServletResponseWrapper 包裝器實現,以下是完整的實現思路、代碼示例和注意事項。

一、核心原理

  1. Filter的執行時機:Filter在Servlet處理請求後、響應返回客户端前攔截響應,此時可對響應對象進行包裝和修改。
  2. 響應包裝器(HttpServletResponseWrapper):原生 HttpServletResponse 的輸出流(getWriter()/getOutputStream())直接指向客户端,無法緩存/修改內容。通過繼承 HttpServletResponseWrapper,替換輸出流為字節數組緩存流,先將響應內容寫入緩存,修改後再輸出到客户端。
  3. 核心方法
  • doFilter(ServletRequest, ServletResponse, FilterChain):Filter的核心方法,攔截請求/響應;
  • HttpServletResponseWrapper:重寫 getWriter()/getOutputStream(),替換為緩存流;
  • ByteArrayOutputStream:緩存響應內容,便於修改。

二、常見應用場景及實現

場景1:統一添加響應頭(最簡單場景)

需求:所有響應統一添加 Access-Control-Allow-Origin(跨域)、Content-Type(字符編碼)等響應頭。

1. 實現Filter
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

// 攔截所有請求(/*),也可指定特定路徑如/api/*
@WebFilter(filterName = "ResponseHeaderFilter", urlPatterns = "/*")
public class ResponseHeaderFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化邏輯(可選)
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        // 1. 將ServletResponse強轉為HttpServletResponse(Web場景下必轉)
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        // 2. 添加統一響應頭
        httpResponse.setHeader("Access-Control-Allow-Origin", "*"); // 跨域允許所有源
        httpResponse.setHeader("Content-Type", "text/html;charset=UTF-8"); // 統一字符編碼
        httpResponse.setHeader("X-Powered-By", "Java Filter"); // 自定義響應頭
        
        // 3. 放行請求,讓Servlet處理並生成響應
        chain.doFilter(request, httpResponse);
        
        // 注意:添加響應頭需在chain.doFilter前執行,否則響應已提交,無法修改
    }

    @Override
    public void destroy() {
        // 銷燬邏輯(可選)
    }
}
場景2:修改響應內容(核心場景)

需求:攔截Servlet返回的HTML/JSON內容,替換指定文本(如將所有「Java」替換為「Java Web」,或統一添加版權信息)。

1. 自定義響應包裝器(關鍵)

繼承 HttpServletResponseWrapper,緩存響應內容到字節數組:

import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;

/**
 * 自定義響應包裝器:緩存響應內容,便於修改
 */
public class CustomResponseWrapper extends HttpServletResponseWrapper {
    // 緩存響應內容的字節數組流
    private final ByteArrayOutputStream byteArrayOutputStream;
    private ServletOutputStream servletOutputStream;
    private PrintWriter printWriter;

    public CustomResponseWrapper(HttpServletResponse response) {
        super(response);
        byteArrayOutputStream = new ByteArrayOutputStream();
    }

    // 重寫getOutputStream():替換為緩存流(二進制響應如文件、圖片)
    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (printWriter != null) {
            throw new IllegalStateException("已調用getWriter(),不可重複獲取輸出流");
        }
        if (servletOutputStream == null) {
            servletOutputStream = new CustomServletOutputStream(byteArrayOutputStream);
        }
        return servletOutputStream;
    }

    // 重寫getWriter():替換為緩存流(字符響應如HTML/JSON)
    @Override
    public PrintWriter getWriter() throws IOException {
        if (servletOutputStream != null) {
            throw new IllegalStateException("已調用getOutputStream(),不可重複獲取Writer");
        }
        if (printWriter == null) {
            // 指定UTF-8編碼,避免中文亂碼
            printWriter = new PrintWriter(byteArrayOutputStream, true, StandardCharsets.UTF_8);
        }
        return printWriter;
    }

    // 重寫flushBuffer():確保內容寫入緩存
    @Override
    public void flushBuffer() throws IOException {
        if (servletOutputStream != null) {
            servletOutputStream.flush();
        }
        if (printWriter != null) {
            printWriter.flush();
        }
    }

    // 獲取緩存的響應內容(字節數組)
    public byte[] getResponseBytes() {
        flushBuffer();
        return byteArrayOutputStream.toByteArray();
    }

    // 獲取緩存的響應內容(字符串,指定編碼)
    public String getResponseString() {
        return new String(getResponseBytes(), StandardCharsets.UTF_8);
    }

    /**
     * 自定義ServletOutputStream:將內容寫入ByteArrayOutputStream
     */
    private static class CustomServletOutputStream extends ServletOutputStream {
        private final ByteArrayOutputStream outputStream;

        public CustomServletOutputStream(ByteArrayOutputStream outputStream) {
            this.outputStream = outputStream;
        }

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

        // 以下為Servlet 3.1+必需實現的方法(空實現即可,非核心)
        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setWriteListener(WriteListener listener) {}
    }
}
2. 實現Filter(修改響應內容)
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;

@WebFilter(filterName = "ResponseContentFilter", urlPatterns = "/hello")
public class ResponseContentFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        // 1. 包裝原生響應對象
        CustomResponseWrapper responseWrapper = new CustomResponseWrapper((HttpServletResponse) response);
        
        // 2. 放行請求:Servlet將響應內容寫入包裝器的緩存流
        chain.doFilter(request, responseWrapper);
        
        // 3. 讀取緩存的響應內容並修改
        String originalContent = responseWrapper.getResponseString();
        // 示例1:替換文本(如將「Hello」替換為「Hello Filter」)
        String modifiedContent = originalContent.replace("Hello", "Hello Filter");
        // 示例2:添加版權信息(HTML響應)
        modifiedContent += "<br/><small>© 2025 Java Web Filter</small>";
        
        // 4. 將修改後的內容寫入原生響應流,返回給客户端
        HttpServletResponse originalResponse = (HttpServletResponse) response;
        // 重置響應頭(避免原內容長度導致截斷)
        originalResponse.resetBuffer();
        // 設置響應長度(可選,優化傳輸)
        originalResponse.setContentLength(modifiedContent.getBytes().length);
        // 寫入修改後的內容
        try (OutputStream out = originalResponse.getOutputStream()) {
            out.write(modifiedContent.getBytes());
            out.flush();
        }
    }

    // init()和destroy()省略(默認空實現即可)
}
3. 測試Servlet(生成響應)
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.write("<h1>Hello Java</h1>"); // 原始響應內容
        out.flush();
    }
}
測試結果

訪問 http://localhost:8080/項目名/hello,頁面顯示:

<h1>Hello Filter Java</h1>
<br/><small>© 2025 Java Web Filter</small>
場景3:統一處理響應字符編碼

需求:強制所有響應使用UTF-8編碼,避免中文亂碼(結合包裝器確保編碼生效)。

@WebFilter("/*")
public class CharsetFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        // 設置請求編碼(可選,配套處理)
        request.setCharacterEncoding("UTF-8");
        // 設置響應編碼
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setCharacterEncoding("UTF-8");
        httpResponse.setContentType("text/html;charset=UTF-8");
        
        // 放行(若需修改內容,仍需包裝器)
        chain.doFilter(request, httpResponse);
    }
}

三、關鍵注意事項

  1. 響應流的互斥性getWriter()(字符流)和 getOutputStream()(字節流)不可同時調用,否則會拋出 IllegalStateException,包裝器中需做互斥判斷。
  2. 響應已提交的問題
  • 修改響應頭/內容必須在 chain.doFilter() 前(響應頭)或 chain.doFilter() 後(響應內容),且需確保 response.isCommitted()false(未提交);
  • 若調用 response.flushBuffer()/response.sendRedirect()/response.sendError(),響應會被提交,無法再修改。
  1. 二進制響應的處理
  • 若響應是圖片、文件等二進制內容,避免修改(或僅修改響應頭),否則會導致文件損壞;
  • 可通過 Content-Type 判斷響應類型,僅處理文本類型(如 text/htmlapplication/json)。
  1. 性能問題
  • 緩存響應內容會佔用內存,大響應(如大文件)不建議修改內容,僅修改響應頭;
  • 生產環境可限制包裝器僅攔截特定路徑(如 /api/*),避免全局攔截導致性能損耗。
  1. 編碼一致性:修改響應內容時,需確保編碼統一(如UTF-8),否則會出現中文亂碼。

四、Filter的配置方式

  1. 註解配置:使用 @WebFilter(Servlet 3.0+ 支持),無需web.xml;
  2. XML配置(兼容低版本):
<!-- web.xml中配置Filter -->
<filter>
    <filter-name>ResponseContentFilter</filter-name>
    <filter-class>com.example.filter.ResponseContentFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>ResponseContentFilter</filter-name>
    <url-pattern>/hello</url-pattern>
</filter-mapping>

五、擴展場景

  • 響應壓縮:通過包裝器將響應內容壓縮為GZIP格式,減少傳輸體積;
  • 異常統一響應:攔截Servlet拋出的異常,替換為自定義JSON響應(如 {"code":500,"msg":"服務器錯誤"});
  • 響應緩存控制:添加 Cache-Control/Expires 響應頭,控制瀏覽器緩存。

通過以上實現,可靈活控制Java Web的HTTP響應,滿足統一化、定製化的開發需求。核心是理解 HttpServletResponseWrapper 的緩存機制,以及Filter的執行時機。