中文亂碼是 Java Web 開發者的「成人禮」。本質上是 **字符編碼不一致** 導致的字節流解碼錯誤。本文從 HTTP 協議層面劃分 **請求解碼、業務處理、響應編碼** 三大環節,提供 **Filter 統一過濾、容器配置、代碼規範** 三級解決方案,並深度解析 Tomcat 8/9/10、Spring Boot、Nginx 反向代理場景下的配置細節,徹底告別亂碼。
瀏覽器編碼 (UTF-8/GBK)
→ HTTP 請求體/URL/Header (字節流)
→ Tomcat Connector 解碼 (request.setCharacterEncoding / URIEncoding)
→ Servlet request.getParameter() (字符串)
→ Java 內存 (UTF-16 char[])
→ 業務邏輯 / 數據庫 (JDBC URL characterEncoding)
→ 響應字符串
→ Servlet response.setContentType / setCharacterEncoding
→ Tomcat 編碼輸出
→ 瀏覽器解碼渲染 (Content-Type charset)
核心原則:全鏈路統一 UTF-8。任何一環不一致即產生亂碼。
在 `web.xml` 或 `@WebFilter` 中配置,**必須是第一個 Filter** (或 Spring Boot `Ordered.HIGHEST_PRECEDENCE`),在讀取請求參數前執行。
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
@WebFilter(urlPatterns = "/*", dispatcherTypes = {DispatcherType.REQUEST, DispatcherType.FORWARD})
public class EncodingFilter implements Filter {
private static final String ENCODING = "UTF-8";
private static final String CONTENT_TYPE = "text/html;charset=" + ENCODING;
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
// 1. 設置響應編碼 (最早執行,防止提交響應頭)
response.setCharacterEncoding(ENCODING);
response.setContentType(CONTENT_TYPE);
// 2. 處理請求編碼
// POST: request.setCharacterEncoding 有效
// GET: 參數在 URL 查詢字符串,Tomcat 8+ 默認 ISO-8859-1 解碼,需 Wrapper 重新解碼
if ("POST".equalsIgnoreCase(request.getMethod())) {
request.setCharacterEncoding(ENCODING);
chain.doFilter(request, response);
} else {
// GET 請求包裝器:重新解碼 queryString
chain.doFilter(new HttpServletRequestWrapper(request) {
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
if (value == null) return null;
try {
// Tomcat 8+ 已按 ISO-8859-1 解碼為字符串,需轉回字節再按 UTF-8 解碼
return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
return value; // 降級
}
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) return null;
for (int i = 0; i < values.length; i++) {
try {
values[i] = URLDecoder.decode(values[i], StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException ignored) {}
}
return values;
}
}, response);
}
}
}
<!-- server.xml Connector 配置 -->
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
URIEncoding="UTF-8"
useBodyEncodingForURI="true" />
URIEncoding="UTF-8":強制 GET 請求 URL 查詢字符串按 UTF-8 解碼 (Tomcat 8+ 默認已為 UTF-8,但顯式配置更安全)。useBodyEncodingForURI="true":若請求體編碼已設置 (如 Filter 中 `request.setCharacterEncoding`),則用該編碼解碼 URL 參數 (兼容舊應用)。<!-- web.xml (Servlet 3.0+) -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <!-- Spring 提供 -->
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value> <!-- 強制覆蓋 request/response 編碼 -->
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
server:
servlet:
encoding:
charset: UTF-8
force: true
enabled: true
tomcat:
uri-encoding: UTF-8
# use-body-encoding-for-uri: true # 可選
# Spring Boot 會自動注冊 CharacterEncodingFilter (Ordered.HIGHEST_PRECEDENCE)
# MySQL 8.0+ (默認 utf8mb4)
jdbc:mysql://localhost:3306/db?characterEncoding=utf8&useUnicode=true&serverTimezone=Asia/Shanghai
# 8.0+ 無需 characterEncoding,默認 utf8mb4,但顯式無害
# PostgreSQL
jdbc:postgresql://localhost:5432/db?charSet=utf-8
# Oracle (需確保數據庫字符集 AL32UTF8)
jdbc:oracle:thin:@localhost:1521:orcl
// Spring Boot
spring:
servlet:
multipart:
enabled: true
# file-size-threshold: 0 # 內存閾值
# max-file-size: 10MB
# max-request-size: 100MB
# Standard Servlet MultipartConfigElement 設置
@MultipartConfig(location="/tmp", fileSizeThreshold=1024*1024)
public class UploadServlet extends HttpServlet { ... }
**關鍵**:上傳組件 (Commons FileUpload / Servlet 3.0 Part) 需設置 `setHeaderEncoding("UTF-8")` 解析文件名/字段名。
location / {
proxy_pass https://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 關鍵:透傳編碼相關 Header,防止 Tomcat 誤判
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Accept-Encoding ""; # 禁用 gzip 讓後端處理 (可選)
}
| 維度 | POST | GET |
|---|---|---|
| 數據位置 | 請求體 | URL 查詢字符串 |
| Tomcat 默認解碼 | ISO-8859-1 (需 `request.setCharacterEncoding` 覆蓋) | ISO-8859-1 (Tomcat 7) / UTF-8 (Tomcat 8+ 默認) |
| Filter 生效點 | `request.setCharacterEncoding()` 有效 | 無效!需 Wrapper 重新解碼或 Tomcat `URIEncoding` |
| 最佳實踐 | Filter 統一設置 | Tomcat `URIEncoding="UTF-8"` + Filter Wrapper 雙保險 |
$ arthas
[arthas@12345]$ watch org.apache.catalina.connector.Request setCharacterEncoding '{params, returnObj, throwExp}' -x 2
[arthas@12345]$ trace org.springframework.web.filter.CharacterEncodingFilter doFilter
[arthas@12345]$ getstatic org.apache.catalina.connector.CoyoteRequest URIEncoding
// 實時查看編碼設置是否生效、請求參數解碼後的真實值
Java Web 中文亂碼的本質是 **編碼協議不一致**。解決之道是建立 **全鏈路 UTF-8 標準**:
遵循上述規範,配合 Arthas 線上驗證,可徹底消滅中文亂碼,構建穩健的國際化 Web 應用。