Encodage Base64 en Java : un guide complet
Voici la situation : vous avez des octets. Un fichier, un mot de passe, un certificat, une salutation de 13 octets, un upload de 200 mégaoctets. Et il vous faut les mettre dans quelque chose qui ne comprend que du texte : un champ JSON, un en-tête HTTP, une colonne de base de données, une URL, un fichier de configuration. C'est tout le travail de Base64, et ce guide est le manuel Java pour le bien faire. Repères rapides, car la page d'accueil détaille le format pas à pas : Base64 réécrit chaque trio d'octets de données en quatre caractères tirés d'un alphabet de 64 lettres, avec un ou deux = accrochés quand le dernier bloc est court. Le prix du voyage, c'est la taille : chaque trio d'octets devient quatre caractères, donc la sortie encodée arrive environ 33 pour cent plus grosse que l'entrée, plus un peu si des retours à la ligne sont impliqués.
L'info en une, et elle est bonne. Depuis le 18 mars 2014, chaque JDK embarque une boîte à outils Base64 complète dans la bibliothèque standard : java.util.Base64. Pas de téléchargement, pas de coordonnée Maven, pas de bibliothèque native. Un import, trois personnalités d'encodeur, et le même comportement de Java 8 jusqu'au Java 26 d'aujourd'hui. Tout cet article est construit sur cette unique classe, et elle ne lève jamais sur les données elles-mêmes : le travail de l'encodeur ne peut pas échouer sur une entrée invalide, parce que chaque octet possible est encodable.
Une limite honnête avant de commencer : c'est ici le côté encodeur de l'histoire. Vous allez apprendre la décision chaîne-vers-octets qui détermine vraiment la justesse, les molettes de padding et d'enveloppement, le base64url et son mode sans padding pour les jetons, et les cas d'usage où les développeurs Java croisent le plus souvent la sortie encodée. Le décodage, là où vit la plupart de la vraie douleur, a son propre guide et est lié à la fin de celui-ci.
Un import, zéro téléchargement
Installer Base64 en Java, c'est la réponse en une ligne que vous donnez au tableau blanc : « C'est dans le JDK. » La classe java.util.Base64 fait partie du module java.base depuis la 1.8, et sa javadoc dit toujours Since: 1.8 douze ans plus tard. La seule chose que vous installez est un JDK : n'importe quel Java 8 ou plus récent de n'importe quel éditeur (Oracle, Eclipse Temurin, Amazon Corretto, Zulu) fonctionne, et sur une machine basée sur Debian c'est une seule commande :
sudo apt install openjdk-17-jdk-headless
L'API est une fabrique : vous ne construisez jamais d'encodeur ; vous demandez à la classe de vous en donner un. Le côté encodeur a quatre portes, toutes renvoyant des instances de la classe imbriquée Base64.Encoder :
| Méthode de fabrique | Alphabet | Forme de sortie |
|---|---|---|
getEncoder() |
A-Z a-z 0-9 + / |
Avec padding, pas de retours à la ligne |
getUrlEncoder() |
A-Z a-z 0-9 - _ |
Avec padding, pas de retours à la ligne |
getMimeEncoder() |
A-Z a-z 0-9 + / |
Avec padding, lignes de 76 caractères, CRLF |
getMimeEncoder(int, byte[]) |
A-Z a-z 0-9 + / |
Avec padding, votre longueur de ligne, votre séparateur |
Trois propriétés méritent d'être connues d'avance. Les instances sont thread-safe, et la fabrique renvoie la même instance partagée à chaque appel, donc Base64.getEncoder() == Base64.getEncoder() est vrai ; construisez-en une dans un champ statique et partagez-la partout. Les encodeurs ne lèvent jamais sur les données : chaque valeur d'octet a un encodage, donc il n'y a pas d'état « entrée invalide » à gérer, et les seules exceptions que vous rencontrerez portent sur une mauvaise configuration (un mauvais séparateur de ligne) ou un tableau de destination trop petit. Et chaque encodeur de cette liste ajoute du padding par défaut ; la molette qui l'éteint, withoutPadding(), apparaît dans la section base64url, parce que c'est là que vous en aurez besoin.
Vous croiserez encore de vieilles bibliothèques dans les bases de code, alors voici une carte rapide du paysage. Apache Commons Codec (actuellement 1.22.1) livre sa propre org.apache.commons.codec.binary.Base64 depuis la 1.0, avec une API Builder qui expose la politique strict ou permissif, la longueur de ligne et le séparateur comme des molettes ; c'est le bon outil seulement si vous devez soutenir des JVM antérieures à Java 8. Guava livre com.google.common.io.BaseEncoding, un vétéran aux capacités comparables, toujours courant dans les piles big data. Pour tout ce qui tourne sur une JVM moderne, java.util.Base64 est la valeur par défaut : zéro dépendance, et les benchmarks de la communauté le trouvent toujours le plus rapide du lot (on y revient dans la section sécurité et vitesse).
Votre premier encodage
Quatre-vingt-dix pour cent de la vie de l'encodage tiennent en trois lignes. Voici toute la cérémonie, en utilisant le plus petit exemple que l'article Wikipédia sur Base64 emploie pour expliquer l'alphabet :
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
}
}
La chaîne TWFu est l'exemple que l'article Wikipédia sur Base64 emploie pour expliquer l'alphabet, alors si votre encodeur en fait « Man », la machine est honnête. Mais regardez la première ligne de cet exemple, parce que c'est la ligne où l'encodage a vraiment lieu en Java. Il n'existe pas de méthode encodeToString(String), exprès. Une String Java est une séquence d'unités de code UTF-16, pas des octets, et Base64 est un format d'octets, alors l'API vous fait trancher la question des octets vous-même : "Man".getBytes(StandardCharsets.UTF_8). Cet unique appel, avec un charset explicite, est l'endroit où « café » reste correct pour les cent prochaines années, et c'est l'habitude la plus importante de tout cet article. La section suivante lui est dédiée, parce que l'alternative est le classique bug du charabia.
Deux notes sur la deuxième ligne. encodeToString() renvoie une String construite à partir des octets encodés ; la javadoc explique qu'elle construit le résultat en utilisant le charset ISO-8859-1, ce qui n'a aucune importance en pratique, parce que chaque caractère de sortie Base64 est du pur ASCII et se regarde pareil en Latin-1, UTF-8 et l'essentiel du reste de la ménagerie des charsets. Et si vous préférez posséder vous-même le tampon de sortie, encode(byte[]) renvoie un byte[] neuf, et encode(byte[] src, byte[] dst) écrit dans une destination que vous fournissez, en renvoyant le nombre (et en levant IllegalArgumentException: Output byte array is too small for encoding all input bytes si la destination est courte, sans écrire le moindre octet).
La décision du charset
Rendons concrète l'étape chaîne-vers-octets avec le cas classique. Le mot « café » est un mot, mais en octets tout dépend du charset que vous avez choisi :
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 : l'accent fait deux octets en UTF-8, un en Latin-1
System.out.println(Base64.getEncoder().encodeToString(utf8));
// Y2Fmw6k=
System.out.println(Base64.getEncoder().encodeToString(latin1));
// Y2Fm6Q==
}
}
Deux chaînes Base64 différentes pour un seul mot, et les deux sont « correctes » tant qu'on dit au lecteur quel charset utiliser. Toute la leçon tient en une ligne : l'encodeur est fidèle aux octets que vous lui donnez, et vous êtes responsable des octets. En pratique, cela veut dire : convenir de l'UTF-8 avec votre partenaire, passer StandardCharsets.UTF_8 explicitement, et écrire le charset dans la spec, le schéma ou le message de commit, parce que personne du côté récepteur ne peut le deviner à partir du Base64 seul. Le jumeau côté décodeur de ce bug est le sujet du guide sœur.
Une note de version, parce qu'elle change le mode d'échec du code paresseux. Le new String(bytes) sans argument et le String.getBytes() sans charset utilisent le charset par défaut de la plateforme, qui historiquement était Cp1252 sur Windows et quelque chose qui dépend de la locale sur Linux. Depuis le JDK 18 (JEP 400, « UTF-8 by Default »), la valeur par défaut est UTF-8 sur toutes les plateformes, donc sur une JVM moderne la forme paresseuse est juste par hasard. Cela ne la rend pas sûre : votre code survivra au JDK pour lequel il a été écrit, et la personne qui l'hérite ne devrait pas avoir à savoir ce qu'est la valeur par défaut. Écrivez le charset.
Un détail de conception apparenté : il n'existe nulle part dans l'API de surcharge encode(String), et c'est délibéré. Toutes les autres étapes du pipeline (tableaux, tampons, flux) prennent des octets, et une méthode acceptant une String aurait dû choisir un charset pour vous, ce qui est exactement la décision que le JDK refuse de prendre. L'unique méthode de type String qui existe, encodeToString, est du côté sortie, où la question du charset n'existe pas : la sortie Base64 est du pur ASCII. Toute la forme de l'API est un petit argument pour « décidez vos octets exprès ».
Padding, enveloppement et la molette MIME
Les encodeurs Java prennent deux décisions de mise en forme à votre place par défaut, et les deux méritent d'être comprises parce que les deux sont des molettes que vous pouvez tourner. La première est le padding : chaque encodeur ajoute les caractères = qui rendent la sortie un multiple de quatre, comme le demande la RFC 4648 : les implémentations DOIVENT inclure les caractères de padding appropriés à la fin des données encodées, sauf si la spécification de référence dit le contraire. La seconde est le repli de ligne : seul l'encodeur MIME enveloppe, à 76 caractères avec un retour chariot et un retour à la ligne, et il n'ajoute pas de séparateur de ligne après la dernière ligne partielle, un détail que la javadoc souligne explicitement et que d'autres outils ratent :
| Encodeur | Pad la sortie | Enveloppe les lignes | Séparateur de ligne |
|---|---|---|---|
getEncoder() |
oui | non | n/a |
getUrlEncoder() |
oui | non | n/a |
getMimeEncoder() |
oui | oui, 76 caractères | CRLF |
getMimeEncoder(64, "\n") |
oui | oui, 64 caractères | LF |
La molette MIME est la partie la plus utile de l'API pour ceux qui héritent des formats des autres. Le constructeur standard est getMimeEncoder() (76, CRLF, direct de la RFC 2045) ; la version à deux arguments, getMimeEncoder(int lineLength, byte[] lineSeparator), vous laisse reproduire d'autres conventions. Les deux bizarreries à connaître : la longueur de ligne est « arrondie vers le bas au multiple de 4 le plus proche », alors demander 77 vous donne silencieusement 76, et une valeur arrondie qui n'est pas positive vous donne zéro enveloppement ; et le séparateur ne doit contenir aucun caractère de l'alphabet Base64, ou le constructeur lève une IllegalArgumentException sur le champ, parce qu'un séparateur qui pourrait être confondu avec des données est un bug en attente de se produire. Voici la molette en action, au standard MIME et à la saveur PEM :
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));
// lignes de 76 caractères, CRLF entre elles
System.out.println(pem.encodeToString(data));
// lignes de 64 caractères, LF seuls entre elles
}
}
Deux notes pratiques. Si votre consommateur s'attend à ce qu'une chaîne enveloppée se termine par un retour à la ligne (certain outillage de courrier le fait), ajoutez-le vous-même après l'encodage : le JDK s'arrête délibérément après la dernière ligne partielle. Et si vous produisez des données qui vivront dans une URL ou un jeton, l'enveloppement est la mauvaise molette tout court ; ces consommateurs veulent une seule longue ligne et généralement sans padding, ce qui est la section suivante.
base64url et la molette sans padding
Le Base64 standard termine son alphabet par + et /, et ce sont exactement les deux caractères qui se mal comportent dans les URL : un + dans une chaîne de requête est déjà une espace avant que le serveur ne l'ait jamais analysée, un / est un séparateur de chemin, et un = en traîne veut être encodé en pourcentage, en un monstre à trois caractères. La RFC 4648, section 5, dessine le correctif : l'alphabet sûr pour URL et noms de fichiers, où + devient -, / devient _, et le padding final = est typiquement omis quand la longueur est connue implicitement. La RFC est catégorique sur le nom : cet encodage « ne doit pas être considéré comme le même que l'encodage base64 », et le nom que vous entendrez est base64url. Les JSON Web Tokens, les paramètres de state OAuth, les identifiants de session API et les identifiants vidéo de onze caractères vivent tous dans ce dialecte.
Java vous donne l'alphabet avec getUrlEncoder(), mais voici la molette qui prend les gens : l'encodeur URL-safe pad encore par défaut, et les standards des jetons ne veulent pas de padding. La RFC 7515 est explicite : les parties JWS utilisent le base64url « en omettant tous les caractères '=' de fin ... et sans inclure aucun retour à la ligne, aucun espace blanc ni aucun autre caractère supplémentaire ». Alors la recette JWT canonique en Java est une chaîne de deux méthodes :
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
}
}
L'appel withoutPadding() renvoie une nouvelle instance d'encodeur qui se comporte identiquement sauf qu'elle omet les pads de fin ; l'originelle reste intacte, et la javadoc le dit précisément. Le côté décodeur accepte à la fois les entrées avec et sans padding, alors une valeur que vous produisez sans padding reste lisible par un décodeur strict, voilà pourquoi sans padding est le choix sûr pour tout ce qui traverse une frontière API. Maintenant, une grosse réserve : les deux parties ci-dessus sont les moitiés non signées d'un JWT. Un vrai jeton a besoin d'une signature calculée sur « header.payload », et c'est de la cryptographie, pas de l'encodage. Pour la production, créez et vérifiez vos jetons avec une bibliothèque JOSE : JJWT (0.13.0) ou nimbus-jose-jwt (10.9.1). L'artefact API de JJWT, par exemple, n'est qu'à une coordonnée Maven :
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<!-- ajoutez jjwt-impl et jjwt-jackson à l'exécution, selon la doc du projet -->
Les identifiants YouTube sont l'autre face de cette molette : onze caractères de base64url sans padding, un identifiant qui doit survivre à être collé partout où une URL est autorisée. Si votre système génère des identifiants qui voyagent dans des URL, la chaîne withoutPadding() ci-dessus est la forme à copier.
Encoder des fichiers
Le travail de fichier du quotidien est le miroir du préféré du décodeur : lire un fichier, l'encoder, écrire le texte. Quatre lignes avec java.nio.file :
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());
}
}
Ce dernier print est la facture des 33 pour cent, rendue visible. Un fichier de 1 Mo devient environ 1,33 Mo de texte (4/3 de l'original, plus au plus deux caractères de padding), et si vous l'enveloppiez à la MIME, les retours à la ligne ajoutent quelques pour cent de plus : le vieux calcul de l'ère du courrier, toujours vrai, est 4/3 fois 78/76, soit environ 1,37 fois l'original pour un payload MIME enveloppé. Deux conséquences. D'abord, dimensionnez tout stockage ou champ de message depuis la longueur encodée, pas la longueur brute : une colonne VARCHAR(255) qui accepte joyeusement une valeur brute de 192 octets refusera son encodage de 256 caractères. Deuxièmement, c'est la direction encodage qui rend la mémoire pire, alors pour les gros fichiers la version à tableaux est le mauvais outil et la section streaming est le bon. Une petite joie pour les gens des fichiers : parce que les premiers caractères de sortie sont une fonction pure des premiers octets d'entrée, chaque PNG encodé en Base64 commence par iVBORw0K et chaque GIF encodé par R0lGOD ; vous pouvez reconnaître le type de fichier avant qu'un seul octet ne soit décodé.
JSON, API et data URIs
Deux des endroits les plus communs où la sortie encodée vit sur le câble.
Un : du binaire dans le JSON. Points de terminaison d'upload de fichiers, API de contenu, coffres à secrets et webhooks imbriquent du binaire sous forme de texte Base64 dans le JSON, parce que les octets bruts casseraient l'échappement des chaînes JSON. Le côté encodeur tient en une ligne à la frontière, et la seule décision est le dialecte que la spec demande :
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"));
// La spec demande du base64url sans padding :
String field = Base64.getUrlEncoder().withoutPadding().encodeToString(image);
// Remettez "field" à votre bibliothèque JSON comme simple valeur chaîne.
System.out.println(field.length());
}
}
Le piège, ce n'est pas l'encodage ; c'est la lecture de la spec. Certaines API veulent du Base64 standard avec padding, d'autres du base64url sans, et quelques-unes sont indulgentes avec les deux. Quand la spec se tait, le correctif le moins cher est de regarder une valeur d'exemple venue de l'autre côté : un - ou un _ n'importe où tranche l'alphabet, et des = en fin de chaîne tranchent le padding. Se tromper de dialecte ne fait généralement pas planter l'autre côté ; il corrompt généralement le fichier, ce qui est la classe de bug la plus lente à trouver.
Deux : les data URIs. La chaîne data:image/png;base64,... qui intègre une image dans du HTML ou du CSS est la data URI de la RFC 2397 : data:, un type média facultatif, un drapeau ;base64 facultatif, une virgule, puis les données. En construire une, c'est de la concaténation de chaînes, et la seule décision est de savoir si le drapeau est là (pas de drapeau veut dire que le payload est du texte encodé en pourcentage, ce que personne ne veut pour du binaire) :
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...
}
}
Le conseil de la RFC elle-même s'applique avec intérêt : les data URIs sont faites pour des valeurs courtes. Intégrer une icône de 50 Ko est un échange normal (une requête de moins) ; intégrer une photo de 5 Mo est un bug de performance déguisé en commodité. Gardez le drapeau, gardez le type média honnête, et gardez les octets petits.
Construire l'en-tête d'authentification Basic
L'en-tête d'authentification le plus ancien du web est toujours le cas d'usage Base64 le plus simple en Java, parce que c'est exactement un appel d'encodage. Selon la RFC 7617, une requête Basic envoie Authorization: Basic suivi de l'encodage Base64 de username:password ; l'exemple de la RFC elle-même, QWxhZGRpbjpvcGVuIHNlc2FtZQ==, est « Aladdin:open sesame » portant un déguisement. Côté client, construire l'en-tête, c'est deux lignes de Base64 plus un appel HTTP moderne :
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());
}
}
Trois mises en garde appartiennent à cet en-tête. D'abord, la RFC est explicite : Basic est de l'encodage, pas de la protection : les identifiants sont lisibles par n'importe qui qui peut voir les paquets, alors cet en-tête n'est fort que de l'HTTPS en dessous, et c'est une mauvaise idée sur tout ce qui n'est pas TLS. Deuxièmement, le charset : la RFC s'attend à des identifiants US-ASCII (UTF-8 pour le reste, et le paramètre d'authentification charset est indicatif), alors choisissez StandardCharsets.UTF_8 et restez cohérent des deux côtés. Troisièmement, une note de version : le client java.net.http vient de Java 11 ; sur une JVM plus ancienne, le même en-tête part sur une HttpURLConnection avec un appel setRequestProperty, et la ligne Base64 est identique dans les deux cas. Du côté serveur du même en-tête, l'analyse et le décodage sont l'exemple du guide sœur, avec le découpage sur le premier deux-points et la comparaison en temps constant. Les deux côtés sont deux appels de la même API, et c'est l'élégance discrète de celui-ci.
Valeurs dans les config, les variables d'env et les colonnes
Base64 est un conteneur de texte, voilà pourquoi il apparaît dans des endroits où vous ne l'attendez pas : un DSN de base de données avec des points-virgules dans un fichier d'env, un mot de passe avec des guillemets dans un fichier de propriétés, un certificat multi-lignes dans une config map, un blob binaire dans une colonne TEXT parce que le schéma a été dessiné avant que quiconque ait envisagé les BLOB. Le côté encodage est un appel, et le cadrage honnête est ce que c'est : une astuce de sécurité de format, pas une astuce de secret :
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);
}
}
Deux règles gardent cela honnête. D'abord, ne stockez jamais un secret en Base64 et en l'appelant chiffré : Base64 n'ajoute aucune entropie et n'enlève aucune information, le moment où un développeur lit le fichier il peut décoder la valeur en un appel, et la section sécurité de la RFC pointe exactement cet échec : des gens qui révèlent leurs identifiants en collant des échanges de protocole « encodés ». Si la valeur est secrète, chiffrez-la d'abord, et seulement ensuite emballez le chiffrement en Base64 si le canal exige du texte. Deuxièmement, budgetisez la taille : la valeur stockée est environ un tiers plus grosse que l'origine, et une colonne ou un champ qui contenait la valeur brute ne contiendra pas l'encodée. Et quand la valeur revient, décodez-la à la frontière et gardez-la en octets (pour le binaire) ou en chaîne à charset explicite (pour le texte) ; cette direction est le territoire du guide sœur.
Le streaming pour les grosses données
L'encodage est la direction qui rend la mémoire pire, alors l'histoire des gros fichiers ici tourne autour du fait de garder l'ensemble de travail petit. La version à tableaux de l'exemple de la section fichiers va bien jusqu'au moment où le fichier cesse de tenir confortablement en mémoire ; au-delà, l'adaptateur de flux est le bon coup. wrap(OutputStream) renvoie un flux de sortie qui encode au fil de vos écritures, alors un fichier de plusieurs gigaoctets n'est jamais tenu comme un seul tableau d'octets :
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();
}
}
Il y a un comportement sur ce flux qui mérite un surlignage, parce que la javadoc elle-même pointe dessus : le flux enveloppé peut retenir quelques octets résiduels en interne, et la pratique recommandée est de « fermer promptement le flux de sortie retourné après usage, au cours de laquelle il affichera tous les octets résiduels possibles vers le flux de sortie sous-jacent ». Si vous arrêtez d'écrire et lisez le fichier de sortie avant de fermer, la queue de vos données est encore assise dans l'encodeur, et le fichier a l'air tronqué. C'est pour cela que l'exemple ferme packed avant que rien d'autre ne touche le fichier, et en production vous mettriez les deux flux dans un bloc try-with-resources. Prenez l'habitude : sur le flux d'encodage, fermer fait partie de l'encodage.
Le vieux garde
Les bases de code héritées sont pleines d'API Base64 qui précèdent java.util.Base64, et les reconnaître vous épargne les mystères du « pourquoi ceci enveloppe-t-il ma sortie ». Les quatre que vous croiserez vraiment :
| API | Où vous le croiserez | Que faire |
|---|---|---|
sun.misc.BASE64Encoder / BASE64Decoder |
Code antérieur à Java 8 | Migrez vers java.util.Base64 ; supprimé dans Java 9 |
javax.xml.bind.DatatypeConverter |
Code de l'ère XML, vieux services web | Supprimé dans Java 11 (JEP 320) ; migrez |
org.apache.commons.codec.binary.Base64 |
Code qui doit tourner sur des JVM pré-8 | Gardez pour le support pré-8 ; sinon la classe du JDK est la valeur par défaut |
com.google.common.io.BaseEncoding |
Piles riches en Guava et big data | Va très bien ; la classe du JDK n'a pas de dépendances |
Le duo sun.misc est celui qui porte le drame. C'était une API interne, non supportée (le genre qui compile très bien sur le JDK du moment et disparaît sans avertissement de dépréciation), et sa sortie avait ses propres habitudes, comme envelopper le texte encodé par lignes, et c'est là que viennent une bonne part des bugs du type « mon Base64 a des retours à la ligne dedans ». Quand Java 9 est sorti en septembre 2017, le nettoyage du système de modules l'a supprimée, et le guide officiel de migration ne mâche pas ses mots : « Notons que sun.misc.BASE64Encoder et sun.misc.BASE64Decoder ont été supprimés. À la place, utilisez la classe prise en charge java.util.Base64, ajoutée dans le JDK 8 ». Si vous lancez jdeps sur du code qui référence encore les vieilles classes, l'outil signale la dépendance comme « JDK removed internal API », ce qui est le plus proche d'un cône de chantier que le JDK sache faire. Le DatatypeConverter de JAXB a eu une vie plus longue mais similaire, déprécié avec les modules Java EE dans l'ère Java 9 et supprimé tout court dans Java 11 par la JEP 320, « Supprimer les modules Java EE et CORBA ». Les deux migrations sont mécaniques : les vieux appels printBase64Binary et BASE64Encoder().encode se mappent un-à-un sur getEncoder().encodeToString, à part les différences d'enveloppement, et une fois le code sur java.util.Base64, il tourne sur chaque JDK de 8 à 26 sans plus de réflexion.
Sécurité et vitesse
La section sécurité est courte, parce que le travail de l'encodeur ne peut pas échouer sur des données, mais elle n'est pas vide. Base64 n'est pas du chiffrement, et la norme le dit en des termes qui ne laissent pas de place au doute : l'encodage Base « masque visuellement des informations autrement facilement reconnaissables, comme des mots de passe, mais ne fournit aucune confidentialité calculatoire », et la même section note que cela « a été connu pour causer des incidents de sécurité ». Les corollaires pratiques du côté encodeur : n'encodez pas un secret pour le rendre sûr (il est maintenant moins sûr, parce qu'il passe dans plus de canaux) ; si la valeur est secrète, chiffrez d'abord et encodez le chiffrement ; et gardez en tête le jumeau de la malléabilité, où un récepteur peut échanger une orthographe valide contre une autre (padding différent, saleté dans les bits libres) sans changer les données décodées. Un encodeur déterministe aide ici : java.util.Base64 produit exactement une sortie pour exactement une entrée, alors si votre propre système écrit et lit une valeur, l'orthographe est stable, et ce sont les valeurs externes à la frontière de confiance qui ont besoin d'un contrôle de forme canonique.
Sur la vitesse, le côté encodeur a la même histoire que le côté décodeur : sur une JVM moderne, l'implémentation intégrée est assez rapide pour que Base64 soit presque jamais le goulot d'étranglement, et c'est le point de référence du benchmark. Le même benchmark gRPC-java de 2025 mentionné dans le guide sœur (issue 11857, JMH sur les JDK 17 et 21) a mis l'encodeur JDK à environ 2,5 à 3,8 fois le débit de celui de Guava, avec le plus gros écart sur x86. Deux notes pratiques : pour les chemins critiques, partagez une instance d'encodeur (la fabrique renvoie déjà la même partagée) et préférez encode(byte[], byte[]) vers un tableau pré-dimensionné pour éviter l'allocation ; pour d'énormes données, la section streaming est l'histoire mémoire, et le coût de l'enveloppement est du bruit à côté du disque. L'unique vraie taxe de performance du Base64, c'est la taille elle-même, et aucune implémentation, y compris celle-ci, ne peut la négocier plus bas.
La liste de contrôle des pièges
Tous les pièges réunis en un seul endroit, tous spécifiques Java :
- Le charset manquant.
text.getBytes()sans charset explicite utilise la valeur par défaut de la plateforme : juste par hasard sur JDK 18+, faux sur tout ce qui est plus ancien, et faux en principe partout. PassezStandardCharsets.UTF_8et écrivez le charset dans la spec. - Le JWT avec padding.
getUrlEncoder()pad par défaut, et les jetons ne veulent pas de padding. L'appelwithoutPadding()fait partie de la recette, pas un extra facultatif ; un jeton avec des=en fin de chaîne est un jeton que certains validateurs rejetteront et d'autres massacreront. - La sortie enveloppée. L'encodeur MIME enveloppe à 76 avec CRLF et n'ajoute pas de retour à la ligne en fin de chaîne. Si le consommateur s'attend à un retour de fin, ajoutez-le ; s'il s'attend à aucun retour du tout, n'utilisez pas l'encodeur MIME.
- Le double encodage. Encoder une valeur qui est déjà du Base64 produit une chaîne parfaitement valide, parfaitement inutile. La cause classique : un champ arrive pré-encodé d'une API et votre code « aimablement » l'encode encore une fois. Vérifiez avant d'encoder.
- Les signes plus dans les URL. La sortie Base64 standard contient
+, qui est une espace dans une chaîne de requête avant que le serveur ne le voie. Si une valeur à alphabet standard doit voyager dans une URL, encodez-la en pourcentage, ou générez-la d'office dans l'alphabet URL-safe. - La facture des 33 pour cent. Une valeur qui tient dans la colonne brute ne tiendra pas dans l'encodée. Dimensionnez stockages, champs de message et en-têtes depuis
4 * ceil(n / 3), et souvenez-vous que la sortie MIME enveloppée ajoute quelques pour cent par-dessus. - Le flux non fermé. Le flux de sortie enveloppé retient les octets résiduels jusqu'à la fermeture. Lire le fichier avant la fermeture vous donne un encodage tronqué. Try-with-resources, à chaque fois.
- Les secrets en plein jour. Base64 est du ruban d'emballage, pas un cadenas. Des identifiants encodés dans un fichier de configuration, un log ou une variable d'env sont des identifiants lisibles. Chiffrez d'abord, ou pas du tout.
- Le mur Android. Sur Android,
java.util.Base64n'existe qu'à partir du niveau API 26 ; en dessous, la classe du framework estandroid.util.Base64avec ses propres constantes de drapeaux (NO_PADDING,URL_SAFEetNO_WRAP). Durcir l'un d'eux sans vérification casse sur exactement les appareils que vous n'avez jamais testés. - La bizarrerie de longueur de ligne.
getMimeEncoder(77, ...)enveloppe silencieusement à 76, parce que la longueur est arrondie vers le bas à un multiple de quatre, et demander 3 ou moins désactive l'enveloppement tout court. Si votre format exige une longueur de ligne impaire, la molette MIME n'est pas l'outil.
De sun.misc à la bibliothèque standard
L'histoire Java est courte et a un avant et un après bien nets. Avant 2014, si vous aviez besoin de Base64 dans le JDK, vous obteniez le duo interne sun.misc.BASE64Encoder et sun.misc.BASE64Decoder, non supporté dès le premier jour, avec ses propres habitudes d'enveloppement à 76 caractères, ou vous alliez vers javax.xml.bind.DatatypeConverter dans du code XML, ou vous ajoutiez Apache Commons Codec ou Guava au build, et c'est comme ça que beaucoup de bases de code d'entreprise se sont retrouvées avec trois implémentations Base64 et aucune idée de laquelle était laquelle. Le 18 mars 2014, Java 8 a livré java.util.Base64 : une classe, trois alphabets, les règles de la RFC 4648 et de la RFC 2045 implémentées correctement, le patron de fabrique, les molettes de padding et d'enveloppement, et des adaptateurs de flux dans les deux sens. C'était le Base64 que la langue aurait dû avoir dès le départ, et la javadoc dit Since: 1.8 depuis.
Le nettoyage est venu en deux vagues. Java 9 (21 septembre 2017) a supprimé le duo sun.misc dans le cadre du nettoyage du système de modules, avec le guide de migration qui pointe chaque développeur vers la classe du JDK 8, et Java 11 a supprimé le module JAXB et son DatatypeConverter avec lui (JEP 320). Java 18 (22 mars 2022) a fait atterrir la JEP 400, « UTF-8 by Default », qui n'a pas touché Base64 du tout mais a changé le mode d'échec des appels paresseux getBytes() qui l'alimentent : le charset par défaut de la plateforme est devenu UTF-8 sur tous les OS, alors les vieux motifs de charabia ont simplement cessé de se reproduire sur les nouvelles JVM. Depuis la 1.8, l'API publique n'a changé aucune méthode. Ce qui a bougé, c'est le moteur en dessous : corrections de bugs et travail de performance, voilà pourquoi les benchmarks de la communauté continuent de trouver la version de la bibliothèque standard plus rapide que les bibliothèques héritage qu'elle a remplacées. Aujourd'hui, sur n'importe quel JDK de 8 à 26, la réponse à « comment est-ce que je Base64 ça en Java » est un import et un appel de fabrique, et c'est le cas depuis plus d'une décennie.
Quelques délices de nerd
Parce qu'un manuel devrait se terminer sur un sourire, voici quelques faits spécifiques Java qui sont simplement amusants :
- La javadoc dit
Since: 1.8, et c'est vrai depuis douze ans. Pas une méthode ajoutée, pas une supprimée, pas un comportement changé : l'une des surfaces API les plus longtemps gelées de la langue, et vous l'utilisez sans y penser. encodeToStringconstruit sa String de résultat avec le charset ISO-8859-1, selon la javadoc. C'est un détail complètement inutile en pratique, parce que la sortie Base64 est du pur ASCII et se regarde pareil en Latin-1, UTF-8 et l'essentiel du reste de la ménagerie des charsets, mais la javadoc vous le dit quand même, et c'est ça, le JDK.- L'encodeur MIME n'ajoute pas de séparateur de ligne après la dernière ligne partielle. D'autres outils, y compris certaines bibliothèques de courrier très célèbres, terminent la sortie enveloppée par un CRLF final. Si votre diff contre une implémentation de référence fait exactement deux caractères en fin de chaîne, vous avez trouvé cette bizarrerie.
- Demandez à
getMimeEncoderdes lignes de 77 caractères et il vous en donne de 76 : la longueur de ligne est arrondie vers le bas au multiple de quatre le plus proche, en silence, parce qu'un repli qui coupe un groupe de quatre caractères produirait du n'importe quoi. L'API refuse de construire une ligne cassée plutôt que de vous demander votre permission. Base64.getEncoder() == Base64.getEncoder()est vrai. Les méthodes de fabrique renvoient la même instance partagée à chaque appel, alors l'API « obtenez-en un neuf » est un déguisement pour un singleton, et la promesse de thread-safety n'est qu'une description de ce que la JVM fait déjà.- Sur Android, l'API jumelle
android.util.Base64expose les mêmes décisions sous forme de drapeaux :NO_PADDING,URL_SAFE,NO_WRAP. Deux API, une table de décisions, et c'est un témoignage discret de la façon dont le design du Base64 est bien en place maintenant. - La section 5 de la RFC 4648 est l'endroit où naît le nom « base64url » : la spec dit que l'encodage URL-safe « peut être appelé base64url » et met en garde qu'il « ne doit pas être considéré comme le même que l'encodage base64 ». Son origine est mise en note d'après un message de 2001 sur une liste de diffusion P2P-hackers, alors le nom dans chaque URL que vous collez a un pedigree de liste de diffusion.
- Encodez le mot
base64et vous obtenezYmFzZTY0, sans padding, parce que six est un multiple de trois. Un format qui se décrit lui-même est l'équivalent technique d'un miroir qui parle en morse, et c'est ici le reflet du miroir lui-même. - Lancez
jdeps -jdkinternalssur du code antérieur à Java 8 et regardez-le signalersun.misc.BASE64Encodercomme « JDK removed internal API ». L'exemple de l'outil dans le guide officiel de migration est une classe Base64, et c'est le JDK qui pointe du doigt vos imports en disant « on en a déjà parlé ». - Le facteur 1,37. Chaque payload MIME enveloppé coûte environ 1,37 fois sa taille d'origine (4/3 pour l'alphabet, 78/76 pour le rythme CRLF), une fraction si stable que le vieux calcul du courrier la cite encore : le péage que l'infrastructure de courrier des années 1990 prélevait sur chaque pièce jointe est exactement la facture que
getMimeEncoder()fait payer aujourd'hui.
Partir dans l'autre sens
C'est le côté encodeur de l'histoire, et c'est le plus calme des deux : le travail n'échoue jamais sur les données, les pièges portent sur vos décisions (charset, padding, enveloppement, dialecte) plutôt que sur les surprises des autres, et toute l'API tient dans un import. L'autre direction est là où Base64 cesse d'être commode et commence à être hostile, parce que le décodage est l'endroit où vous rencontrez les choix de padding des autres, leurs retours à la ligne, leurs charsets et leur armure, avec une IllegalArgumentException plantée entre vous et la vérité. Le décodage Base64 en Java, lié depuis cette page, couvre le décodeur dans la même profondeur : les trois personnalités de décodeur, les messages d'erreur exacts, les règles de padding, le base64url et les JWT, le MIME et le PEM, et les pièges spécifiques Java réunis en un seul endroit. Lisez les deux en duo et le sujet entier est à vous.
Dernière mise à jour : 2026-09-08
Article associé : Décodage Base64 en Java : un guide complet