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

Java URL 中文編碼解碼完整實現:手寫 UTF-8 Percent-Encoding 與解析

     編輯:Java 相關知識

在 URL 傳遞中文參數時,瀏覽器與服務端需遵循 Percent-Encoding(百分號編碼) 標準(RFC 3986)。本文提供一個零依賴、手寫實現的工具類,涵蓋 UTF-8 編碼為 `%E4%BD%A0` 格式、解碼還原中文,並解析核心算法原理,適合無法引入外部庫、或需理解底層機制的場景。

核心需求

  • 編碼:中文 → UTF-8 字節 → 每字節 `%XX` 大寫十六進制(如「你」→ `%E4%BD%A0`)。
  • 解碼:`%XX` 序列 → 字節數組 → UTF-8 解碼 → 字符串(支援多字節字符、ASCII 透傳、`+` 轉空格)。
  • 標準合規:RFC 3986 保留字符不編碼(`- _ . ~`),其餘非 ASCII 全編碼。

完整工具類

package com.example.util;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

/**
 * URL 編碼解碼工具類
 * 

* 提供標準庫版(推薦生產)與手寫版(教學/無依賴環境)雙實現。 *

*/ public final class UrlCodecUtil { private UrlCodecUtil() {} // ==================== 推薦:標準庫實現(JDK 內建、經過充分測試) ==================== /** * 標準 URL 編碼(application/x-www-form-urlencoded 規則) *

空格編碼為 +,保留字符 - _ . * 不編碼,其餘非 ASCII 按 UTF-8 %XX

*/ public static String encode(String input) { if (input == null) return ""; try { return URLEncoder.encode(input, StandardCharsets.UTF_8.name()); } catch (UnsupportedEncodingException impossible) { throw new IllegalStateException("UTF-8 not supported", impossible); } } /** * 標準 URL 解碼 */ public static String decode(String input) { if (input == null) return ""; try { return URLDecoder.decode(input, StandardCharsets.UTF_8.name()); } catch (UnsupportedEncodingException impossible) { throw new IllegalStateException("UTF-8 not supported", impossible); } } /** * RFC 3986 兼容編碼(路徑/查詢參數通用) *

空格編碼為 %20,保留字符 - _ . ~ 不編碼

*/ public static String encodeRfc3986(String input) { if (input == null) return ""; String encoded = encode(input); return encoded.replace("+", "%20").replace("*", "%2A").replace("%7E", "~"); } // ==================== 教學:手寫 UTF-8 Percent-Encoding 實現 ==================== /** * 手寫編碼:字符串 → UTF-8 字節 → %XX 大寫十六進制 *

ASCII 可見字符(33-126)保留字符外直接輸出,其餘全編碼

*/ public static String manualEncode(String input) { if (input == null) return ""; StringBuilder sb = new StringBuilder(input.length() * 3); for (char c : input.toCharArray()) { // ASCII 可打印且非保留字符:直接輸出 if (c >= 33 && c <= 126 && !isReservedChar(c)) { sb.append(c); } else { // 非 ASCII 或保留字符:UTF-8 編碼後逐字節 %XX byte[] bytes; try { bytes = String.valueOf(c).getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { bytes = new byte[0]; } for (byte b : bytes) { int unsigned = b & 0xFF; // 無符號字節值 0-255 sb.append('%'); sb.append(Character.forDigit((unsigned >> 4) & 0xF, 16)); sb.append(Character.forDigit(unsigned & 0xF, 16)); } } } return sb.toString(); } /** * 判斷 RFC 3986 保留字符:! * ' ( ) ; : @ & = + $ , / ? # [ ] */ private static boolean isReservedChar(char c) { return "!*'();:@&=+$,/?#[]".indexOf(c) >= 0; } /** * 手寫解碼:%XX 序列 → 字節數組 → UTF-8 解碼 *

支援多字節字符(中文 3 字節)、ASCII 透傳、+ 轉空格

*/ public static String manualDecode(String input) { if (input == null) return ""; byte[] bytes = new byte[input.length()]; // 最大可能長度 int byteCount = 0; for (int i = 0; i < input.length(); i++) { char c = input.charAt(i); if (c == '%' && i + 2 < input.length()) { // 解析兩位十六進制 int high = hexToInt(input.charAt(i + 1)); int low = hexToInt(input.charAt(i + 2)); if (high != -1 && low != -1) { bytes[byteCount++] = (byte) ((high << 4) | low); i += 2; continue; } } else if (c == '+') { bytes[byteCount++] = (byte) ' '; // + 轉空格 continue; } // 普通 ASCII 字符直接存入 if (c <= 0x7F) { bytes[byteCount++] = (byte) c; } else { // 非 ASCII 直接字符(極少見),按 UTF-8 編碼存入 try { byte[] extra = String.valueOf(c).getBytes("UTF-8"); System.arraycopy(extra, 0, bytes, byteCount, extra.length); byteCount += extra.length; } catch (UnsupportedEncodingException ignored) {} } } // 截取有效長度並 UTF-8 解碼 byte[] validBytes = new byte[byteCount]; System.arraycopy(bytes, 0, validBytes, 0, byteCount); try { return new String(validBytes, "UTF-8"); } catch (UnsupportedEncodingException e) { return new String(validBytes, StandardCharsets.ISO_8859_1); // 降級 } } private static int hexToInt(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'A' && c <= 'F') return c - 'A' + 10; if (c >= 'a' && c <= 'f') return c - 'a' + 10; return -1; } // ==================== 測試入口 ==================== public static void main(String[] args) { String original = "你好 世界!Hello World"; System.out.println("原文: " + original); // 標準庫 String encoded = encode(original); System.out.println("標準編碼: " + encoded); System.out.println("標準解碼: " + decode(encoded)); // 手寫版 String manualEncoded = manualEncode(original); System.out.println("手寫編碼: " + manualEncoded); System.out.println("手寫解碼: " + manualDecode(manualEncoded)); // 驗證雙向一致性 assert original.equals(decode(encoded)); assert original.equals(manualDecode(manualEncoded)); System.out.println("✅ 雙向驗證通過"); } }

算法核心解析

編碼流程

  1. 遍歷字符串每個字符(`char`,UTF-16 代碼單元)。
  2. 判斷是否為 非保留 ASCII 可打印字符(`!` `*` `'` `(` `)` `;` `:` `@` `&` `=` `+` `$` `,` `/` `?` `#` `[` `]` 除外),是則直接輸出。
  3. 否則:`String.valueOf(char).getBytes("UTF-8")` 獲取 UTF-8 字節序列(中文 3 字節、Emoji 4 字節)。
  4. 每字節 `b & 0xFF` 轉無符號整數,高 4 位 + 低 4 位轉十六進制字符,前綴 `%` 輸出。

解碼流程

  1. 掃描字符串,遇到 `%` 且後續兩字符為有效十六進制 → 合併為一字節存入字節數組。
  2. 遇到 `+` → 存入空格字節(`0x20`)。
  3. 其他 ASCII 字符直接存入字節數組。
  4. 最終字節數組按 UTF-8 解碼為字符串(自動處理多字節字符組合)。

生產環境建議

場景推薦方案理由
一般 Web 開發URLEncoder/URLDecoder 或 框架封裝(Spring `UriUtils`、Netty `QueryStringDecoder`)標準庫經過海量驗證、處理邊界情況完善
Android / 受限環境手寫版或 android.net.Uri.encode/decode標準庫行為一致、體積可控
高性能/高併發Netty `UrlEncoder/Decoder`、或預編碼常用字符串零拷貝、對象池、原生優化
教學/面試/無依賴本文手寫版理解 Percent-Encoding 本質、UTF-8 多字節組合邏輯

常見坑點與對策

  • 雙重編碼:前端 `encodeURIComponent` + 後端再次 `encode` → 解碼兩次。約定:單邊編碼,通常前端編碼、後端解碼。
  • 空格語義差異:`application/x-www-form-urlencoded` 空格→`+`;RFC 3986 路徑/查詢→`%20`。用 `encodeRfc3986()` 統一。
  • Emoji/補充平面字符:UTF-16 `char` 無法單獨表示,需按 `codePoint` 處理。手寫版中 `String.valueOf(c).getBytes("UTF-8")` 隱式處理了代理對,但逐 `char` 遍歷會拆分代理對導致錯誤。生產用標準庫或 `codePoint` 遍歷。
  • 編碼不一致:前端 UTF-8、後端 ISO-8859-1 解碼 → 亂碼。全鏈路強制 UTF-8(Tomcat `URIEncoding="UTF-8"`、Nginx `charset utf-8`、DB 連接 `characterEncoding=utf8`)。

理解 Percent-Encoding 本質,配合標準庫正確使用,是處理 URL 中文參數的基石。手寫實現適合學習原理,生產請信賴成熟庫。

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