在Java Web開發中,Filter(過濾器)可攔截請求/響應流程,修改HTTP響應 是其核心應用場景之一(如統一添加響應頭、修改響應內容、處理字符編碼等)。由於HTTP響應流默認是單向、不可重複讀取的,修改響應內容需藉助 HttpServletResponseWrapper 包裝器實現,以下是完整的實現思路、代碼示例和注意事項。
一、核心原理
- Filter的執行時機:Filter在Servlet處理請求後、響應返回客户端前攔截響應,此時可對響應對象進行包裝和修改。
- 響應包裝器(HttpServletResponseWrapper):原生
HttpServletResponse的輸出流(getWriter()/getOutputStream())直接指向客户端,無法緩存/修改內容。通過繼承HttpServletResponseWrapper,替換輸出流為字節數組緩存流,先將響應內容寫入緩存,修改後再輸出到客户端。 - 核心方法:
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);
}
}
三、關鍵注意事項
- 響應流的互斥性:
getWriter()(字符流)和getOutputStream()(字節流)不可同時調用,否則會拋出IllegalStateException,包裝器中需做互斥判斷。 - 響應已提交的問題:
- 修改響應頭/內容必須在
chain.doFilter()前(響應頭)或chain.doFilter()後(響應內容),且需確保response.isCommitted()為false(未提交); - 若調用
response.flushBuffer()/response.sendRedirect()/response.sendError(),響應會被提交,無法再修改。
- 二進制響應的處理:
- 若響應是圖片、文件等二進制內容,避免修改(或僅修改響應頭),否則會導致文件損壞;
- 可通過
Content-Type判斷響應類型,僅處理文本類型(如text/html、application/json)。
- 性能問題:
- 緩存響應內容會佔用內存,大響應(如大文件)不建議修改內容,僅修改響應頭;
- 生產環境可限制包裝器僅攔截特定路徑(如
/api/*),避免全局攔截導致性能損耗。
- 編碼一致性:修改響應內容時,需確保編碼統一(如UTF-8),否則會出現中文亂碼。
四、Filter的配置方式
- 註解配置:使用
@WebFilter(Servlet 3.0+ 支持),無需web.xml; - 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的執行時機。