Java での Base64 エンコード:完全ガイド
状況はこうです:手元にはバイトがあります。ファイル、パスワード、証明書、13バイトの挨拶、200MB のアップロード。そしてそれらを、テキストしか理解しないもののなかに入れたい:JSON のフィールド、HTTP ヘッダー、データベースの列、URL、設定ファイル。それが Base64 の仕事そのもので、このガイドはそれを上手にするための Java ハンドブックです。方向感覚を少しだけ:ホームページでフォーマットを段階的に歩いているので、ここではざっと。Base64 はデータの3バイトを、64文字のアルファベットから選んだ4文字に書き換え、最後のチャンクが短いときは = を1つか2つつけます。移動の代償はサイズです:3バイトが4文字になるため、エンコードされた出力は入力の約33パーセント大きくなり、改行が絡めばさらに少し増します。
これがヘッドラインのニュースで、いい知らせです。2014年3月18日以降、すべての JDK が標準ライブラリに完全な Base64 ツールキットを同梱しています:java.util.Base64。ダウンロード不要、Maven コーディネート不要、ネイティブライブラリも不要。import は1つ、エンコーダーの気質が3つ、Java 8 から今日の Java 26 まで挙動は不変です。この記事のすべては、その1つのクラスの上に建てられています。しかも、データそのもので例外を投げることはありません:エンコーダーの仕事は、あらゆるありうるバイトがエンコード可能なので、無効な入力で失敗することができないからです。
始める前に、正直な境界線を1つだけ:これは物語のエンコーダー側です。実際に正しさを左右する文字列からバイトへの判断、パディングとラッピングのダイヤル、トークンのための base64url とそのパディングなしモード、そして Java 開発者がエンコード済み出力に最も頻繁に出会うユースケースを学びます。ほとんどの本物の痛みが住むデコードには独自のガイドがあり、この記事の末尾からリンクしています。
import 1つ、ダウンロード0
Java で Base64 をインストールする方法は、ホワイトボードで即答するあの1行です:「JDK に入っている」。クラス java.util.Base64 は 1.8 以来 java.base モジュールの一部であり、javadoc は12年経った今もなお Since: 1.8 と書かれています。インストールするのは JDK だけです:どのベンダー(Oracle、Eclipse Temurin、Amazon Corretto、Zulu)の Java 8 以降でも構いません。Debian 系のマシンでは1コマンドです:
sudo apt install openjdk-17-jdk-headless
この API はファクトリです:エンコーダーを自分で構築するのではなく、クラスに1つくださいと頼みます。エンコーダー側には4つの扉があり、すべて内部クラス Base64.Encoder のインスタンスを返します:
| ファクトリメソッド | アルファベット | 出力の形 |
|---|---|---|
getEncoder() |
A-Z a-z 0-9 + / |
パディングあり、改行なし |
getUrlEncoder() |
A-Z a-z 0-9 - _ |
パディングあり、改行なし |
getMimeEncoder() |
A-Z a-z 0-9 + / |
パディングあり、76文字の行、CRLF |
getMimeEncoder(int, byte[]) |
A-Z a-z 0-9 + / |
パディングあり、あなたの行の長さ、あなたのセパレータ |
先に知っておく価値のある性質が3つあります。インスタンスはスレッドセーフで、ファクトリは呼び出しのたびに同じ共有インスタンスを返すので、Base64.getEncoder() == Base64.getEncoder() は true です:static フィールドに1つ作って、どこでも共有しましょう。エンコーダーはデータで例外を投げません:すべてのバイト値にエンコーディングがあるので、対処する「無効な入力」の状態は存在せず、出会う例外は誤設定(不適切な行セパレータ)や小さすぎる宛先配列についてだけです。そしてこのリストのすべてのエンコーダーはデフォルトでパディングを追加します。それをオフにするダイヤル withoutPadding() は base64url セクションに登場します:それがちょうど必要になる場所だからです。
コードベースでは古いライブラリにもまだ出会うので、簡単な地図を。Apache Commons Codec(現在の 1.22.1)は 1.0 以来、独自の実装 org.apache.commons.codec.binary.Base64 を出荷しています。Builder API が、厳格か寛容かのポリシー、行の長さ、セパレータをダイヤルとして公開します。ただし、Java 8 以前の JVM をサポートしなければならないときだけ、それが正しいツールです。Guava は同じほど実力のある大ベテラン com.google.common.io.BaseEncoding を同梱しています。ビッグデータ基盤では今なおよく見かけます。モダンな JVM 上のものなら、java.util.Base64 がデフォルトです:依存関係ゼロで、コミュニティのベンチマークは毎回、これが中身で最速だと結論づけています(それはセキュリティと速度のセクションで詳しく)。
最初のエンコード
エンコードの実生活の9割は、3行に収まります。Wikipedia の Base64 記事がアルファベットを説明するのに使っている最小の例を使って、儀式の全体を示しましょう:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class FirstEncode {
public static void main(String[] args) {
byte[] text = "Man".getBytes(StandardCharsets.UTF_8);
String packed = Base64.getEncoder().encodeToString(text);
System.out.println(packed); // TWFu
}
}
文字列 TWFu は、Wikipedia の Base64 記事がアルファベットを説明するのに使っている例です。あなたのエンコーダーが「Man」をそれに変えるなら、機械は正直です。でも、その例の最初の行を見てください。Java では実際にエンコードが起きるのが、この行だからです。encodeToString(String) メソッドは、意図的に存在しません。Java の String はバイトではなく UTF-16 コードユニットの列であり、Base64 はバイト形式なので、API はバイトの問題を自分で決断させるのです:"Man".getBytes(StandardCharsets.UTF_8)。この明示的な文字コード付きの1つの呼び出しこそが、「café」を今後100年正しく保つ場所であり、この記事全体でいちばん重要な習慣です。次のセクションはそれに捧げられています。なぜなら、その代わりの道は古典的な文字化けバグだからです。
2行目へのメモが2つ。encodeToString() はエンコードされたバイトから作られた String を返します。javadoc には、結果を ISO-8859-1 文字セットで構築すると説明されていますが、実際には問題になりません。Base64 の出力文字はすべて素の ASCII で、Latin-1、UTF-8、文字セットの獣舎の残りの大半では見かけが同じだからです。そして出力バッファを自分で所有したいなら、encode(byte[]) は新しい byte[] を返し、encode(byte[] src, byte[] dst) はあなたが提供する宛先へ書き、個数を返します(宛先が足りないときは、1バイトも書かずに IllegalArgumentException: Output byte array is too small for encoding all input bytes を投げます)。
文字コードの決断
文字列からバイトへのステップを、定番のケースで具体的にしましょう。「café」という単語は1つの単語ですが、バイトとしては、あなたが選んだ文字コードに完全次第です:
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class CharsetEncode {
public static void main(String[] args) {
byte[] utf8 = "café".getBytes(StandardCharsets.UTF_8);
byte[] latin1 = "café".getBytes(Charset.forName("ISO-8859-1"));
System.out.println(utf8.length + " vs " + latin1.length);
// 5 vs 4:アクセントは UTF-8 では2バイト、Latin-1 では1バイト
System.out.println(Base64.getEncoder().encodeToString(utf8));
// Y2Fmw6k=
System.out.println(Base64.getEncoder().encodeToString(latin1));
// Y2Fm6Q==
}
}
1つの単語に2つの異なる Base64 文字列。読者にどの文字コードを使うかを伝えれば、どちらも「正しい」です。教訓のすべては1行に収まります:エンコーダーはあなたが渡したバイトに忠実であり、バイトの責任を負うのはあなたです。実務では、それはこういうことを意味します:相手と UTF-8 で合意し、StandardCharsets.UTF_8 を明示的に渡し、文字コードを仕様やスキーマ、コミットメッセージに書き留めておくことです。受ける側は、Base64 だけではそれを推測できないからです。このバグのデコーダー側の双子が、姉妹ガイドの主題です。
バージョンのメモを1つ。面倒なコードの失敗モードを変えるからです。引数なしの new String(bytes) と、文字コード指定なしの String.getBytes() はプラットフォーム既定の文字コードを使います。歴史的には、Windows では Cp1252、Linux ではロケール次第の何かでした。JDK 18(JEP 400、「UTF-8 by Default」)以降、すべてのプラットフォームで既定は UTF-8 なので、モダンな JVM では面倒な形がたまたま正しいです。それで安全になるわけではありません:あなたのコードは、書かれた JDK より長く生き残ります。そしてそれを引き継ぐ人は、既定値が何かを知る必要がないはずです。文字コードを書きましょう。
関連する設計の細部:API のどこにも encode(String) オーバーロードがなく、それは意図的なものです。パイプラインの他のすべてのステップ(配列、バッファ、ストリーム)はバイトを受け取り、String を受け取るメソッドはあなたのために文字コードを選ばなければなりません。まさにそれは、JDK が断固としてしない決断です。存在する唯一の String 型メソッド encodeToString は、文字コードの問題が存在しない出力側にあります:Base64 の出力は純粋な ASCII なので。この API の全体の形は、「バイトを意図的に決めろ」という小さな議論です。
パディング、ラッピング、そして MIME のダイヤル
Java のエンコーダーは、デフォルトで2つのフォーマット判断をあなたの代わりにします。どちらも回せるダイヤルなので、理解する価値があります。第一がパディング:すべてのエンコーダーは、出力を4の倍数にする = 文字を追加します。RFC 4648 が求める形で:参照元の仕様書が別段を定める場合を除き、実装はエンコードされたデータの末尾に適切なパディング文字を含むものとする(MUST)。第二が行折り返し:ラップするのは MIME エンコーダーだけで、76文字でキャリッジリターンと改行を使用します。しかも最後の不完全行の後に、行区切り記号を追加しません。javadoc が明示的に指摘し、他のツールが間違える細部です:
| エンコーダー | 出力をパディングする | 行をラップする | 行セパレータ |
|---|---|---|---|
getEncoder() |
はい | いいえ | 該当なし |
getUrlEncoder() |
はい | いいえ | 該当なし |
getMimeEncoder() |
はい | はい、76文字 | CRLF |
getMimeEncoder(64, "\n") |
はい | はい、64文字 | LF |
MIME のダイヤルは、他人のフォーマットを継承する人にとって、API で最も便利な部分です。標準のコンストラクタは getMimeEncoder()(76、CRLF、RFC 2045 そのまま)で、2引数版 getMimeEncoder(int lineLength, byte[] lineSeparator) が、他の慣習を再現できます。知っておくべき2つのクイーク:行の長さは「4の倍数に切り下げられる」ので、77 を求めると黙って76が返り、切り下げた値が正でなければラップはまったく行われません。また、セパレータは Base64 アルファベットの文字を一切含めてはならず、含めるとコンストラクタはその場で IllegalArgumentException を投げます。データと混同されうるセパレータは、起こるのを待っているバグだからです。ダイヤルの実動です。MIME 標準と、PEM 風味の2つ:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class WrapDials {
public static void main(String[] args) {
byte[] data = "Hello, wrapped world! This line keeps going and going and going until it finally has to wrap.".getBytes(StandardCharsets.UTF_8);
Base64.Encoder mime = Base64.getMimeEncoder();
Base64.Encoder pem = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.ISO_8859_1));
System.out.println(mime.encodeToString(data));
// 76文字の行、行間は CRLF
System.out.println(pem.encodeToString(data));
// 64文字の行、行間は素の LF
}
}
実用的なメモが2つ。消費側がラップされた文字列は改行で終わることを期待するなら(一部のメールツールはそうします)、エンコード後に自分で追加してください:JDK は意図的に最後の不完全行で止まります。そして URL やトークンのなかに住むデータを生成するなら、ラッピングはまったく違うダイヤルです。その消費側は長い行1本を望み、たいていパディングは不要です。それが次のセクションです。
base64url とパディングなしダイヤル
標準 Base64 のアルファベットは + と / で終わります。URL で正しく振る舞わないのは、まさにこの2文字です:クエリ文字列の中の + は、サーバーがそれを解析するまえにすでにスペースになっており、/ はパスの区切り文字で、ぶら下った = は3文字の怪物になるパーセントエンコーディングを求めます。RFC 4648 セクション5が修正案を描きます:URL 安全かつファイル名安全なアルファベットで、+ が - になり、/ が _ になり、長さが暗黙のうちにわかっているときは、末尾の = パディングは通常落とされます。RFC は名前について断言します:このエンコーディングは「base64 エンコーディングと同じとは見なすべきではない」、そしてあなたが聞く名前は base64url です。JSON Web Token、OAuth の state パラメータ、API のセッション ID、11文字の動画 ID は、すべてこの方言の中に住んでいます。
Java は getUrlEncoder() でそのアルファベットをくれますが、ここが人をひっかけるダイヤルです:URL 安全のエンコーダーはデフォルトでまだパディングし、トークンの標準はパディングを望みません。RFC 7515 は明確に、JWS の各部分は base64url を「末尾の「=」文字をすべて省略し……改行、空白、その他の追加文字を一切含めずに」使うと書いています。したがって、正規の Java JWT レシピは2メソッドのチェーンです:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class TokenParts {
public static void main(String[] args) {
Base64.Encoder url = Base64.getUrlEncoder().withoutPadding();
byte[] header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}".getBytes(StandardCharsets.UTF_8);
byte[] payload = "{\"sub\":\"1234567890\",\"name\":\"John Doe\"}".getBytes(StandardCharsets.UTF_8);
System.out.println(url.encodeToString(header));
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
System.out.println(url.encodeToString(payload));
// eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0
}
}
withoutPadding() の呼び出しは、末尾のパディングを省略する点を除き挙動が同一の新しいエンコーダーインスタンスを返します。元のインスタンスは無傷で、javadoc はそれを正確に言っています。デコーダー側はパディング有りとなしの両方の入力を受け入れるので、あなたがパディングなしで生成した値も厳格なデコーダーが読めます。だから API 境界を越えるものには、パディングなしが安全な選択です。さて、大きな免責事項を1つ:上の2つの部分は、JWT の未署名な半分です。本当のトークンには「header.payload」の上に計算された署名が必要で、それは暗号学であり、エンコーディングではありません。本番では、JOSE ライブラリでトークンを発行し検証してください:JJWT(0.13.0)か nimbus-jose-jwt(10.9.1)。例えば JJWT の API アーティファクトは、1つの座標先にあります:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<!-- ランタイムで jjwt-impl と jjwt-jackson を追加、プロジェクトのドキュメントに従って -->
YouTube の ID がこのダイヤルの別の顔です:パディングなし base64url の11文字で、URL が許されるどこにでも貼り付けられて生き残らなければならない識別子です。あなたのシステムが URL のなかを旅する識別子を生成するなら、上の withoutPadding() チェーンが模すべき形です。
ファイルのエンコード
毎日のファイル作業は、デコーダーお気に入りの鏡像です:ファイルを読み、エンコードし、テキストを書き出す。java.nio.file で4行:
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class EncodeFile {
public static void main(String[] args) throws Exception {
byte[] raw = Files.readAllBytes(Paths.get("report.pdf"));
String packed = Base64.getEncoder().encodeToString(raw);
Files.write(Paths.get("report.pdf.b64"), packed.getBytes(StandardCharsets.ISO_8859_1));
System.out.println(raw.length + " -> " + packed.length());
}
}
その最後の print が、可視化された33パーセントの請求書です。1 MB のファイルは約 1.33 MB のテキストになります(元の4/3に、最大2つのパディング文字を加えたもの)。MIME スタイルでラップすると、改行がさらに数パーセント増やします:まだ正しい、古いメール時代の計算は 4/3 倍 78/76、つまりラップされた MIME ペイロードは元の約1.37倍です。帰結が2つあります。第一に、どのストレージやメッセージフィールドも、生の長さではなくエンコード後の長さからサイズを決めること:192バイトの生値を快く収めてくれる VARCHAR(255) の列は、その256文字のエンコードを拒否します。第二に、メモリを悪くするのはエンコード方向なので、大きなファイルには配列版が間違ったツールで、ストリーミングのセクションが正しいツールです。ファイル界への小さな楽しみ:出力の最初の文字は最初の入力バイトの純粋な関数なので、すべての Base64 エンコード済みの PNG は iVBORw0K で始まり、すべてのエンコード済みの GIF は R0lGOD で始まります。1バイトたりともデコードするまえに、ファイルタイプを見分けることができるのです。
JSON、API、data URI
エンコード済み出力がワイヤー上に住む、もっとも一般的な場所の2つです。
1つ目:JSON の中のバイナリ。 ファイルアップロードのエンドポイント、コンテンツ API、シークレットストア、Webhook は、生のバイトが JSON 文字列のエスケープを壊すため、JSON の中に Base64 テキストとしてバイナリを埋め込みます。エンコーダー側は境界での1行で、唯一の判断は、仕様がどの方言を求めるかです:
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class JsonField {
public static void main(String[] args) throws Exception {
byte[] image = Files.readAllBytes(Paths.get("logo.png"));
// 仕様が base64url、パディングなしを指定:
String field = Base64.getUrlEncoder().withoutPadding().encodeToString(image);
//「field」を素の文字列値として JSON ライブラリに渡します。
System.out.println(field.length());
}
}
落とし穴はエンコードではなく、仕様を読むことです。標準 Base64 かつパディング付きを求める API もあれば、パディングなしの base64url を求める API もあり、両方に寛容な少数派もいます。仕様が沈黙しているときは、最安の修正は相手側の例値を眺めることです:どこかに - や _ があればアルファベットは決まり、末尾の = があればパディングは決まります。方言を間違っても、たいてい相手側はクラッシュしません:たいていファイルが破損します。そしてそれは、見つけるのがもっとも遅いタイプのバグです。
2つ目:data URI。 画像を HTML や CSS にインライン化する data:image/png;base64,... という文字列が、RFC 2397 の data URI です:data:、任意の media type、任意の ;base64 フラグ、カンマ、そしてデータ。1つを作るのは文字列結合で、唯一の判断は、フラグがあるかどうかです(フラグがないとペイロードはパーセントエンコードされたテキストになり、バイナリでは誰もそれを望まない):
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class DataUriBuild {
public static void main(String[] args) throws Exception {
byte[] icon = Files.readAllBytes(Paths.get("icon.png"));
String b64 = Base64.getEncoder().encodeToString(icon);
String uri = "data:image/png;base64," + b64;
System.out.println(uri.substring(0, Math.min(40, uri.length())) + "...");
// data:image/png;base64,iVBORw0KGgo...
}
}
RFC 自身の助言が、興味深く当てはまります:data URI は短い値のためにあります。50 KB のアイコンをインラインにするのは普通の取引です(リクエストが1つ減る)。5 MB の写真をインラインにするのは、便利さの衣装をまとったパフォーマンスバグです。フラグを保ち、media type は正直に保ち、バイトは小さく保つこと。
Basic 認証ヘッダーを作る
ウェブで最も古い認証ヘッダーは、まだ Java では最も簡単な Base64 のユースケースです:ちょうど1回のエンコード呼び出しだからです。RFC 7617 に従い、Basic リクエストは Authorization: Basic の後に username:password の Base64 エンコードを送り、RFC 自身の例 QWxhZGRpbjpvcGVuIHNlc2FtZQ== は変装した「Aladdin:open sesame」です。クライアント側でヘッダーを作るのは、Base64 の2行とモダンな HTTP 呼び出しです:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class BasicAuthClient {
public static void main(String[] args) throws Exception {
byte[] credentials = ("alice:secret123").getBytes(StandardCharsets.UTF_8);
String header = "Basic " + Base64.getEncoder().encodeToString(credentials);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/api/status"))
.header("Authorization", header)
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
}
}
このヘッダーには3つの注意がついて回ります。第一に、RFC は Basic は保護ではなくエンコーディングだと明確にしています:認証情報はパケットを見られる誰にでも読めるので、このヘッダーは、その下にある HTTPS の強さと同じ強さしかなく、TLS 以外では悪い考えです。第二に、文字コード:RFC は US-ASCII の認証情報を想定しています(それ以外は UTF-8、charset 認証パラメータは助言にとどまる)。だから StandardCharsets.UTF_8 を選び、両側で一貫性を保つこと。第三に、バージョンの注意:java.net.http クライアントは Java 11 からです。古い JVM では同じヘッダーが HttpURLConnection の1つの setRequestProperty 呼び出しで乗り、Base64 の行はどちらでも同じです。同じヘッダーのサーバー側では、解析とデコードは姉妹ガイドの例で、最初のコロンでの分割と定時間比較があります。2つの側は、同じ API の2つの呼び出しです。それがこの1つの静かな優雅さです。
設定、環境変数、列の中の値
Base64 はテキストの容器です。それが、予期しない場所に現れる理由です:環境変数ファイルの中のセミコロン付きのデータベース DSN、properties ファイルの中の引用符付きのパスワード、config map の中の複数行の証明書、TEXT 列の中のバイナリのブロブ(スキーマが誰しも BLOB を考えるまえに設計されたので)。エンコード側は1回の呼び出しで、正直な枠組みは、それが何であるかということです:秘密性のトリックではなく、フォーマットの安全のためのトリック:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class ConfigEncode {
public static void main(String[] args) {
String dsn = "pg:host=db;password=qu\"ote";
byte[] raw = dsn.getBytes(StandardCharsets.UTF_8);
String packed = Base64.getEncoder().encodeToString(raw);
System.out.println(packed);
// cGc6aG9zdD1kYjtwYXNzd29yZD1xdSJvdGU=
System.out.println("DB_DSN_B64=" + packed);
}
}
これを誠実に保つ2つのルールがあります。第一に、秘密を Base64 として保存して、それを暗号化済みと称してはいけない:Base64 はエントロピーを一切加えず、情報を一切取り除きません。開発者がファイルを読んだ瞬間に、1回の呼び出しで値をデコードできます。RFC のセキュリティのセクションは、まさにこの失敗を指しています:「エンコードした」プロトコルのやり取りを貼り付けて認証情報を明かしてしまう人々。値が秘密なら、まず暗号化し、そのうえでチャネルがテキストを要求するなら、はじめて暗号文を Base64 にパックしてください。第二に、サイズに予算を計上すること:保存される値は元より約3分の1大きく、生の値が入った列やフィールドには、エンコード後の値が入りません。そして値が戻ってきたら、境界でデコードし、バイナリならバイトとして、テキストなら明示的な文字コードの文字列として保持してください。その方向は姉妹ガイドの管轄です。
大きなデータのためのストリーミング
エンコードはメモリを悪くする方向なので、ここでの大きなファイルの物語は、ワーキングセットを小さく保つことです。ファイルのセクションの例の配列版は、ファイルがメモリに快適に収まらなくなるまでは大丈夫ですが、それを超えると、ストリームアダプターが一手です。wrap(OutputStream) は、書き込むにつれてエンコードする出力ストリームを返します。そのため、複数 GB のファイルは、1つのバイト配列として保持されることがありません:
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class StreamEncode {
public static void main(String[] args) throws Exception {
OutputStream packed = Base64.getEncoder().wrap(Files.newOutputStream(Paths.get("bigfile.b64")));
InputStream raw = Files.newInputStream(Paths.get("bigfile.bin"));
byte[] buf = new byte[8192];
int n;
while ((n = raw.read(buf)) != -1) {
packed.write(buf, 0, n);
}
packed.close();
raw.close();
}
}
このストリームには、ハイライトに値する挙動が1つあります:javadoc 自体がそれを指しているからです:ラップされたストリームは内部に数バイトの残りを持つことがあり、推奨される実践は「使用後に返された出力ストリームをただちにクローズすること。クローズの際に、残っているあらゆるバイトが元の出力ストリームへフラッシュされる」ことです。書き込みを止めて、クローズするまえに出力ファイルを読んだら、データの末尾はまだエンコーダーの中にいて、ファイルは途中で切れているように見えます。だから例では、他の何かがファイルに触れる前に packed をクローズしており、本番では両方のストリームを try-with-resources ブロックに入れます。習慣にしてください:エンコードストリームでは、クローズはエンコードの一部です。
古参たちとの遭遇
引き継いだコードベースには、java.util.Base64 より前の Base64 API が満ちています。それらを認識すれば、「なぜこれは私の出力をラップするのか」という謎から救われます。実際に会うのは4つ:
| API | どこで出会うか | どうするか |
|---|---|---|
sun.misc.BASE64Encoder / BASE64Decoder |
Java 8 以前のコード | java.util.Base64 に移行;Java 9 で削除 |
javax.xml.bind.DatatypeConverter |
XML 時代のコード、古い Web サービス | Java 11(JEP 320)で削除;移行すること |
org.apache.commons.codec.binary.Base64 |
8 以前の JVM で動かさなければならないコード | 8 以前をサポートするなら残す;それ以外は JDK のクラスがデフォルト |
com.google.common.io.BaseEncoding |
Guava 多用の基盤、ビッグデータ基盤 | 問題なく動く;JDK のクラスは依存関係ゼロ |
sun.misc のペアが、ドラマを持つ1つです。それは内部的でサポートされない API で(当時の JDK では普通にコンパイルできて、非推奨の警告もなく消えるタイプ)、その出力は独自の習慣を持っていました:エンコードされたテキストに行折り返しを施すことです。「私の Base64 に改行が入っている」というバグの、驚くべき数の出自がここにあります。2017年9月に Java 9 が出ると、モジュールシステムのクリーンアップがそれを削除しました。公式の移行ガイドは言葉を濁しません:「特筆すべきは、sun.misc.BASE64Encoder と sun.misc.BASE64Decoder が削除されたことです。代わりに、JDK 8 で追加されたサポートされた java.util.Base64 クラスを使用してください」。古いクラスをまだ参照しているコードに jdeps を実行すると、ツールはその依存を「JDK removed internal API」とフラグ立てます。これは、JDK が交通コーンに最も近い表現です。JAXB の DatatypeConverter はもっと長く、しかし似たような人生を送りました:Java 9 の時代に Java EE モジュールとともに非推奨となり、Java 11 で JEP 320 「Java EE と CORBA モジュールの削除」により完全に削除されました。両方の移行は機械的です:古い printBase64Binary と BASE64Encoder().encode の呼び出しは、ラッピングの違いを無視すれば getEncoder().encodeToString に1対1で対応し、コードが java.util.Base64 の上にあれば、8 から 26 のすべての JDK で、それ以上の考えなしに動きます。
セキュリティと速度
セキュリティのセクションは短いです:エンコーダーの仕事はデータで失敗できないからです。でも空ではありません。Base64 は暗号化ではなく、標準はそれをその言葉で言います:Base エンコーディングは「パスワードのように、さもないと簡単に見分けられる情報を視覚的に隠す」が、「計算による秘匿性は何も提供しない」、同じセクションは、これが「セキュリティインシデントを引き起こしたことがある」とも指摘しています。エンコーダー側の実用的な帰結:安全にするために秘密をエンコードしないこと(より多くのチャネルに乗れるようになったので、今や安全性が低い)。値が秘密なら、まず暗号化し、暗号文をエンコードすること。そして可変性の双子を心に留めること:受信者は、デコードされたデータを変えることなく、1つの有効な表記をもう1つの表記(パディングの違い、余分なビットのゴミ)にすり替えることができる。決定的なエンコーダーはここで助けになります:java.util.Base64 は1つの入力に対してちょうど1つの出力を生成するので、自分のシステムが値を書きも読みもするなら、表記は安定しており、正規形のチェックを必要とするのは、信頼境界にある外部の値です。
速度について、エンコーダー側はデコーダー側と同じ物語です:モダンな JVM では、組み込みの実装は Base64 がボトルネックになることはほとんどないほど速く、ベンチマークの基準点です。姉妹ガイドで紹介したのと同じ 2025 年の gRPC-java ベンチマーク(issue 11857、JDK 17 と 21 での JMH)は、JDK エンコーダーのスループットを Guava の約2.5倍から3.8倍と測定し、x86 で差が最大でした。実用的なメモが2つ:ホットパスでは、エンコーダーインスタンスを1つ共有すること(ファクトリはすでに同じ共有インスタンスを返します)、アロケーションをスキップするために、あらかじめサイズを決めた配列への encode(byte[], byte[]) を優先すること。巨大なデータでは、ストリーミングのセクションがメモリの物語で、ラッピングのコストはディスクの横では騒音にすぎません。Base64 で唯一の実在するパフォーマンスの税金は、サイズそのものです。この実装も含め、どの実装もそれを値引く交渉はできません。
罠チェックリスト
すべての罠を1つの場所に集めました。すべて Java 固有です:
- 文字コードの欠落。 明示的な文字コードなしの
text.getBytes()はプラットフォーム既定の文字コードを使います:JDK 18 以降は偶然正しく、それより古いものでは間違い、原理的にはどこでも間違いです。StandardCharsets.UTF_8を渡し、仕様書に文字コードを書きましょう。 - パディング付きの JWT。
getUrlEncoder()はデフォルトでパディングし、トークンはパディングなしを望みます。withoutPadding()の呼び出しは、レシピの一部であり、オプションの付け足しではありません。末尾に=のついたトークンは、一部のバリデータが拒否し、一部のバリデータが壊してしまうトークンです。 - ラップされた出力。 MIME エンコーダーは CRLF で76文字でラップし、末尾の改行は追加しません。消費側が末尾の改行を期待するなら、追加してください。消費側が改行なしを期待するなら、MIME エンコーダーを使わないでください。
- 二重エンコード。 すでに Base64 の値をエンコードすると、完全に有効で、完全に無用の文字列が生まれます。定番の原因:フィールドが API から事前にエンコードされて届き、あなたのコードが「親切にも」もう1回エンコードする。エンコードするまえに確認すること。
- URL 中のプラス記号。 標準 Base64 の出力には
+が含まれ、それはサーバーがそれをみるまえに、クエリ文字列ではすでにスペースです。標準アルファベットの値が URL のなかを旅しなければならないなら、パーセントエンコードするか、最初から URL 安全アルファベットで生成してください。 - 33パーセントの請求書。 生の列に収まる値は、エンコード後の列には収まりません。ストレージ、メッセージフィールド、ヘッダーは
4 * ceil(n / 3)からサイズを決め、ラップされた MIME の出力はその上に数パーセント乗ることを思い出してください。 - 閉じられていないストリーム。 ラップされた出力ストリームは、クローズするまで残ったバイトを保持します。クローズするまえにファイルを読めば、途中で切れたエンコードが得られます。毎回、try-with-resources。
- 丸見えの秘密。 Base64 は梱包テープであり、鍵ではありません。設定ファイル、ログ、環境変数の中のエンコード済みの認証情報は、読める認証情報です。まず暗号化するか、秘密扱いをやめるか。
- Android の壁。 Android では、
java.util.Base64は API レベル 26 からしか存在しません。それ以下では、フレームワークのクラスはandroid.util.Base64で、独自のフラグ定数(NO_PADDING、URL_SAFE、NO_WRAP)を持ちます。チェックなしで片方をハードコードすると、あなたが決してテストしなかったデバイスで、まさにそこで壊れます。 - 行の長さのクイーク。
getMimeEncoder(77, ...)は黙って76でラップします:長さは4の倍数に切り下げられるからです。3以下を求めると、ラッピングは完全に無効になります。あなたのフォーマットが奇数の行長を求めるなら、MIME のダイヤルは適切なツールではありません。
sun.misc から標準ライブラリへ
Java の物語は、明確な前後がある短いものです。2014年より前に、JDK の中で Base64 が必要なら、内部ペア sun.misc.BASE64Encoder と sun.misc.BASE64Decoder を手に入れました:初日からサポートされず、独自の76文字ラッピングの習慣を持ちます。あるいは XML のコードでは javax.xml.bind.DatatypeConverter に手を伸ばし、あるいは Apache Commons Codec か Guava をビルドに追加しました。それで多くのエンタープライズコードベースが、Base64 実装を3つ持ち、どれがどれかさっぱりわからない状態になったのです。2014年3月18日、Java 8 が java.util.Base64 を出荷しました:クラス1つ、アルファベット3つ、RFC 4648 と RFC 2045 のルールが正しく実装され、ファクトリパターン、パディングとラッピングのダイヤル、両方向のストリームアダプター。それは、この言語が最初から持つべきだった Base64 で、javadoc はそれ以来ずっと Since: 1.8 と言い続けています。
クリーンアップは2つの波で来ました。Java 9(2017年9月21日)がモジュールシステムのクリーンアップの一環として sun.misc ペアを削除し、移行ガイドはすべての開発者を JDK 8 のクラスへ向かわせ、Java 11 が JAXB モジュールとその DatatypeConverter も一緒に削除しました(JEP 320)。Java 18(2022年3月22日)が JEP 400 「UTF-8 by Default」を持ち込みました:Base64 には一切触れませんが、それを供給する面倒な getBytes() 呼び出しの失敗モードを変えました:プラットフォーム既定の文字コードがすべての OS で UTF-8 になったので、古い文字化けのパターンは新しい JVM では単に再現しなくなったのです。1.8 以来、公開 API は1つのメソッドたりとも変わっていません。動いたのはその下にいるエンジンです:バグ修正とパフォーマンス作業。だからこそコミュニティのベンチマークは、標準ライブラリ版が、それが置き換えたレガシーライブラリを上回り続けることを見つけています。今日、8 から 26 のどの JDK でも、「Java でこれを Base64 にするには」という問いへの答えは、import 1つとファクトリ呼び出しで、それは10年以上そうだったままです。
オタクのためのいくつかの歓び
ハンドブックは笑顔で終わるべきなので、単に楽しい Java 固有の事実をいくつか:
- javadoc は
Since: 1.8と言い、12年間それが真でした。メソッドの追加は1つもなければ、削除も1つもなければ、挙動の変更も1つもありません:この言語で最も長く凍結された API 表面の1つで、あなたは考えもせずに使っています。 - javadoc によれば、
encodeToStringは結果の String を ISO-8859-1 文字セットで構築します。実際にはまったく不要な細部です:Base64 の出力は純粋な ASCII で、Latin-1、UTF-8、文字セットの獣舎の残りの大半では同じに見えるからです。でも javadoc はそれでも伝えます。それが JDK が JDK たる所以です。 - MIME エンコーダーは、最後の不完全行の後に、行区切り記号を追加しません。他のツール、非常に有名なメールライブラリのいくつかを含むが、ラップされた出力を末尾の CRLF で終わらせます。参照実装との diff がちょうど末尾2文字なら、このクイークにたどり着いたということです。
getMimeEncoderに77文字の行を求めると、76を返します:行の長さは、静かに、4の倍数に切り下げられるのです。なぜなら、4文字のグループを分断するラップはゴミを生成するからです。API は、あなたの許可を求めるのではなく、壊れた行を作ることを拒否します。Base64.getEncoder() == Base64.getEncoder()は true です。ファクトリメソッドは呼び出しのたびに同じ共有インスタンスを返すので、「新しいのをください」API はシングルトンの衣装で、スレッドセーフの約束は、JVM がすでにやっていることの説明にすぎません。- Android では、双子の API
android.util.Base64が、同じ判断をフラグとして公開します:NO_PADDING、URL_SAFE、NO_WRAP。API は2つ、判断表は1つ。これが、Base64 の設計が今やどれだけ定着しているかについての静かな証言です。 - RFC 4648 セクション5が、「base64url」という名前の出生地です:仕様は、URL 安全エンコーディングは「base64url と呼べるかもしれない」と言い、警告として「base64 エンコーディングと同じとは見なすべきではない」と書いています。その出自は、2001年の P2P-hackers メーリングリストへの投稿に脚注されています。だからあなたが貼り付けるすべての URL にあるこの名前は、メーリングリストの出所を持つのです。
- 単語
base64をエンコードすると、YmFzZTY0になります:パディングなし。6は3の倍数なので。自らを記述するフォーマットは、モールス信号で話す鏡の技術的な同物で、これは鏡自身の姿映しです。 - Java 8 以前のコードに
jdeps -jdkinternalsを実行して、sun.misc.BASE64Encoderが「JDK removed internal API」とフラグ立てられるのを眺めてください。公式の移行ガイドの、このツールの例が Base64 クラスというのが、JDK があなたの import を指差して「これは話したよね」と言っているようなものです。 - 1.37 倍の係数。すべてのラップされた MIME ペイロードは、元のサイズの約1.37倍のコストがかかります(アルファベットで4/3、CRLF のリズムで 78/76)。古いメールの計算が今なお引用するほど安定した分数です:1990年代のメール基盤がすべての添付ファイルに課した通行料が、ちょうど今日の
getMimeEncoder()が請求する請求書なのです。
向かうのは別の道
これが物語のエンコーダー側です。2つのうち穏やかな方です:仕事はデータで失敗することがなく、罠は他人の驚きではなく、あなたの判断(文字コード、パディング、ラッピング、方言)についてで、API 全体は import 1つに収まります。別の方向は、Base64 が便利さを失い、敵対的になる場所です:デコードとは、他人のパディングの選択、改行、文字コード、アーマーに出会う場所で、あなたと真実のあいだに IllegalArgumentException が立っています。このページからリンクされる「Java での Base64 デコード」は、同じ深さでデコーダーを扱います:3つのデコーダーの気質、正確なエラーメッセージ、パディングのルール、base64url と JWT、MIME と PEM、そして1つの場所に集めた Java 固有の落とし穴。2つを1組として読めば、この主題全体があなたのものです。
最終更新: 2026-10-09