首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java 文件下載中文檔名亂碼終極解決方案:跨瀏覽器兼容編碼策略

     編輯:Java 程式設計入門

問題根源:HTTP 標準與瀏覽器實現差異

RFC 5987 規定 Content-Disposition 標頭應使用 UTF-8 編碼,但歷史原因導致各瀏覽器解析方式不一:

  • Chrome/Firefox/Edge 現代版:支援 filename*=UTF-8''encoded_name 格式(RFC 5987)
  • IE / 舊版 Firefox:僅支援 filename="ISO-8859-1_encoded" 或 filename="GBK_encoded"
  • Safari:行為介於兩者之間

因此需要根據 User-Agent 動態選擇編碼策略。

核心工具類:智能檔名編碼器


import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

public final class FileNameEncoder {

    private FileNameEncoder() {}

    /**
     * 通用入口:自動識別瀏覽器並返回安全檔名
     */
    public static String encode(HttpServletRequest request, String originalName) {
        String agent = request.getHeader("User-Agent");
        boolean isIE = agent != null && (agent.contains("MSIE") || agent.contains("Trident"));
        boolean isFirefox = agent != null && agent.toLowerCase().contains("firefox");
        boolean isChrome = agent != null && agent.toLowerCase().contains("chrome");
        boolean isSafari = agent != null && agent.toLowerCase().contains("safari") && !isChrome;

        try {
            // 現代瀏覽器優先使用 RFC 5987 標準格式
            if (isFirefox || isChrome || isSafari) {
                return "filename*=UTF-8''" + URLEncoder.encode(originalName, StandardCharsets.UTF_8)
                        .replace("+", "%20") // 空格編碼為 %20 而非 +
                        .replace("*", "%2A")
                        .replace("'", "%27");
            }

            // IE 及舊版瀏覽器:ISO-8859-1 編碼
            if (isIE) {
                String encoded = URLEncoder.encode(originalName, StandardCharsets.UTF_8)
                        .replace("+", "%20");
                // IE 對過長檔名有限制,超過 150 字元需特殊處理
                if (encoded.length() > 150) {
                    return "filename=\"" + new String(originalName.getBytes(StandardCharsets.UTF_8), "ISO-8859-1") + "\"";
                }
                return "filename=\"" + encoded + "\"";
            }

            // 預設回退:雙重標頭兼容
            String utf8Encoded = "filename*=UTF-8''" + URLEncoder.encode(originalName, StandardCharsets.UTF_8)
                    .replace("+", "%20");
            String isoEncoded = "filename=\"" + new String(originalName.getBytes(StandardCharsets.UTF_8), "ISO-8859-1") + "\"";
            return utf8Encoded + "; " + isoEncoded;

        } catch (UnsupportedEncodingException e) {
            // 極端情況回退:僅保留 ASCII
            return "filename=\"" + originalName.replaceAll("[^\\x00-\\x7F]", "_") + "\"";
        }
    }

    /**
     * 直接用於 Response Header 設置
     */
    public static void setContentDisposition(HttpServletRequest request, HttpServletResponse response, String fileName) {
        response.setHeader("Content-Disposition", "attachment; " + encode(request, fileName));
    }
}

Spring MVC / Servlet 實戰整合

Controller 寫法


@GetMapping("/download")
public void download(HttpServletRequest request, HttpServletResponse response) throws IOException {
    String fileName = "產品手冊_2024版.pdf";
    String filePath = "/data/files/manual_2024.pdf";

    // 關鍵:設置編碼後的 Content-Disposition
    FileNameEncoder.setContentDisposition(request, response, fileName);
    response.setContentType("application/octet-stream");

    try (InputStream in = new FileInputStream(filePath);
         OutputStream out = response.getOutputStream()) {
        byte[] buffer = new byte[8192];
        int len;
        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
        out.flush();
    }
}

Spring Boot 靜態資源下載(ResourceHttpRequestHandler)


@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/download/**")
                .addResourceLocations("file:/data/files/")
                .resourceChain(true)
                .addResolver(new PathResourceResolver() {
                    @Override
                    protected Resource getResource(String resourcePath, Resource location) throws IOException {
                        Resource resource = super.getResource(resourcePath, location);
                        if (resource != null && resource.exists()) {
                            // 可在這裡攔截並設置 Header,但較複雜,建議用 Controller
                        }
                        return resource;
                    }
                });
    }
}

舊版代碼遷移指南(兼容原文邏輯)

原文提供的 toUtf8String 手動拼接 %XX 的方式屬於早期 URLEncoder 未普及時的產物,現已不推薦使用,理由:

  1. URLEncoder.encode 內部已正確處理 UTF-8 多字節字符,無需手動拆解 byte[]
  2. 手動實現易出錯(如補位、大小寫、保留字符集處理)
  3. JDK 內建經過嚴格測試,性能更優

若必須維護舊代碼,最小化改造


// 替換原手動編碼邏輯
public static String toUtf8String(String s) {
    try {
        return URLEncoder.encode(s, StandardCharsets.UTF_8).replace("+", "%20");
    } catch (UnsupportedEncodingException e) {
        throw new RuntimeException(e); // 不會發生
    }
}

測試驗證清單

瀏覽器測試檔名預期結果
Chrome 120+測試_文件(1).pdf彈出下載,檔名完整無亂碼
Firefox 115+报表_中文_测试.xlsx正常下載,編碼正確
IE 11 / Edge IE模式附件_测试_文档.doc下載正常,無亂碼
Safari 16+图片_测试_名称.png正常保存
移動端微信內置瀏覽器微信_文件_测试.pdf正常下載

最佳實踐總結

  1. 統一入口:專案建立 FileNameEncoder 工具類,所有下載接口強制調用。
  2. 雙標頭並發:filename*=UTF-8''...; filename="fallback" 最大兼容性。
  3. 長度保護:IE 限制 150 字元,超長自動截斷或改用 ISO-8859-1 編碼。
  4. 單元測試:用 MockHttpServletRequest 模擬不同 User-Agent 斷言 Header 值。
  5. 安全補充:檔名參數務必做路徑遍歷防護(filename.contains("..") 過濾)。

採用此方案,可徹底解決 99% 以上場景的中文檔名亂碼問題,且代碼簡潔、易維護、符合現代 HTTP 標準。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved