歷史項目無源碼、IP 寫死在編譯後類中,反編譯修改不可行。靈機一動:IP 最終以字符串形式輸出到頁面,**攔截響應輸出流,字符串替換後再寫入客戶端**。本文提供基於 HttpServletResponseWrapper 包裝輸出流 + Filter 替換內容的完整可用方案。
ResponseWrapper extends HttpServletResponseWrapper,內部用 ByteArrayOutputStream 緩衝所有輸出數據。getOutputStream()、getWriter() 返回包裝流,攔截 ServletOutputStream.write() 和 PrintWriter.write()。HttpServletResponse 返回客戶端。package org.mice.ipchange;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
public class ResponseWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
private ServletOutputStream servletOutputStream;
private PrintWriter printWriter;
public ResponseWrapper(HttpServletResponse response) throws IOException {
super(response);
// 包裝 ServletOutputStream,寫入時導向 buffer
servletOutputStream = new ServletOutputStream() {
@Override public void write(int b) throws IOException { buffer.write(b); }
@Override public void write(byte[] b, int off, int len) throws IOException { buffer.write(b, off, len); }
@Override public void write(byte[] b) throws IOException { buffer.write(b); }
@Override public void flush() throws IOException { buffer.flush(); }
@Override public void close() throws IOException { buffer.close(); }
};
// 包裝 PrintWriter,同樣導向 buffer
printWriter = new PrintWriter(new OutputStreamWriter(buffer, getCharacterEncoding()));
}
@Override public ServletOutputStream getOutputStream() throws IOException { return servletOutputStream; }
@Override public PrintWriter getWriter() throws UnsupportedEncodingException { return printWriter; }
@Override public void flushBuffer() throws IOException {
if (servletOutputStream != null) servletOutputStream.flush();
if (printWriter != null) printWriter.flush();
}
@Override public void reset() { buffer.reset(); super.reset(); }
// 供 Filter 獲取完整響應數據
public byte[] getResponseData() throws IOException {
flushBuffer(); // 強制刷新兩個流
return buffer.toByteArray();
}
}
package org.mice.ipchange;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import javax.servlet.*;
import javax.servlet.http.*;
public class IPChangeFilter implements Filter {
private static final String BUNDLE_NAME = "org.mice.ipchange.ipchange";
private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 防亂碼:強制指定編碼 (根據項目實際調整)
response.setContentType("text/html;charset=GBK");
HttpServletResponse httpResp = (HttpServletResponse) response;
ResponseWrapper wrapper = new ResponseWrapper(httpResp);
// 1. 放行鏈路,業務邏輯寫入 wrapper 緩衝區
chain.doFilter(request, wrapper);
// 2. 獲取完整響應內容
String content = new String(wrapper.getResponseData(), "GBK"); // 指定解碼編碼
// 3. 替換邏輯
String replaced = replaceText(content);
// 4. 寫入真實響應
httpResp.setContentType("text/html;charset=GBK");
try (PrintWriter out = httpResp.getWriter()) {
out.write(replaced);
}
}
private String replaceText(String content) {
Enumeration<String> keys = RESOURCE_BUNDLE.getKeys();
while (keys.hasMoreElements()) {
String oldVal = keys.nextElement();
String newVal = RESOURCE_BUNDLE.getString(oldVal);
content = content.replaceAll(oldVal, newVal); // 正則替換,若需精確匹配用 replace
}
return content;
}
public static String getString(String key) {
try { return RESOURCE_BUNDLE.getString(key); }
catch (MissingResourceException e) { return key; }
}
@Override public void init(FilterConfig config) throws ServletException {}
@Override public void destroy() {}
}
# 格式: 被替換的舊值 = 新值
# 支持正則,特殊字符需轉義
192\.168\.1\.100 = 203.0.113.50
old\.domain\.com = new.domain.com
localhost:8080 = prod.server.com
放置在 classpath 根目錄 (如 src/main/resources/org/mice/ipchange/ipchange.properties)。
<filter>
<filter-name>ipChangeFilter</filter-name>
<filter-class>org.mice.ipchange.IPChangeFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ipChangeFilter</filter-name>
<url-pattern>/*</url-pattern> <!-- 攔截所有請求 -->
</filter-mapping>
| 場景 | 問題 | 對策 |
|---|---|---|
| 二進制響應 (圖片/文件下載) | 字符串替換破壞二進制內容 | 在 Filter 中判斷 Content-Type 非文本類型直接放行不處理 |
| GZIP 壓縮響應 | 獲取到的是壓縮字節,無法替換 | 禁用壓縮,或在壓縮前攔截 (需更深層 Filter 順序) |
| 大響應體 (超大 HTML) | ByteArrayOutputStream 內存暴漲 | 設置閾值超限時降級不處理,或使用流式替換 (複雜) |
| 編碼不一致 | 解碼亂碼、替換失效 | 強制統一 GBK/UTF-8,從 response.getCharacterEncoding() 獲取 |
利用 Servlet Filter + ResponseWrapper 緩衝全量響應 + 字符串替換,在無源碼、無法修改業務邏輯的約束下,低成本實現 IP/域名等靜態字符串的動態替換。核心在於 包裝輸出流攔截寫入 與 配置驅動替換規則 的解耦設計。適用於遷移上雲、域名變更、內外網映射等場景。