编码和解码字符串
java.util.Base64 处理的是字节。转换字符串时要显式指定字符集,因为不带参数的 getBytes() 使用平台默认字符集,而在 Java 18 之前它并不总是 UTF-8。
import java.nio.charset.StandardCharsets;
import java.util.Base64;
String enc = Base64.getEncoder()
.encodeToString("héllo wörld".getBytes(StandardCharsets.UTF_8));
System.out.println(enc); // aMOpbGxvIHfDtnJsZA==
String dec = new String(Base64.getDecoder().decode(enc), StandardCharsets.UTF_8);
System.out.println(dec); // héllo wörld
Base64.Encoder 和 Base64.Decoder 实例是线程安全的,可以放在 static final 字段里复用。
带填充和不带填充的 URL 安全 Base64
getUrlEncoder() 使用 - 和 _。默认带填充;withoutPadding() 会返回一个省略填充的新编码器:
byte[] d = "<<???>>".getBytes(StandardCharsets.UTF_8);
Base64.getUrlEncoder().encodeToString(d); // PDw_Pz8-Pg==
Base64.getUrlEncoder().withoutPadding().encodeToString(d); // PDw_Pz8-Pg
new String(Base64.getUrlDecoder().decode("PDw_Pz8-Pg"), StandardCharsets.UTF_8);
// <<???>>
解码器带不带填充的输入都接受,所以 getDecoder().decode("aGk") 同样可以工作,返回 hi。JWT 的各段是不带填充的 Base64URL;什么时候该用哪种,见 Base64 与 Base64URL 的区别。
MIME 编码器和解码器
getMimeEncoder() 按 RFC 2045 的要求,每行写 76 个字符,行之间用 \r\n 分隔。最后一行之后不加换行符。
String mime = Base64.getMimeEncoder().encodeToString("x".repeat(60).getBytes());
// eHh4eHh4...eHh4 (76 chars) + "\r\n" + "eHh4"
Base64.getMimeEncoder(64, "\n".getBytes()); // PEM-style 64-char lines
行长度会向下取整到 4 的倍数。getMimeDecoder() 是宽松的那个:它会忽略行分隔符以及字母表之外的任何其他字符。
Base64.getMimeDecoder().decode("aGVs\r\nbG8K!"); // bytes of "hello\n"
MIME Base64 编码器 可以在浏览器中生成同样的格式。
编码和解码文件
小文件:
import java.nio.file.*;
byte[] file = Files.readAllBytes(Path.of("hello.txt"));
System.out.println(Base64.getEncoder().encodeToString(file)); // aGVsbG8K
Files.write(Path.of("copy.txt"), Base64.getDecoder().decode("aGVsbG8K"));
用 wrap() 流式处理大文件
Encoder.wrap(OutputStream) 和 Decoder.wrap(InputStream) 会边读边编码、边读边解码,所以无论文件多大,内存占用都保持平稳:
try (InputStream in = new FileInputStream("big.bin");
OutputStream out = Base64.getEncoder().wrap(new FileOutputStream("big.b64"))) {
in.transferTo(out);
}
try (InputStream in = Base64.getDecoder().wrap(new FileInputStream("big.b64"));
OutputStream out = new FileOutputStream("big.out")) {
in.transferTo(out);
}
关闭包装流很重要:编码器在 close() 时才会写出最后一组和填充。忘了关闭,最后一组字符(最多四个,包括填充)就会丢失。在一个 300 KB 的测试文件上,输出与 encodeToString 完全一致,往返结果逐字节相同。InputStream.transferTo 需要 Java 9;在 Java 8 上请用缓冲区循环复制。
校验与异常
基本解码器和 URL 解码器是严格的,会抛出 java.lang.IllegalArgumentException。错误信息会说明出了什么问题,字符以十六进制显示:
传给 getDecoder() 的输入 | 错误信息 |
|---|---|
PDw_Pz8-Pg== | Illegal base64 character 5f |
"aGVs\nbG8K" | Illegal base64 character a |
aGk=!! | Input byte array has incorrect ending byte at 4 |
aGk== | Input byte array has incorrect ending byte at 4 |
a | Input byte[] should at least have 2 bytes for base64 bytes |
所以校验只需要一个 try/catch:
static boolean isBase64(String s) {
try {
Base64.getDecoder().decode(s);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
有一个漏洞:解码器接受不为零的填充位,所以 aGl= 会被顺利解码为 hi。如果只接受规范的输入,就重新编码后再比较。Base64 校验工具 会指出是哪个字符有问题,Base64 常见错误 解释了每一类失败。
常见陷阱
- 平台字符集:
"text".getBytes()和new String(bytes)使用默认字符集。在 Java 17 及更早版本的 Windows 上,它通常是 Windows-1252,会破坏非 ASCII 文本(出现乱码)。Java 18 把默认字符集改成了 UTF-8(JEP 400),但显式传入StandardCharsets.UTF_8没有任何代价。 - 解码器与输入不匹配:把 URL 安全的输入交给基本解码器,会得到
Illegal base64 character 5f或2d。MIME 输入则会得到Illegal base64 character a或d。 encode()与encodeToString():encode()返回一个由 ASCII 字符组成的byte[]。直接打印这个数组,看到的是[B@1b6d3586之类的东西,而不是 Base64。- 没有关闭包装流:不调用
close(),编码器的输出就会被截断。 - 以为 Base64 能隐藏数据:它不能,见 编码与加密的区别。
版本说明
- Java 8:引入
java.util.Base64,包括基本、URL 和 MIME 三种变体,以及withoutPadding()和wrap()。 - Java 9:
InputStream.transferTo,上文用它来复制流。 - Java 11:随着 JAXB 从 JDK 中移除,
javax.xml.bind.DatatypeConverter.printBase64Binary也不复存在。 - Java 18:UTF-8 成为默认字符集。
Kotlin 可以使用上面的全部内容;它还有自己的跨平台 API,见 Kotlin 中的 Base64。
常见问题
Illegal base64 character 5f 是什么意思?
数字是出问题的字符的十六进制值。5f 是 _,2d 是 -,说明你在用 getDecoder() 解码 URL 安全的 Base64,应该改用 getUrlDecoder()。a 是换行符,20 是空格,这种情况请用 getMimeDecoder() 或先去掉空白。
还应该用 sun.misc.BASE64Encoder 或 javax.xml.bind.DatatypeConverter 吗?
不应该。sun.misc.BASE64Encoder 是内部 API,在现代 JDK 中已经没有了;javax.xml.bind 也在 Java 11 中从 JDK 移除。java.util.Base64 可以替代这两者。
Java 解码器能接受不带填充的 Base64 吗?
能。Base64.getDecoder().decode("aGk") 返回 hi 的字节。解码器接受缺失的填充,但拒绝错误的填充。
在 Android 上如何进行 Base64 编码?
API 级别 26 及以上可以使用 java.util.Base64。旧代码使用 android.util.Base64.encodeToString(bytes, Base64.NO_WRAP);不加 NO_WRAP 时它会插入换行符。