Devi lavorare con il formato Base64? Allora questo sito è perfetto per te! Usa il nostro praticissimo strumento online per codificare o decodificare i tuoi dati.

Codifica Base64 in Java: una guida completa

Ecco la situazione: hai dei byte. Un file, una password, un certificato, un saluto di 13 byte, un upload di 200 megabyte. E li devi mettere dentro qualcosa che capisce solo testo: un campo JSON, un header HTTP, una colonna di database, un URL, un file di configurazione. Quello è l'intero lavoro del Base64, e questa guida è il manuale Java per farlo bene. Brevissimo orientamento, perché la home page passeggia per il formato passo per passo: Base64 riscrive ogni tre byte di dati come quattro caratteri da un alfabeto di 64 lettere, con uno o due pad = appiccicati quando l'ultimo pezzo è corto. Il prezzo del viaggio è la dimensione: ogni tre byte diventano quattro caratteri, quindi l'output codificato atterra circa il 33 percento più grande dell'input, più un po' di extra se ci sono a capo.

La notizia principale, ed è una buona notizia. Dal 18 marzo 2014 ogni JDK include nella libreria standard una cassetta degli attrezzi Base64 completa: java.util.Base64. Nessun download, nessuna coordinata Maven, nessuna libreria nativa. Un import, tre personalità di encoder, e lo stesso comportamento dal Java 8 di allora al Java 26 di oggi. Tutto in questo articolo si regge su quella singola classe, e non lancia mai eccezioni sui dati stessi: il lavoro dell'encoder non può fallire su input non valido, perché ogni byte possibile è codificabile.

Un confine onesto prima di iniziare: questa è la parte encoder della storia. Imparerai la decisione da stringa a byte che determina davvero la correttezza, le perille del padding e dell'avvolgimento, base64url e la sua modalità senza padding per i token, e i casi d'uso dove gli sviluppatori Java incontrano l'output codificato più spesso. La decodifica, dove vive gran parte del dolore vero, ha la sua guida ed è collegata in fondo a questa.

Un import, zero download

Installare Base64 in Java è la risposta in una riga che dai alla lavagna: "È nel JDK." La classe java.util.Base64 fa parte del modulo java.base dal 1.8, e il suo javadoc dice ancora Since: 1.8 dopo dodici anni. L'unica cosa che installi è un JDK: qualsiasi Java 8 o più recente di qualsiasi produttore (Oracle, Eclipse Temurin, Amazon Corretto, Zulu) va bene, e su una macchina basata su Debian è un solo comando:

sudo apt install openjdk-17-jdk-headless

L'API è una factory: non costruisci mai un encoder; lo chiedi alla classe. Il lato encoder ha quattro porte, tutte restituiscono istanze della classe annidata Base64.Encoder:

Metodo factory Alfabeto Forma dell'output
getEncoder() A-Z a-z 0-9 + / Con pad, nessun a capo
getUrlEncoder() A-Z a-z 0-9 - _ Con pad, nessun a capo
getMimeEncoder() A-Z a-z 0-9 + / Con pad, righe da 76 caratteri, CRLF
getMimeEncoder(int, byte[]) A-Z a-z 0-9 + / Con pad, la tua lunghezza di riga, il tuo separatore

Tre proprietà vale la pena conoscerle subito. Le istanze sono thread-safe, e la factory restituisce la stessa istanza condivisa a ogni chiamata, quindi Base64.getEncoder() == Base64.getEncoder() è vero; costruiscine uno in un campo statico e condividilo dappertutto. Gli encoder non lanciano mai eccezioni sui dati: ogni valore di byte ha una codifica, quindi non c'è uno stato di "input non valido" da gestire, e le uniche eccezioni che incontrerai riguardano la configurazione sbagliata (un separatore di riga non valido) o un array di destinazione troppo piccolo. E ogni encoder di questa lista aggiunge il padding di default; la manopola che lo spegne, withoutPadding(), appare nella sezione base64url, perché è lì che ti servirà.

Continuerai a incontrare librerie più vecchie nei codebase, quindi una mappa rapida del panorama. Apache Commons Codec (attualmente 1.22.1) offre la sua org.apache.commons.codec.binary.Base64 dal 1.0, con un'API Builder che espone come perille la politica rigoroso-o-tollerante, la lunghezza delle righe e il separatore; è lo strumento giusto solo se devi supportare JVM pre-Java-8. Guava offre com.google.common.io.BaseEncoding, un veterano dalle capacità simili, ancora comune negli stack big data. Per tutto ciò che gira su una JVM moderna, java.util.Base64 è la scelta di default: zero dipendenze, e i benchmark della comunità continuano a trovarlo il più veloce del gruppo (ne parliamo nella sezione su sicurezza e velocità).

La tua prima codifica

Novanta percento della vita di chi codifica sta in tre righe. Ecco l'intera cerimonia, usando l'esempio più piccolo che l'articolo Wikipedia sul Base64 usa per spiegare l'alfabeto:

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 stringa TWFu è l'esempio che l'articolo Wikipedia sul Base64 usa per spiegare l'alfabeto, quindi se il tuo encoder trasforma "Man" in quello, la macchina è onesta. Ma guarda la prima riga di quell'esempio, perché è la riga dove la codifica avviene davvero in Java. Il metodo encodeToString(String) non esiste apposta. Una String Java è una sequenza di unità di codice UTF-16, non byte, e il Base64 è un formato di byte, quindi l'API ti fa decidere la questione dei byte da te: "Man".getBytes(StandardCharsets.UTF_8). Quella chiamata, con un charset esplicito, è il posto dove "café" resta corretto per i prossimi cent'anni, ed è l'abitudine più importante di tutto questo articolo. La sezione seguente è dedicata a questo, perché l'alternativa è il classico bug del mojibake.

Due note sulla seconda riga. encodeToString() restituisce una String costruita dai byte codificati; il javadoc spiega che costruisce il risultato usando il charset ISO-8859-1, il che in pratica non è un problema perché ogni carattere di output Base64 è ASCII puro e si presenta identico in Latin-1, UTF-8 e nella maggior parte del resto dello zoo dei charset. E se preferisci gestire tu il buffer di output, encode(byte[]) restituisce un byte[] fresco, e encode(byte[] src, byte[] dst) scrive in una destinazione che fornisci tu, restituendo il conteggio (e lanciando IllegalArgumentException: Output byte array is too small for encoding all input bytes se la destinazione è corta, senza scrivere un singolo byte).

La decisione del charset

Diamo concretezza al passo da stringa a byte con il caso classico. La parola "café" è una parola, ma in byte dipende interamente dal charset che hai scelto:

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'accento è due byte in UTF-8, uno in Latin-1
    System.out.println(Base64.getEncoder().encodeToString(utf8));
    // Y2Fmw6k=
    System.out.println(Base64.getEncoder().encodeToString(latin1));
    // Y2Fm6Q==
  }
}

Due stringhe Base64 diverse per una parola, e entrambe sono "corrette" a patto che chi legge sappia quale charset usare. L'intera lezione sta in una riga: l'encoder è fedele ai byte che gli dai, e sei tu a rispondere dei byte. In pratica significa: accordati su UTF-8 con il tuo interlocutore, passa StandardCharsets.UTF_8 esplicitamente, e scrivi il charset nella specifica, nello schema o nel messaggio di commit, perché nessuno dal lato ricevente può indovinarlo dal solo Base64. Il gemello lato decodifica di questo bug è l'argomento della guida sorella.

Una nota sulle versioni, perché cambia il modo di fallimento del codice pigro. Il new String(bytes) senza argomenti e il String.getBytes() senza charset usano il charset di default della piattaforma, che storicamente era Cp1252 su Windows e qualcosa legato alla locale su Linux. Dal JDK 18 (JEP 400, "UTF-8 di default") il default è UTF-8 su ogni piattaforma, quindi su una JVM moderna la forma pigra casca nel giusto. Questo non la rende sicura: il tuo codice sopravviverà al JDK per cui è stato scritto, e chi lo erediterà non dovrebbe dover sapere qual è il default. Scrivi il charset.

Un dettaglio di design correlato: non c'è nessun overload encode(String) da nessuna parte nell'API, ed è deliberato. Ogni altro passo del pipeline (array, buffer, stream) prende byte, e un metodo che accettasse String dovrebbe scegliere un charset per te, ed è esattamente la decisione che il JDK si rifiuta di prendere. L'unico metodo di tipo String che esiste, encodeToString, è sul lato output, dove la questione del charset non esiste: l'output Base64 è ASCII puro. Tutta la forma dell'API è un piccolo argomento a favore di "decidi i tuoi byte apposta".

Padding, avvolgimento e la manopola MIME

Gli encoder Java prendono due decisioni di formattazione per te di default, e entrambe valgono la pena di essere comprese perché entrambe sono perille che puoi girare. La prima è il padding: ogni encoder aggiunge i caratteri = che rendono l'output un multiplo di quattro, come chiede il RFC 4648: le implementazioni DEVONO includere caratteri di padding adeguati alla fine dei dati codificati, a meno che la specifica di riferimento non disponga diversamente. La seconda è l'avvolgimento delle righe: solo l'encoder MIME avvolge, a 76 caratteri con carriage return e line feed, e non aggiunge un separatore di riga dopo l'ultima riga parziale, un dettaglio che il javadoc cita esplicitamente e che altri strumenti sbagliano:

Encoder Aggiunge il padding Avvolge le righe Separatore di riga
getEncoder() sì no n/d
getUrlEncoder() sì no n/d
getMimeEncoder() sì sì, 76 caratteri CRLF
getMimeEncoder(64, "\n") sì sì, 64 caratteri LF

La manopola MIME è la parte più utile dell'API per chi eredita i formati degli altri. Il costruttore standard è getMimeEncoder() (76, CRLF, direttamente dal RFC 2045); la versione a due argomenti, getMimeEncoder(int lineLength, byte[] lineSeparator), ti fa riprodurre altre convenzioni. Le due stranezze da conoscere: la lunghezza di riga viene "arrotondata al multiplo di 4 inferiore", quindi chiedere 77 ti dà silenziosamente 76, e un valore arrotondato non positivo ti dà nessun avvolgimento; e il separatore non deve contenere nessun carattere dell'alfabeto Base64, altrimenti il costruttore lancia un IllegalArgumentException sul posto, perché un separatore che potrebbe essere confuso con i dati è un bug in attesa di accadere. Ecco la manopola in azione, standard MIME e in stile 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));
    // righe da 76 caratteri, CRLF in mezzo
    System.out.println(pem.encodeToString(data));
    // righe da 64 caratteri, solo LF in mezzo
  }
}

Due note pratiche. Se il tuo consumatore si aspetta che una stringa avvolta finisca con un a capo (alcuni strumenti di posta lo fanno), aggiungilo tu dopo la codifica: il JDK si ferma deliberatamente dopo l'ultima riga parziale. E se stai producendo dati che vivranno in un URL o in un token, l'avvolgimento è la manopola sbagliata in tutta la sua interezza; quei consumatori vogliono una riga lunga e di solito senza padding, ed è la sezione successiva.

base64url e la manopola senza padding

Il Base64 standard chiude il suo alfabeto con + e /, e sono esattamente i due caratteri che non si comportano bene negli URL: un + in una query string è già uno spazio prima che il server lo analizzi, una / è un separatore di percorso, e un = appeso vuole la codifica percent e diventa un mostro di tre caratteri. La sezione 5 del RFC 4648 disegna la correzione: l'alfabeto sicuro per URL e nomi di file, dove + diventa -, / diventa _, e il padding di coda = viene di solito buttato via quando la lunghezza è nota implicitamente. Il RFC è inflessibile sul nome: questa codifica "non dovrebbe essere considerata la stessa della codifica base64", e il nome che sentirai è base64url. I JSON Web Token, i parametri state di OAuth, gli ID di sessione API e gli ID video di undici caratteri vivono tutti in questo dialetto.

Java ti dà l'alfabeto con getUrlEncoder(), ma ecco la manopola che beffa la gente: l'encoder URL-safe aggiunge il padding di default, e gli standard dei token non vogliono il padding. Il RFC 7515 è esplicito che le parti JWS usano base64url "con tutti i caratteri '=' di coda omessi ... e senza l'inclusione di a capo, spazi bianchi o altri caratteri aggiuntivi". Quindi la ricetta canonica JWT in Java è una catena di due metodi:

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
  }
}

La chiamata withoutPadding() restituisce una nuova istanza di encoder che si comporta identicamente tranne che per l'omissione dei pad di coda; l'originale resta intatto, e il javadoc lo dice con precisione. Il lato decoder accetta sia input con pad sia senza, quindi un valore che produci senza padding resterà leggibile da un decoder rigoroso, ed è per questo che senza padding è la scelta sicura per tutto ciò che attraversa un confine API. Ora, un grande avvertimento: le due parti sopra sono le metà non firmate di un JWT. Un token vero ha bisogno di una firma calcolata su "header.payload", e quella è criptografia, non codifica. In produzione, emetti e verifica i token con una libreria JOSE: JJWT (0.13.0) o nimbus-jose-jwt (10.9.1). L'artefatto API di JJWT, per esempio, è a una coordinata di distanza:

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-api</artifactId>
  <version>0.13.0</version>
</dependency>
<!-- aggiungi jjwt-impl e jjwt-jackson a tempo di esecuzione, come indicato nella documentazione del progetto -->

Gli ID di YouTube sono l'altra faccia di questa manopola: undici caratteri di base64url senza padding, un identificatore che deve sopravvivere a essere incollato ovunque un URL sia ammesso. Se il tuo sistema genera identificatori che viaggiano negli URL, la catena withoutPadding() sopra è la forma da copiare.

Codificare i file

Il lavoro sui file del quotidiano è lo specchio del preferito del decoder: leggi un file, codificalo, scrivi il testo fuori. Quattro righe con 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());
  }
}

Quel print finale è la bolletta del 33 percento, resa visibile. Un file da 1 MB diventa circa 1,33 MB di testo (4/3 dell'originale, più al massimo due caratteri di padding), e se lo hai avvolto in stile MIME, gli a capo aggiungono qualche punto percentuale in più: la vecchia matematica dell'era della posta, ancora vera, è 4/3 per 78/76, ovvero circa 1,37 volte l'originale per un payload MIME avvolto. Due conseguenze. Prima, dimensiona qualsiasi archivio o campo di messaggio dalla lunghezza codificata, non da quella grezza: una colonna VARCHAR(255) che ospita allegramente un valore grezzo di 192 byte rifiuterà la sua codifica di 256 caratteri. Secondo, la direzione di codifica è quella che peggiora la memoria, quindi per file grandi la versione ad array è lo strumento sbagliato e la sezione sullo streaming è quella giusta. Un piccolo giubilo per la gente dei file: perché i primi caratteri di output sono una funzione pura dei primi byte di input, ogni PNG codificato in Base64 comincia con iVBORw0K e ogni GIF codificata con R0lGOD; puoi riconoscere il tipo di file prima che un solo byte venga decodificato.

JSON, API e data URI

Due dei posti più comuni dove l'output codificato vive sul cavo.

Uno: binario dentro il JSON. Endpoint di upload file, API di contenuto, archivi di segreti e webhook incorporano il binario come testo Base64 dentro il JSON, perché i byte grezzi romperebbero l'escape della stringa JSON. Il lato encoder è una riga al confine, e l'unica decisione è quale dialetto chiede la specifica:

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 specifica chiede base64url, senza padding:
    String field = Base64.getUrlEncoder().withoutPadding().encodeToString(image);
    // Passa "field" alla tua libreria JSON come normale valore stringa.
    System.out.println(field.length());
  }
}

L'insidia non è codificare; è leggere la specifica. Alcune API vogliono Base64 standard con padding, altre vogliono base64url senza, e poche sono tolleranti con entrambe. Quando la specifica tace, la correzione più economica è guardare un valore di esempio dall'altra parte: un - o un _ in qualunque punto chiude la questione dell'alfabeto, e un = di coda chiude quella del padding. Scegliere il dialetto sbagliato di solito non fa crollare il lato opposto; di solito corrompe il file, che è il tipo di bug più lento da trovare.

Due: le data URI. La stringa data:image/png;base64,... che inlinea un'immagine in HTML o CSS è la data URI del RFC 2397: data:, un media type facoltativo, un flag ;base64 facoltativo, una virgola, poi i dati. Costruirne una è concatenazione di stringhe, e l'unica decisione è se il flag c'è (niente flag significa che il payload è testo con codifica percent, cosa che nessuno vuole per il binario):

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...
  }
}

Il consiglio dello stesso RFC si applica con interesse: le data URI sono per valori corti. Inlinearci un'icona da 50 KB è uno scambio normale (una richiesta in meno); inlinearci una foto da 5 MB è un bug di prestazioni vestito da comodità. Tieni il flag, tieni il media type onesto, e tieni i byte piccoli.

Costruire l'header di autenticazione Basic

L'header di autenticazione più vecchio del web è ancora il caso d'uso Base64 più semplice in Java, perché è esattamente una chiamata di codifica. Secondo il RFC 7617, una richiesta Basic invia Authorization: Basic seguito dalla codifica Base64 di username:password; l'esempio dello stesso RFC, QWxhZGRpbjpvcGVuIHNlc2FtZQ==, è "Aladdin:open sesame" in incognito. Sul lato client, costruire l'header sono due righe di Base64 più una chiamata HTTP moderna:

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());
  }
}

Tre cautele appartengono a questo header. Prima, il RFC è esplicito che Basic è codifica, non protezione: le credenziali sono leggibili da chiunque possa vedere i pacchetti, quindi questo header è forte solo quanto l'HTTPS che c'è sotto, ed è una cattiva idea su tutto tranne TLS. Secondo, il charset: il RFC si aspetta credenziali US-ASCII (UTF-8 per tutto il resto, e il parametro di autenticazione charset è solo un suggerimento), quindi scegli StandardCharsets.UTF_8 e resta coerente su entrambi i lati. Terzo, una nota sulle versioni: il client java.net.http è dal Java 11; su una JVM più vecchia lo stesso header va su una HttpURLConnection con una chiamata setRequestProperty, e la riga Base64 è identica in entrambi i casi. Sul lato server dello stesso header, analisi e decodifica sono l'esempio della guida sorella, con la divisione al primo due punti e il confronto a tempo costante. I due lati sono due chiamate della stessa API, ed è l'eleganza silenziosa di questa.

Valori in configurazioni, variabili d'ambiente e colonne

Il Base64 è un contenitore di testo, ed è per questo che si presenta in posti dove non te lo aspetti: un DSN di database con punti e virgola in un file di ambiente, una password con virgolette in un file properties, un certificato su più righe in una config map, un blob binario in una colonna TEXT perché lo schema è stato progettato prima che qualcuno considerasse i BLOB. Il lato codifica è una chiamata, e l'inquadramento onesto è quello che è: un trucco di sicurezza di formato, non un trucco di segretezza:

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);
  }
}

Due regole tengono onesto questo trucco. Prima, non immagazzinare mai un segreto come Base64 e chiamarlo cifrato: il Base64 non aggiunge entropia e non rimuove informazioni, nel momento in cui uno sviluppatore legge il file può decodificare il valore in una chiamata, e la sezione di sicurezza del RFC punta a esattamente questo fallimento, la gente che rivela credenziali incollando scambi di protocollo "codificati". Se il valore è segreto, cifralo prima, e solo dopo impacchettalo nel Base64 se il canale chiede testo. Secondo, tieni conto della dimensione: il valore immagazzinato è circa un terzo più grande dell'originale, e una colonna o un campo che ospitavano il valore grezzo non ospiteranno quello codificato. E quando il valore torna, decodificalo al confine e tienilo come byte (per il binario) o stringa con charset esplicito (per il testo); quella direzione è il territorio della guida sorella.

Lo streaming per i dati grandi

La codifica è la direzione che peggiora la memoria, quindi la storia dei file grandi qui riguarda il tenere l'insieme di lavoro piccolo. La versione ad array dell'esempio della sezione sui file va bene fino al punto in cui il file smette di stare comodo in memoria; oltre, l'adattatore a stream è la mossa. wrap(OutputStream) restituisce uno stream di output che codifica mentre scrivi, quindi un file da giga byte non viene mai tenuto come un singolo array di byte:

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();
  }
}

C'è un comportamento su questo stream che merita un risalto, perché il javadoc stesso ci punta: lo stream avvolto può tenere internamente alcuni byte residui, e la pratica raccomandata è "chiudere prontamente lo stream di output restituito dopo l'uso, durante il quale scaricherà tutti i possibili byte residui nello stream di output sottostante". Se smetti di scrivere e leggi il file di output prima di chiudere, la coda dei tuoi dati è ancora seduta nell'encoder, e il file sembra troncato. Ecco perché l'esempio chiude packed prima che qualsiasi altra cosa tocchi il file, e in produzione metteresti entrambi gli stream in un blocco try-with-resources. Prendi l'abitudine: sullo stream di codifica, chiudere fa parte della codifica.

Incontro con la vecchia guardia

I codebase ereditati sono pieni di API Base64 che precedono java.util.Base64, e riconoscerle ti salva da misteri del tipo "perché questo avvolge il mio output". Le quattro che incontrerai davvero:

API Dove la incontrerai Cosa fare
sun.misc.BASE64Encoder / BASE64Decoder Codice pre-Java-8 Migra a java.util.Base64; rimosso nel Java 9
javax.xml.bind.DatatypeConverter Codice dell'era XML, vecchi web service Rimosso nel Java 11 (JEP 320); migra
org.apache.commons.codec.binary.Base64 Codice che deve girare su JVM pre-8 Tienilo per il supporto pre-8; altrimenti la classe del JDK è la scelta di default
com.google.common.io.BaseEncoding Stack pieni di Guava e big data Va bene così; la classe del JDK non ha dipendenze

La coppia sun.misc è quella con il dramma. Era un'API interna, non supportata (del tipo che compila senza problemi sul JDK del momento e svanisce senza un avviso di deprecazione), e il suo output aveva abitudini proprie, come avvolgere il testo codificato sulle righe, ed è da lì che viene una quantità sorprendente di bug "il mio Base64 ha a capo dentro". Quando Java 9 è uscito nel settembre 2017, la pulizia del sistema a moduli l'ha rimossa, e la guida di migrazione ufficiale non usa giri di parole: "In particolare, sun.misc.BASE64Encoder e sun.misc.BASE64Decoder sono stati rimossi. Invece, usa la classe supportata java.util.Base64, aggiunta nel JDK 8". Se esegui jdeps su codice che riferenzia ancora le classi vecchie, lo strumento marca la dipendenza come "JDK removed internal API", che è il più vicino a un cono stradale a cui il JDK arrivi. Il DatatypeConverter di JAXB ha avuto una vita più lunga ma simile, deprecato con i moduli Java EE nell'era di Java 9 e rimosso del tutto nel Java 11 dal JEP 320, "Remove the Java EE and CORBA Modules". Entrambe le migrazioni sono meccaniche: le vecchie chiamate printBase64Binary e BASE64Encoder().encode si mappano uno a uno su getEncoder().encodeToString, a parte le differenze di avvolgimento, e una volta che il codice è su java.util.Base64 gira su ogni JDK da 8 a 26 senza ulteriori pensieri.

Sicurezza e velocità

La sezione sulla sicurezza è corta, perché il lavoro dell'encoder non può fallire sui dati, ma non è vuota. Il Base64 non è cifratura, e lo standard lo dice con parole testuali: la codifica Base "nasconde visivamente informazioni altrimenti facilmente riconoscibili, come le password, ma non fornisce alcuna riservatezza computazionale", e la stessa sezione nota che questo "è noto che ha causato incidenti di sicurezza". I corollari pratici per il lato encoder: non codificare un segreto per renderlo sicuro (ora è meno sicuro, perché sta in più canali); se il valore è segreto, cifra prima e codifica il testo cifrato; e tieni a mente il gemello della malleabilità, dove un ricevente può scambiare una notazione valida con un'altra (padding diverso, rifiuto nei bit di scarto) senza cambiare i dati decodificati. Un encoder deterministico aiuta qui: java.util.Base64 produce esattamente un output per esattamente un input, quindi se il tuo stesso sistema scrive e legge un valore, la notazione è stabile, e sono i valori esterni al confine di fiducia che hanno bisogno del controllo della forma canonica.

Sulla velocità, il lato encoder ha la stessa storia del lato decoder: su una JVM moderna l'implementazione integrata è abbastanza veloce che il Base64 è quasi mai il collo di bottiglia, ed è il punto di riferimento del benchmark. Lo stesso benchmark gRPC-java del 2025 menzionato nella guida sorella (issue 11857, JMH su JDK 17 e 21) ha messo l'encoder del JDK a circa 2.5 a 3.8 volte la portata di Guava, con la differenza maggiore su x86. Due note pratiche: per i percorsi caldi, condividi un'istanza di encoder (la factory restituisce già la stessa istanza condivisa) e preferisci encode(byte[], byte[]) in un array già dimensionato per saltare l'allocazione; per dati enormi, la sezione sullo streaming è la storia della memoria, e il costo dell'avvolgimento è rumore rispetto al disco. L'unica vera tassa di prestazioni nel Base64 è la dimensione stessa, e nessuna implementazione, inclusa questa, può trattarla per ridurla.

La lista di controllo delle trappole

Ogni trappola raccolta in un posto solo, tutte specifiche di Java:

  • Il charset mancante. text.getBytes() senza charset esplicito usa il default della piattaforma: giusto per caso sul JDK 18+, sbagliato su tutto ciò che è più vecchio, e sbagliato in principio ovunque. Passa StandardCharsets.UTF_8 e scrivi il charset nella specifica.
  • Il JWT con padding. getUrlEncoder() aggiunge il padding di default, e i token non vogliono il padding. La chiamata withoutPadding() fa parte della ricetta, non è un extra facoltativo; un token con = di coda è un token che alcuni validatori rifiuteranno e altri rovineranno.
  • L'output avvolto. L'encoder MIME avvolge a 76 con CRLF e non aggiunge un a capo finale. Se il consumatore si aspetta un a capo finale, aggiungilo; se il consumatore non se ne aspetta nessuno, non usare l'encoder MIME.
  • La doppia codifica. Codificare un valore che è già Base64 produce una stringa perfettamente valida, perfettamente inutile. La causa classica: un campo arriva pre-codificato da un'API e il tuo codice "gentilmente" lo codifica di nuovo. Controlla prima di codificare.
  • Segni più negli URL. L'output Base64 standard contiene +, che in una query string è uno spazio prima che il server lo veda. Se un valore a alfabeto standard deve viaggiare in un URL, applica la codifica percent, o generalo nell'alfabeto URL-safe fin dall'inizio.
  • La bolletta del 33 percento. Un valore che sta nella colonna grezza non starà in quella codificata. Dimensiona archivi, campi di messaggio e header da 4 * ceil(n / 3), e ricorda che l'output MIME avvolto è qualche punto percentuale in più.
  • Lo stream non chiuso. Lo stream di output avvolto tiene i byte residui fino alla chiusura. Leggere il file prima della chiusura ti dà una codifica troncata. Try-with-resources, ogni volta.
  • Segreti alla luce del sole. Il Base64 è nastro imballo, non una serratura. Credenziali codificate in un file di configurazione, in un log o in una variabile d'ambiente sono credenziali leggibili. Cifra prima, o non farlo affatto.
  • Il muro di Android. Su Android, java.util.Base64 esiste solo dal livello API 26; sotto, la classe del framework è android.util.Base64 con le sue costanti flag (NO_PADDING, URL_SAFE e NO_WRAP). Fissarne una senza un controllo si rompe esattamente sui dispositivi che non hai mai testati.
  • La stranezza della lunghezza di riga. getMimeEncoder(77, ...) avvolge silenziosamente a 76, perché la lunghezza è arrotondata al multiplo di quattro inferiore, e chiedere 3 o meno disattiva l'avvolgimento del tutto. Se il tuo formato esige una lunghezza di riga dispari, la manopola MIME non è lo strumento.

Da sun.misc alla libreria standard

La storia di Java è una storia corta con un prima e un dopo chiari. Prima del 2014, se ti serviva Base64 dentro il JDK ottenevi la coppia interna sun.misc.BASE64Encoder e sun.misc.BASE64Decoder, non supportata dal primo giorno, con le sue abitudini di avvolgimento a 76 caratteri, oppure raggiungevi javax.xml.bind.DatatypeConverter nel codice XML, oppure aggiungevi Apache Commons Codec o Guava alla build, ed è così che un sacco di codebase enterprise sono finite con tre implementazioni Base64 e nessuna idea di quale fosse quale. Il 18 marzo 2014, Java 8 ha rilasciato java.util.Base64: una classe, tre alfabeti, le regole RFC 4648 e RFC 2045 implementate per bene, il pattern della factory, le perille del padding e dell'avvolgimento, e adattatori a stream in entrambe le direzioni. Era il Base64 che il linguaggio avrebbe dovuto avere fin dall'inizio, e il javadoc dice Since: 1.8 da allora.

La pulizia è arrivata in due ondate. Java 9 (21 settembre 2017) ha rimosso la coppia sun.misc come parte della pulizia del sistema a moduli, con la guida di migrazione che punta ogni sviluppatore alla classe del JDK 8, e Java 11 ha rimosso il modulo JAXB e il suo DatatypeConverter insieme (JEP 320). Java 18 (22 marzo 2022) ha portato JEP 400, "UTF-8 di default", che non ha toccato il Base64 per nulla ma ha cambiato il modo di fallimento delle chiamate pigre getBytes() che lo alimentano: il charset di default della piattaforma è diventato UTF-8 su ogni sistema operativo, quindi i vecchi schemi di mojibake hanno semplicemente smesso di riprodursi su JVM nuove. Dal 1.8 l'API pubblica non ha cambiato un singolo metodo. A muoversi è stato il motore sotto: correzioni di bug e lavoro sulle prestazioni, ed è per questo che i benchmark della comunità continuano a trovare la versione della libreria standard più veloce delle librerie legacy che ha sostituito. Oggi, su ogni JDK da 8 a 26, la risposta a "come faccio il Base64 di questo in Java" è un import e una chiamata factory, ed è così da oltre un decennio.

Alcuni divertimenti da nerd

Perché un manuale dovrebbe finire con un sorriso, ecco alcuni fatti specifici di Java che sono semplicemente divertenti:

  • Il javadoc dice Since: 1.8, ed è vero da dodici anni. Nessun metodo aggiunto, nessun metodo rimosso, nessun comportamento cambiato: una delle superfici API più a lungo congelate del linguaggio, e la usi senza pensarci.
  • encodeToString costruisce la sua String di risultato con il charset ISO-8859-1, secondo il javadoc. In pratica è un dettaglio completamente superfluo, perché l'output Base64 è ASCII puro e si presenta lo stesso in Latin-1, UTF-8 e nella maggior parte del resto dello zoo dei charset, ma il javadoc te lo dice lo stesso, ed è il JDK che fa il JDK.
  • L'encoder MIME non aggiunge un separatore di riga dopo l'ultima riga parziale. Altri strumenti, incluse alcune librerie di email molto famose, chiudono l'output avvolto con un CRLF finale. Se il tuo diff contro un'implementazione di riferimento è esattamente due caratteri alla fine, hai trovato questa stranezza.
  • Chiedi a getMimeEncoder righe da 77 caratteri e ti dà 76: la lunghezza di riga è arrotondata al multiplo di quattro inferiore, silenziosamente, perché un avvolgimento che spezza un gruppo di quattro caratteri produrrebbe spazzatura. L'API rifiuta di costruire una riga rotta piuttosto che chiederti il permesso.
  • Base64.getEncoder() == Base64.getEncoder() è vero. I metodi factory restituiscono la stessa istanza condivisa a ogni chiamata, quindi l'API "prendine una nuova" è un costume per un singleton, e la promessa di thread-safety è solo la descrizione di ciò che la JVM sta già facendo.
  • Su Android, l'API gemella android.util.Base64 espone le stesse decisioni come flag: NO_PADDING, URL_SAFE, NO_WRAP. Due API, una tabella di decisioni, ed è una testimonianza silenziosa di quanto il design del Base64 sia ormai stabilito.
  • La sezione 5 del RFC 4648 è il posto dove nasce il nome "base64url": la specifica dice che la codifica URL-safe "può essere chiamata base64url" e avverte che "non dovrebbe essere considerata la stessa della codifica base64". La sua origine è citata in nota a un post del 2001 su una mailing list di hacker P2P, quindi il nome in ogni URL che incolli ha un pedigree da mailing list.
  • Codifica la parola base64 e ottieni YmFzZTY0, senza padding, perché sei è un multiplo di tre. Un formato che si descrive da solo è l'equivalente tecnico di uno specchio che parla in Morse, e questo è il riflesso dello specchio stesso.
  • Esegui jdeps -jdkinternals su codice pre-Java-8 e guardalo marcare sun.misc.BASE64Encoder come "JDK removed internal API". L'esempio dello strumento nella guida di migrazione ufficiale è una classe Base64, ed è il JDK che punta ai tuoi import e dice "ne abbiamo già parlato".
  • Il fattore 1.37. Ogni payload MIME avvolto costa circa 1.37 volte le sue dimensioni originali (4/3 per l'alfabeto, 78/76 per il ritmo CRLF), una frazione così stabile che la vecchia matematica della posta la cita ancora: il pedaggio che l'infrastruttura di posta degli anni '90 prelevava su ogni allegato è esattamente la bolletta che getMimeEncoder() addebita oggi.

In direzione opposta

Questa è la parte encoder della storia, ed è la più calma delle due: il lavoro non fallisce mai sui dati, le trappole riguardano le tue decisioni (charset, padding, avvolgimento, dialetto) invece delle sorprese degli altri, e l'intera API sta in un import. L'altra direzione è dove il Base64 smette di essere comodo e diventa avversario, perché decodificare è il posto dove incontri le scelte di padding degli altri, i loro a capo, i loro charset e la loro armatura, con un IllegalArgumentException piantato tra te e la verità. La decodifica Base64 in Java, collegata da questa pagina, copre il decoder con la stessa profondità: le tre personalità di decoder, i messaggi d'errore esatti, le regole del padding, base64url e JWT, MIME e PEM, e le insidie specifiche di Java raccolte in un posto solo. Leggi le due come una coppia e l'intero argomento è tuo.

Ultimo aggiornamento: 2026-09-08

Articolo correlato: Decodifica Base64 in Java: una guida completa