Base64-codering in Java: een complete gids
Dit is de situatie: je hebt bytes. Een bestand, een wachtwoord, een certificaat, een groet van 13 bytes, een upload van 200 megabytes. En je moet ze ergens onderbrengen dat alleen tekst snapt: een JSON-veld, een HTTP-header, een database-kolom, een URL, een config-bestand. Dat is het hele werk van Base64, en deze gids is het Java-handboek om het goed te doen. Even snel oriënteren, want de startpagina doorloopt het formaat stap voor stap: Base64 schrijft elke drie bytes data om in vier tekens uit een alfabet van 64 letters, met een of twee =-pads eraan vastgeplakt als het laatste blok te kort is. De prijs van de rit is de grootte: elke drie bytes worden vier tekens, dus de gecodeerde output komt zo'n 33 procent groter uit dan de invoer, plus nog wat als er regeleindes bij komen.
Het nieuws dat de kop waard is, en het is een goede. Sinds 18 maart 2014 levert elke JDK een compleet Base64-werkpakket in de standaardbibliotheek mee: java.util.Base64. Geen download, geen Maven-coördinaat, geen native bibliotheek. Eén import, drie encoderpersoonlijkheden, en hetzelfde gedrag van Java 8 tot het huidige Java 26. Alles in dit artikel is gebouwd op die ene class, en hij gooit nooit iets over de data zelf: het werk van de encoder kan niet mislukken op ongeldige invoer, want elke mogelijke byte is codeerbaar.
Eén eerlijke grens vóór we beginnen: dit is de encoderkant van het verhaal. Je leert de beslissing van tekenreeks naar bytes die de correctheid écht bepaalt, de padding- en wrapping-regelaars, base64url en zijn modus zonder padding voor tokens, en de gebruikscases waar Java-developers het vaakst gecodeerde output tegenkomen. Decoderen, waar het meeste echte gedoe woont, krijgt zijn eigen gids en is aan het einde van deze verlinkt.
Eén import, nul downloads
Base64 installeren in Java is het antwoord in één regel dat je aan het whiteboard geeft: "Het zit in de JDK." De class java.util.Base64 maakt sinds 1.8 deel uit van de module java.base, en de javadoc zegt twaalf jaar later nog steeds Since: 1.8. Het enige dat je installeert is een JDK: elke Java 8 of nieuwer van welke vendor dan ook (Oracle, Eclipse Temurin, Amazon Corretto, Zulu) doet het, en op een Debian-gebaseerde machine is dat één commando:
sudo apt install openjdk-17-jdk-headless
De API is een factory: je construeert nooit zelf een encoder, je vraagt de class er om. De encoderkant heeft vier deuren, die allemaal instanties teruggeven van de geneste class Base64.Encoder:
| Factory-methode | Alfabet | Outputvorm |
|---|---|---|
getEncoder() |
A-Z a-z 0-9 + / |
Gepad, geen regeleindes |
getUrlEncoder() |
A-Z a-z 0-9 - _ |
Gepad, geen regeleindes |
getMimeEncoder() |
A-Z a-z 0-9 + / |
Gepad, regels van 76 tekens, CRLF |
getMimeEncoder(int, byte[]) |
A-Z a-z 0-9 + / |
Gepad, jouw regellengte, jouw scheiding |
Drie eigenschappen zijn het van te voren weten waard. De instanties zijn thread-safe, en de factory geeft bij elke aanroep dezelfde gedeelde instantie terug, dus is Base64.getEncoder() == Base64.getEncoder() waar; bouw er één in een statisch veld en deel hem overal. De encoders gooien nooit iets over de data: elke byte-waarde heeft een codering, dus is er geen "ongeldige invoer"-state af te handelen, en de enige excepties die je zult tegenkomen gaan over misconfiguratie (een slechte regeleinde-scheiding) of een te kleine bestemmingsarray. En elke encoder op deze lijst voegt standaard padding toe; de regelaar die hem uitschakelt, withoutPadding(), verschijnt in de base64url-sectie, want daar heb je hem nodig.
Oude bibliotheken kom je in codebases nog tegen, dus even een snelle landkaart. Apache Commons Codec (momenteel 1.22.1) levert sinds 1.0 haar eigen org.apache.commons.codec.binary.Base64 mee, met een Builder-API die het strikt-of-liberaal-beleid, de regellengte en de scheiding als regelaars blootlegt; die is het juiste gereedschap alleen als je JVM's vóór Java 8 moet ondersteunen. Guava levert com.google.common.io.BaseEncoding, een even capabele veteraan, nog steeds gangbaar in big data-stacks. Voor alles op een moderne JVM is java.util.Base64 de standaard: nul afhankelijkheden, en community-benchmarks stellen steeds opnieuw vast dat het het snelst is van de hele ploeg (meer daarover in de sectie over beveiliging en snelheid).
Jouw eerste codering
Negentig procent van het coderen past in drie regels. Hier is de volledige ceremonie, met het kleinste voorbeeld dat het Base64-artikel van Wikipedia gebruikt om het alfabet uit te leggen:
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
}
}
De tekenreeks TWFu is het voorbeeld dat het Base64-artikel van Wikipedia gebruikt om het alfabet uit te leggen, dus als je encoder "Man" daarin verandert, is de machine eerlijk. Maar kijk naar de eerste regel van dat voorbeeld, want dat is de regel waar in Java écht het coderen gebeurt. Er is bewust geen encodeToString(String)-methode. Een Java-String is een reeks van UTF-16-code-eenheden, geen bytes, en Base64 is een byte-formaat, dus laat de API de bytesvraag aan jezelf over: "Man".getBytes(StandardCharsets.UTF_8). Die ene aanroep, met een expliciete tekenset, is waar "café" de volgende honderd jaar correct blijft, en het is de belangrijkste gewoonte in dit hele artikel. De volgende sectie is eraan gewijd, want het alternatief is de klassieke mojibake-bug.
Twee notities over de tweede regel. encodeToString() geeft een String terug die is gebouwd uit de gecodeerde bytes; de javadoc legt uit dat deze het resultaat opbouwt met de ISO-8859-1-tekenset, wat in de praktijk geen probleem is omdat elk Base64-output-teken gewoon ASCII is en er identiek uitziet in Latin-1, UTF-8 en het grootste deel van de rest van de tekenset-dierentuin. En als je liever zelf de outputbuffer bezit, geeft encode(byte[]) een vers byte[] terug, en schrijft encode(byte[] src, byte[] dst) in een bestemming die je aanlevert, en geeft het aantal terug (en gooit IllegalArgumentException: Output byte array is too small for encoding all input bytes als de bestemming te kort is, zonder een enkele byte te schrijven).
De tekensetbeslissing
Laten we de stap van tekenreeks naar bytes concreet maken met het klassieke geval. Het woord "café" is één woord, maar in bytes hangt het helemaal af van de tekenset die je koos:
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: het accent is twee bytes in UTF-8, één in Latin-1
System.out.println(Base64.getEncoder().encodeToString(utf8));
// Y2Fmw6k=
System.out.println(Base64.getEncoder().encodeToString(latin1));
// Y2Fm6Q==
}
}
Twee verschillende Base64-tekenreeksen voor één woord, en beide zijn "correct" zolang de lezer verteld krijgt welke tekenset hij moet gebruiken. De hele les past in één regel: de encoder is trouw aan de bytes die je geeft, en jij bent verantwoordelijk voor de bytes. In de praktijk betekent dat: spreek UTF-8 af met je tegenpartij, geef StandardCharsets.UTF_8 expliciet door, en schrijf de tekenset op in de spec, het schema of de commit message, want niemand aan de ontvangstkant kan het uit alleen de Base64 raden. De decoder-tweeling van deze bug is het onderwerp van de zuster-gids.
Eén versienota, want hij verandert de faalmode van lui code. De argumentloze new String(bytes) en de String.getBytes() zonder tekenset gebruiken de standaardtekenset van het platform, die historisch Cp1252 was op Windows en iets locale-afhankelijk op Linux. Sinds JDK 18 (JEP 400, "UTF-8 by Default") is de standaard op elk platform UTF-8, dus op een moderne JVM is de luie vorm toevallig juist. Dat maakt hem niet veilig: je code leeft langer dan de JDK waarvoor hij geschreven is, en de persoon die hem overneemt hoeft niet te weten wat de standaard is. Schrijf de tekenset.
Een gerelateerd detail in het ontwerp: ergens in de API bestaat geen enkele encode(String)-overload, en dat is bewust. Elke andere stap van de pipeline (arrays, buffers, streams) neemt bytes, en een methode die een String accepteert, zou voor jou een tekenset moeten kiezen, en dat is precies de beslissing die de JDK weigert te maken. De ene String-getypeerde methode die er is, encodeToString, zit aan de outputkant, waar de tekensetvraag niet bestaat: Base64-output is zuiver ASCII. De hele vorm van de API is een klein pleidooi voor "beslis je bytes bewust".
Padding, omwikkeling en de MIME-regelaar
De encoders van Java nemen standaard twee opmaakbeslissingen voor je over, en beide zijn het begrijpen waard omdat beide regelaars zijn die je kunt draaien. De eerste is padding: elke encoder voegt de =-tekens toe die de output een veelvoud van vier maken, zoals RFC 4648 eist: implementaties MOETEN geschikte padtekens toevoegen aan het einde van gecodeerde data, tenzij de refererende specificatie anders zegt. De tweede is het omwikkelen van regels: alleen de MIME-encoder wikkelt, op 76 tekens met een carriage return en line feed, en hij voegt geen regeleinde-scheiding toe na de laatste gedeeltelijke regel, een detail dat de javadoc expliciet benoemt en andere tools fout doen:
| Encoder | Padt de output | Wikkelde regels | Regeleinde-scheiding |
|---|---|---|---|
getEncoder() |
ja | nee | n.v.t. |
getUrlEncoder() |
ja | nee | n.v.t. |
getMimeEncoder() |
ja | ja, 76 tekens | CRLF |
getMimeEncoder(64, "\n") |
ja | ja, 64 tekens | LF |
De MIME-regelaar is het nuttigste deel van de API voor mensen die andermans formaten erven. De standaardconstructor is getMimeEncoder() (76, CRLF, rechtstreeks uit RFC 2045); de twee-argumenten versie, getMimeEncoder(int lineLength, byte[] lineSeparator), laat je andere conventies nabouwen. De twee rare weetjes om te kennen: de regellengte wordt "afgerond naar beneden tot het dichtstbijzijnde veelvoud van 4", dus om 77 vragen geeft je stilletjes 76, en een afgeronde waarde die niet positief is, geeft je helemaal geen omwikkeling; en de scheiding mag geen enkel teken van het Base64-alfabet bevatten, anders gooit de constructor ter plekke een IllegalArgumentException, want een scheiding die met data verward kan worden, is een bug die wacht om te gebeuren. Hier is de regelaar in actie, MIME-standaard en met een PEM-smaakje:
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));
// regels van 76 tekens, CRLF ertussen
System.out.println(pem.encodeToString(data));
// regels van 64 tekens, kale LF ertussen
}
}
Twee praktische notities. Als je consument verwacht dat een omwikkelde tekenreeks eindigt met een regeleinde (sommige mailtools doen dat), voeg het dan zelf toe na de encode: de JDK stopt bewust na de laatste gedeeltelijke regel. En als je data produceert die in een URL of een token gaat wonen, is omwikkeling helemaal de verkeerde regelaar; die consumenten willen één lange regel en doorgaans geen padding, en dat is de volgende sectie.
base64url en de regelaar zonder padding
Standaard Base64 eindigt zijn alfabet met + en /, en dat zijn precies de twee tekens die zich in URLs niet goed gedragen: een + in een querystring is al een spatie vóór de server hem ooit parst, een / is een scheidingsteken in het pad, en een ophangende = wil percent-gecodeerd worden tot een monster van drie tekens. RFC 4648 sectie 5 tekent de oplossing: het URL- en bestandsnaam-veilige alfabet, waar + wordt -, / wordt _, en de afsluitende =-padding doorgaans wordt weggelaten wanneer de lengte impliciet bekend is. De RFC is nadrukkelijk over de naam: deze codering "mag niet als dezelfde worden beschouwd als de base64-codering", en de naam die je zult horen is base64url. JSON Web Tokens, OAuth-stateparameters, API-sessie-IDs en video-IDs van elf tekens wonen allemaal in dit dialect.
Java geeft je het alfabet met getUrlEncoder(), maar hier is de regelaar die mensen vangt: de URL-safe encoder padt nog steeds standaard, en de token-standaarden willen geen padding. RFC 7515 stelt expliciet dat JWS-onderdelen base64url gebruiken "met alle afsluitende '='-tekens weggelaten ... en zonder de opname van enige regeleindes, witruimte of andere bijkomende tekens". Het canonieke Java-JWT recept is dus een keten van twee methoden:
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
}
}
De withoutPadding()-aanroep geeft een nieuwe encoder-instantie terug die zich identiek gedraagt, behalve dat deze de afsluitende pads weglaat; het origineel blijft onaangetast, en de javadoc zegt dat precies. Aan de decoderkant worden zowel gepad als ongepad ingestuurde waarden geaccepteerd, dus een waarde die je zonder padding produceert, blijft leesbaar voor een strikte decoder, en daarom is ongepad de veilige keuze voor alles dat een API-grens oversteekt. Nu, één grote disclaimer: de twee delen hierboven zijn de helften van een JWT zonder handtekening. Een echt token heeft een handtekening nodig die wordt berekend over "header.payload", en dat is cryptografie, geen codering. Voor productie laat je tokens uitgeven en verifieer je ze met een JOSE-bibliotheek: JJWT (0.13.0) of nimbus-jose-jwt (10.9.1). Het API-artefact van JJWT is bijvoorbeeld één coördinaat verwijderd:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<!-- voeg jjwt-impl en jjwt-jackson toe in de runtime, volgens de projectdocumentatie -->
YouTube-IDs zijn de andere kant van deze regelaar: elf tekens base64url zonder padding, een identificeerder die moet overleven overal geplakt te worden waar een URL mag. Als je systeem identificeerders genereert die reizen in URLs, is de withoutPadding()-keten hierboven de vorm om te kopiëren.
Bestanden coderen
Het alledaagse bestandswerk is de spiegel van de favoriete klus van de decoder: een bestand lezen, het coderen, de tekst weg schrijven. Vier regels met 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());
}
}
Die laatste print is de 33-procenten rekening, zichtbaar gemaakt. Een bestand van 1 MB wordt ruwweg 1,33 MB aan tekst (4/3 van het origineel, plus hooguit twee padtekens), en als je het MIME-achtig had omwikkelde, voegen de regeleindes nog een paar procent toe: de oude rekensom uit het mailtijdperk, nog steeds waar, is 4/3 keer 78/76, of ongeveer 1,37 keer het origineel voor een omwikkelde MIME-payload. Twee gevolgen. Ten eerste, dimensioneer opslag of een berichtenveld vanuit de gecodeerde lengte, niet de ruwe lengte: een VARCHAR(255)-kolom die met plezier een ruwe waarde van 192 bytes bevat, wijst de gecodeerde vorm van 256 tekens af. Ten tweede is de coderingsrichting de kant die het geheugen verergert, dus voor grote bestanden is de array-versie het verkeerde gereedschap en de streamingsectie de juiste. Een klein plezier voor de bestands-clan: omdat de eerste output-tekens een pure functie zijn van de eerste invoer-bytes, begint elke Base64-gecodeerde PNG met iVBORw0K en elke gecodeerde GIF met R0lGOD; je kunt het bestandstype herkennen vóór er een enkele byte gedecodeerd is.
JSON, APIs en data URIs
Twee van de plekken waar gecodeerde output het vaakst op de draad zit.
Eén: binair in JSON. Upload-endpoints, content-API's, secret-stores en webhooks embedden binair als Base64-tekst in JSON, want ruwe bytes zouden JSON-string-escaping breken. De encoderkant is aan de grens één regel, en de ene beslissing is welk dialect de spec vraagt:
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"));
// De spec zegt base64url, ongepad:
String field = Base64.getUrlEncoder().withoutPadding().encodeToString(image);
// Geef "field" aan je JSON-bibliotheek door als een gewone string-waarde.
System.out.println(field.length());
}
}
De val is niet coderen; het is de spec lezen. Sommige APIs willen standaard Base64 met padding, sommige willen base64url zonder, en een paar zijn liberaal over beide. Als de spec zwijgt, is de goedkoopste fix om naar een voorbeeldwaarde van de andere kant te kijken: een - of _ ergens beslist het alfabet, en afsluitende = beslist de padding. Het dialect fout hebben kraakt de andere kant doorgaans niet; het corrumpeert gewoonlijk het bestand, en dat is de traagste soort bug om te vinden.
Twee: data URIs. De data:image/png;base64,...-tekenreeks die een afbeelding inline zet in HTML of CSS is het data URI van RFC 2397: data:, een optionele media type, een optionele ;base64-vlag, een komma, dan de data. Eén bouwen is string-concatenatie, en de ene beslissing is of de vlag er is (geen vlag betekent dat de payload percent-gecodeerde tekst is, wat niemand voor binair wilt):
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...
}
}
Het eigen advies van de RFC geldt met belangstelling: data URIs zijn voor korte waarden. Een icoon van 50 KB inline zetten is een normale ruil (één verzoek minder); een foto van 5 MB inline zetten is een prestatiebug in een handigheidskostuum. Houd de vlag, houd de media type eerlijk, en houd de bytes klein.
De Basic auth header bouwen
De oudste authenticatieheader op het web is nog steeds het eenvoudigste Base64-gebruiksgeval in Java, want het is exact één encode-aanroep. Volgens RFC 7617 stuurt een Basic-verzoek Authorization: Basic gevolgd door de Base64-codering van username:password; het eigen voorbeeld van de RFC, QWxhZGRpbjpvcGVuIHNlc2FtZQ==, is "Aladdin:open sesame" in vermomming. Aan de clientkant is het bouwen van de header twee regels Base64 plus een moderne HTTP-aanroep:
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());
}
}
Drie waarschuwingen horen bij deze header. Ten eerste is de RFC expliciet dat Basic codering is, geen bescherming: de inloggegevens zijn leesbaar voor iedereen die de pakketten kan zien, dus deze header is slechts zo sterk als de HTTPS eronder, en op iets anders dan TLS is het een slecht idee. Ten tweede, de tekenset: de RFC verwacht US-ASCII inloggegevens (UTF-8 voor alles anders, en de charset-auth-parameter is adviseerlijk), dus kies StandardCharsets.UTF_8 en blijf aan beide kanten consistent. Ten derde, een versienota: de java.net.http-client is van Java 11; op een oudere JVM gaat dezelfde header op een HttpURLConnection met één setRequestProperty-aanroep, en de Base64-regel is in beide gevallen identiek. Aan de serverkant van dezelfde header is het analyseren en decoderen het voorbeeld van de zuster-gids, met de split op het eerste dubbelepunt en de vergelijking in vaste tijd. De twee kanten zijn twee aanroepen van dezelfde API, en dat is de stille elegantie van dit stuk.
Waarden in configs, omgevingsvariabelen en kolommen
Base64 is een tekstcontainer, en daarom duikt het op in plekken waar je het niet zou verwachten: een database-DSN met puntkommas in een env-bestand, een wachtwoord met aanhalingstekens in een properties-bestand, een meerregelig certificaat in een config-map, een binair blob in een TEXT-kolom omdat het schema ontworpen was vóór iemand aan BLOB's had gedacht. De coderingskant is één aanroep, en de eerlijke framing is wat het is: een formatveiligheidstruc, niet een geheimhoudingstruc:
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);
}
}
Twee regels houden dit eerlijk. Ten eerste, sla nooit een secret op als Base64 en noem het versleuteld: Base64 voegt geen entropie toe en verwijdert geen informatie, het moment dat een developer het bestand leest, kan hij de waarde in één aanroep decoderen, en de beveiligingssectie van de RFC wijst precies naar dit falen, mensen die inloggegevens onthullen door "gecodeerde" protocolruilen te plakken. Als de waarde geheim is, versleutel het eerst, en pak het ciphertext dan pas in Base64 als het kanaal tekst vereist. Ten tweede, houd de grootte binnen budget: de opgeslagen waarde is ongeveer een derde groter dan het origineel, en een kolom of veld die de ruwe waarde paste, past de gecodeerde niet. En als de waarde terugkomt, decodeer hem aan de grens en houd hem vast als bytes (voor binair) of een tekenreeks met expliciete tekenset (voor tekst); die richting is het domein van de zuster-gids.
Streamen voor grote data
Coderen is de richting die het geheugen verergert, dus het grote-bestandenverhaal hier gaat over de werkverzameling klein houden. De array-versie van het voorbeeld in de bestanden-sectie is prima tot het punt waar het bestand niet meer comfortabel in het geheugen past; daarboven is de stream-adapter de oplossing. wrap(OutputStream) geeft een output stream terug die codeert terwijl je schrijft, zodat een bestand van meerdere gigabytes nooit als één byte-array wordt vastgehouden:
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();
}
}
Er is één gedrag aan deze stream dat een uitluchting verdient, want de javadoc zelf wijst ernaar: de omwikkelde stream kan intern een paar overgebleven bytes vasthouden, en de aanbevolen praktijk is om "de teruggegeven output stream na gebruik onmiddellijk te sluiten, waarna alle mogelijke overgebleven bytes naar de onderliggende output stream worden geflucht". Stop je met schrijven en lees je het outputbestand vóór het sluiten, dan zit de staart van je data nog in de encoder, en lijkt het bestand afgesneden. Daarom sluit het voorbeeld packed vóór iets anders het bestand raakt, en in productie zou je beide streams in een try-with-resources-blok plaatsen. Krijg de gewoonte: op de coderingsstream is sluiten onderdeel van coderen.
De oude garde ontmoeten
Geërfdde codebases zitten vol Base64-API's die vóór java.util.Base64 zijn, en ze herkennen spaart je de "waarom wikkelt dit mijn output"-mysterie. De vier die je écht zult tegenkomen:
| API | Waar je het tegenkomt | Wat te doen |
|---|---|---|
sun.misc.BASE64Encoder / BASE64Decoder |
Code van vóór Java 8 | Migreer naar java.util.Base64; verwijderd in Java 9 |
javax.xml.bind.DatatypeConverter |
Code uit het XML-era, oude web services | Verwijderd in Java 11 (JEP 320); migreer |
org.apache.commons.codec.binary.Base64 |
Code die op pre-8 JVM's moet draaien | Houd voor pre-8-ondersteuning; anders is de JDK-class de standaard |
com.google.common.io.BaseEncoding |
Guava-rijke en big data-stacks | Werkt prima; de JDK-class heeft geen afhankelijkheden |
Het sun.misc-paar is het met de drama's. Het was een interne, niet-ondersteunde API (het soort dat prima compileert op de JDK van die dag en zonder deprecation-waarschuwing verdwijnt), en zijn output had zijn eigen gewoontes, zoals het omwikkel van de gecodeerde tekst, en vandaar komt een opvallend aantal "mijn Base64 heeft nieuwe regels in"-bugs. Toen Java 9 in september 2017 verscheen, verwijderde de module-systeem-schoonmaak dit, en de officiële migratiegids is er niet omheen: "Opvallend is dat sun.misc.BASE64Encoder en sun.misc.BASE64Decoder verwijderd zijn. Gebruik in plaats daarvan de ondersteunde java.util.Base64-class, die in JDK 8 is toegevoegd". Voer je jdeps uit op code die de oude classes nog aanroept, dan markeert het tool de afhankelijkheid als "JDK removed internal API", en dat komt zo dicht bij een verkeerskegel als de JDK het krijgt. De DatatypeConverter van JAXB had een langere maar gelijksoortig leven, gedeprecateerd met de Java EE-modules in het Java 9-era en in Java 11 door JEP 320, "Remove the Java EE and CORBA Modules", helemaal verwijderd. Beide migraties zijn mechanisch: de oude printBase64Binary- en BASE64Encoder().encode-aanroepen mappen één-op-één naar getEncoder().encodeToString, afgezien van de omwikkelverschillen, en zodra de code op java.util.Base64 zit, draait hij op elke JDK van 8 tot 26 zonder verdere gedachten.
Beveiliging en snelheid
De beveiligingssectie is kort, want het werk van de encoder kan niet mislukken op data, maar hij is niet leeg. Base64 is geen versleuteling, en de standaard zegt het in zoveel woorden: Base-codering "verbergt visueel anders makkelijk herkenbare informatie, zoals wachtwoorden, maar biedt geen enkele computationele vertrouwelijkheid", en dezelfde sectie meldt dat dit "bekend staat veiligheidsincidenten te veroorzaken". De praktische gevolgtrekkingen voor de encoderkant: codeer een secret niet om het veilig te maken (het is nu minder veilig, want het past in meer kanalen); als de waarde geheim is, versleutel eerst en codeer het ciphertext; en houd de vervormbaarheid-tweeling in gedachten, waar een ontvanger een geldige spelling door een andere kan zetten (anders padding, afval in de overtollige bits) zonder de gedecodeerde data te veranderen. Een deterministische encoder helpt hier: java.util.Base64 produceert exact één output voor exact één invoer, dus als je eigen systeem een waarde zowel schrijft als leest, is de spelling stabiel, en zijn het de externe waarden op de vertrouwensgrens die canonieke-vormcontrole nodig hebben.
Over snelheid heeft de encoderkant hetzelfde verhaal als de decoderkant: op een moderne JVM is de ingebouwde implementatie snel genoeg dat Base64 bijna nooit de bottleneck is, en hij is het referentiepunt van de benchmark. Dezelfde benchmark van 2025 van gRPC-java die in de zuster-gids genoemd wordt (issue 11857, JMH op JDK 17 en 21) zette de JDK-encoder op ruwweg 2,5 tot 3,8 keer de doorvoer van die van Guava, met het grootste gat op x86. Twee praktische notities: voor hete paden, deel één encoder-instantie (de factory geeft al dezelfde gedeelde terug) en kies voor encode(byte[], byte[]) in een vooraf gedimensioneerde array om de allocatie over te slaan; voor enorme data is de streamingsectie het geheugenverhaal, en de kost van omwikkeling is ruis naast de schijf. De enige echte prestatietaks in Base64 is de grootte zelf, en geen enkele implementatie, deze meegezien, kan die omlaag onderhandelen.
De val-controlelijst
Alle vallen op één plek verzameld, allemaal Java-specifiek:
- De ontbrekende tekenset.
text.getBytes()zonder expliciete tekenset gebruikt het platformstandaard: per toeval goed op JDK 18+, op alles ouder fout, en in principe overal fout. GeefStandardCharsets.UTF_8door en schrijf de tekenset in de spec. - De gepadde JWT.
getUrlEncoder()padt standaard, en tokens willen geen padding. DewithoutPadding()-aanroep hoort bij het recept, niet bij de optionele extra's; een token met afsluitende=is een token dat sommige validators afwijzen en sommige verpesten. - De omwikkelde output. De MIME-encoder wikkelt om bij 76 met CRLF en voegt geen afsluitende regeleinde toe. Verwacht de consumer een afsluitende regeleinde, voeg hem toe; verwacht de consumer helemaal geen regeleindes, gebruik dan niet de MIME-encoder.
- De dubbele encode. Een waarde coderen die al Base64 is, produceert een perfect geldige, perfect nutteloze tekenreeks. De klassieke oorzaak: een veld arriveert vooraf gecodeerd vanuit een API en je code codeert het "helpvol" nog een keer. Check vóór je codeert.
- Plustekens in URLs. Standaard Base64-output bevat
+, en dat is in een querystring een spatie, vóór de server het überhaupt ziet. Moet een waarde in het standaardalfabet in een URL reizen, percent-encodeer het, of genereer het vanaf het begin in het URL-veilige alfabet. - De 33-procenten rekening. Waarden die in de ruwe kolom passen, passen niet in de gecodeerde. Dimensioneer opslag, berichtenvelden en headers vanaf
4 * ceil(n / 3), en onthoud dat omwikkelde MIME-output daar nog een paar procent bovenop is. - De ongesloten stream. De omwikkelde output stream houdt overgebleven bytes vast tot sluiten. Het bestand lezen vóór het sluiten geeft een afgesneden codering. Try-with-resources, elke keer.
- Secrets in volle openbaarheid. Base64 is pakkband, geen slot. Gecodeerde inloggegevens in een configbestand, een log of een omgevingsvariabele zijn leesbare inloggegevens. Versleutel eerst, of doe het niet.
- De Android-muur. Op Android bestaat
java.util.Base64pas vanaf API level 26; daaronder is de framework-classandroid.util.Base64met zijn eigen vlagconstanten (NO_PADDING,URL_SAFE,NO_WRAP). Eén ervan hardcoden zonder check breekt op precies die apparaten die je nooit getest hebt. - De regellengte-excentriciteit.
getMimeEncoder(77, ...)wikkelt stilletjes om bij 76, want de lengte wordt afgerond naar een veelvoud van vier, en om 3 of minder vragen schakelt omwikkeling helemaal uit. Vereist je format een oneven regellengte, dan is de MIME-draaiknop niet het gereedschap.
Van sun.misc naar de standaardbibliotheek
Het Java-verhaal is een kort verhaal met een duidelijk voor en na. Vóór 2014 kregen je, als je Base64 nodig had binnen de JDK, het interne paar sun.misc.BASE64Encoder en sun.misc.BASE64Decoder, vanaf dag één niet-ondersteund, met hun eigen 76-tekens-omwikkelgewoontes, of je greep in XML-code naar javax.xml.bind.DatatypeConverter, of je voegde Apache Commons Codec of Guava toe aan de build, en zo eindigden heel wat enterprise-codebases met drie Base64-implementaties en geen idee welke welke was. Op 18 maart 2014 bracht Java 8 java.util.Base64: één class, drie alfabetten, de regels van RFC 4648 en RFC 2045 correct geïmplementeerd, het factorypatroon, de padding- en omwikkel-draaiknoppen, en stream-adapters in beide richtingen. Het was de Base64 die de taal vanaf het begin had moeten hebben, en de javadoc zegt sindsdien Since: 1.8.
De schoonmaak kwam in twee golven. Java 9 (21 september 2017) verwijderde het sun.misc-paar als onderdeel van de module-systeem-schoonmaak, met de migratiegids die elke developer naar de JDK 8-class wees, en Java 11 verwijderde de JAXB-module en daarin de DatatypeConverter (JEP 320). Java 18 (22 maart 2022) bracht JEP 400, "UTF-8 by Default", die Base64 helemaal niet aanraakte maar de faalmodus veranderde van de luiere getBytes()-aanroepen die hem voeden: de tekenset van het platformstandaard werd UTF-8 op alle OS'en, zodat oude mojibake-patronen op nieuwe JVM's simpelweg stopten met reproduceren. Sinds 1.8 is van de publieke API geen enkele methode veranderd. Wat zich heeft verschoven, is de motor eronder: bugfixes en prestatiewerk, en daarom blijven community-benchmarks de standaardbibliotheekversie vinden die sneller is dan de legacy-bibliotheken die hij verving. Vandaag, op elke JDK van 8 tot en met 26, is het antwoord op "hoe codeer ik dit in Base64 in Java" één import en een factory-aanroep, en is dat al meer dan een decennium zo.
Enkele nerd-verwennerijen
Omdat een handboek moet eindigen met een glimlach, hier een aantal Java-specifieke feiten die gewoon leuk zijn:
- De javadoc zegt
Since: 1.8, en dat is twaalf jaar lang waar gebleven. Geen enkele methode toegevoegd, geen enkele verwijderd, geen enkel gedrag veranderd: een van de langst bevroren API-vlakken van de taal, en je gebruikt het zonder te denken. encodeToStringbouwt zijn resultaat-String met de ISO-8859-1-tekenset, volgens de javadoc. Het is in de praktijk een volledig overbodig detail, want Base64-output is zuiver ASCII en ziet er in Latin-1, UTF-8 en het grootste deel van de rest van het tekensetdierenpark hetzelfde uit, maar de javadoc vertelt het je toch, en dat is de JDK in al zijn majesteit.- De MIME-encoder voegt na de laatste onvolledige regel geen regeleinde toe. Andere tools, waaronder enkele heel beroemde e-mailbibliotheken, sluiten omwikkelde output af met een afsluitend CRLF. Is je diff tegen een referentieimplementatie precies twee tekens aan het einde, dan heb je deze excentriciteit gevonden.
- Vraag
getMimeEncoderom regels van 77 tekens en je krijgt 76: de regellengte wordt stilletjes afgerond naar het dichtstbijzijnde veelvoud van vier, want een omwikkeling die een groep van vier tekens deelt, zou rommel produceren. De API weigert een kapotte regel te bouwen in plaats van je toestemming te vragen. Base64.getEncoder() == Base64.getEncoder()is true. De factorymethodes geven bij elke aanroep dezelfde gedeelde instantie terug, dus de "haal een nieuwe" API is een kostuum voor een singleton, en de thread-safety-belofte is niets anders dan een beschrijving van wat de JVM al doet.- Op Android blootstelt de tweeling-API
android.util.Base64dezelfde beslissingen als vlaggen:NO_PADDING,URL_SAFE,NO_WRAP. Twee API's, één besluitentabel, en dat is een stil getuigenis van hoe vastgelegd het Base64-ontwerp tegenwoordig is. - Section 5 van RFC 4648 is waar de naam "base64url" wordt geboren: de spec zegt dat de URL-veilige codering "base64url genoemd kan worden" en waarschuwt dat deze "niet als hetzelfde als de base64-codering beschouwd moet worden". De oorsprong is in een voetnoot verwezen naar een posting uit 2001 op een P2P-hackers mailinglist, dus de naam in elke URL die je plakt, heeft een mailinglist-geboorte.
- Codeer het woord
base64en je krijgtYmFzZTY0, zonder padding, want zes is een veelvoud van drie. Een format dat zichzelf beschrijft is het technische equivalent van een spiegel die in Morse spreekt, en dit is de eigen reflectie van die spiegel. - Voer
jdeps -jdkinternalsuit op code van vóór Java 8 en kijk hoe hetsun.misc.BASE64Encodermarkeert als "JDK removed internal API". Het voorbeeld van het tool in de officiële migratiegids is een Base64-class, en dat is de JDK die naar je imports wijst en zegt "hierover hebben we het gehad". - De factor 1,37. Elke omwikkelde MIME-payload kost ongeveer 1,37 keer zijn oorspronkelijke grootte (4/3 voor het alfabet, 78/76 voor het CRLF-ritme), een breuk zo stabiel dat de oude e-mailwiskunde hem nog altijd citeert: de tol die de mailinfrastructuur van de jaren 90 op elke bijlage heft, is exact de rekening die
getMimeEncoder()vandaag in rekening brengt.
De andere kant op
Daarmee is de encoderkant van het verhaal verteld, en dat is de rustigere van de twee: het werk faalt nooit op de data, de vallen gaan over jouw beslissingen (tekenset, padding, omwikkeling, dialect) in plaats van over de verrassingen van andere mensen, en de hele API past in één import. De andere richting is waar Base64 stopt met handig zijn en begint met adversair, want decoderen is waar je de paddingkeuzes van andere mensen tegenkomt, hun regeleindes, hun tekensets en hun pantsering, met een IllegalArgumentException tussen jou en de waarheid. Base64 decoderen in Java, gelinkt vanaf deze pagina, behandelt de decoder in dezelfde diepte: de drie decoderpersoonlijkheden, de exacte foutmeldingen, de paddingregels, base64url en JWTs, MIME en PEM, en de Java-specifieke valkuilen op één plek verzameld. Lees de twee als een paar en het hele onderwerp is aan jou.
Laatst bijgewerkt: 2026-10-06
Gerelateerd artikel: Base64-decodering in Java: een complete gids