上传文件
  1. base64hub
  2. 在代码中使用 Base64
  3. Java
代码

Java 中的 Base64

从 Java 8 开始,java.util.Base64 就能处理标准、URL 安全和 MIME 三种 Base64,还支持流式处理。下面的所有输出均来自 Java 21。

编码和解码字符串

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
aInput 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 时它会插入换行符。

继续阅读