Você tem que lidar com o formato Base64? Então esse site é perfeito para você! Use o nosso ferramenta online super útil para codificar ou decodificar os seus dados.

Codificação Base64 em Java: um guia completo

Aqui está a situação: você tem bytes. Um arquivo, uma senha, um certificado, uma saudação de 13 bytes, um upload de 200 megabytes. E precisa deles dentro de algo que só entende texto: um campo JSON, um header HTTP, uma coluna de banco de dados, uma URL, um arquivo de config. Esse é o trabalho inteiro do Base64, e este guia é o manual Java para fazer isso bem. Uma orientação rápida, porque a página inicial passeia pelo formato passo a passo: o Base64 reescreve cada três bytes de dados como quatro caracteres de um alfabeto de 64 letras, com um ou dois pads = colados quando o último bloco fica curto. O preço da viagem é o tamanho: cada três bytes viram quatro caracteres, então a saída codificada chega mais ou menos 33 por cento maior que a entrada, mais um pouquinho se houver quebras de linha.

A manchete principal, e é uma boa. Desde 18 de março de 2014, todo JDK entrega uma caixa de ferramentas completa de Base64 na biblioteca padrão: java.util.Base64. Sem download, sem coordenada Maven, sem biblioteca nativa. Um import, três personalidades de codificador, e o mesmo comportamento do Java 8 até o Java 26 de hoje. Tudo neste artigo é construído sobre aquela única classe, e ela nunca lança exceção nos dados em si: o trabalho do codificador não pode falhar em entrada inválida, porque todo byte possível é codificável.

Um limite honesto antes de começarmos: esta é a parte do codificador da história. Você vai aprender a decisão de string-para-bytes que realmente determina a correção, os controles de padding e quebra de linha, o base64url e o modo sem padding dele para tokens, e os casos de uso onde desenvolvedores Java encontram saída codificada com mais frequência. Decodificar, onde mora a maior parte da dor real, tem o próprio guia e é linkado no final deste.

Um import, zero downloads

Instalar Base64 no Java é a resposta de uma linha que você dá no quadro branco: "Está no JDK." A classe java.util.Base64 faz parte do módulo java.base desde o 1.8, e o javadoc dela ainda diz Since: 1.8 doze anos depois. A única coisa que você instala é um JDK: qualquer Java 8 ou mais novo de qualquer fornecedor (Oracle, Eclipse Temurin, Amazon Corretto, Zulu) funciona, e numa máquina baseada em Debian isso é um único comando:

sudo apt install openjdk-17-jdk-headless

A API é uma fábrica: você nunca constrói um codificador, pede um para a classe. O lado do codificador tem quatro portas, todas retornando instâncias da classe aninhada Base64.Encoder:

Método de fábrica Alfabeto Formato da saída
getEncoder() A-Z a-z 0-9 + / Com padding, sem quebras de linha
getUrlEncoder() A-Z a-z 0-9 - _ Com padding, sem quebras de linha
getMimeEncoder() A-Z a-z 0-9 + / Com padding, linhas de 76 caracteres, CRLF
getMimeEncoder(int, byte[]) A-Z a-z 0-9 + / Com padding, o seu comprimento de linha, o seu separador

Três propriedades valem a pena saber antes. As instâncias são thread-safe, e a fábrica retorna a mesma instância compartilhada em toda chamada, então Base64.getEncoder() == Base64.getEncoder() é verdadeiro; crie uma num campo estático e compartilhe em todo lugar. Os codificadores nunca lançam exceção nos dados: todo valor de byte tem uma codificação, então não existe estado de "entrada inválida" para tratar, e as únicas exceções que você vai encontrar são sobre configuração errada (um separador de linha ruim) ou um array de destino pequeno demais. E todo codificador desta lista adiciona padding por padrão; o controle que desliga isso, withoutPadding(), aparece na seção de base64url, porque é lá que você vai precisar dele.

Você ainda vai encontrar bibliotecas mais velhas em codebases, então um mapa rápido do cenário. Apache Commons Codec (atualmente 1.22.1) entrega o próprio org.apache.commons.codec.binary.Base64 desde o 1.0, com uma API de Builder que expõe a política estrito-ou-tolerante, o comprimento de linha e o separador como controles; é a ferramenta certa apenas se você precisa suportar JVMs anteriores ao Java 8. O Guava entrega com.google.common.io.BaseEncoding, um veterano de capacidade similar, ainda comum em stacks de big data. Para qualquer coisa num JVM moderno, java.util.Base64 é o padrão: zero dependências, e benchmarks da comunidade continuam encontrando ele o mais rápido do grupo (mais sobre isso na seção de segurança e velocidade).

Sua primeira codificação

Noventa por cento da vida de codificação cabe em três linhas. Aqui está a cerimônia inteira, usando o menor exemplo que o artigo do Wikipedia sobre Base64 usa para explicar o 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
  }
}

A string TWFu é o exemplo que o artigo do Wikipedia sobre Base64 usa para explicar o alfabeto, então se o seu codificador transforma "Man" nisso, a máquina é honesta. Mas olhe a primeira linha daquele exemplo, porque é nela que a codificação acontece de verdade no Java. Não existe o método encodeToString(String) de propósito. Uma String Java é uma sequência de unidades de código UTF-16, não bytes, e Base64 é um formato de bytes, então a API faz você decidir a questão dos bytes sozinho: "Man".getBytes(StandardCharsets.UTF_8). Aquela chamada, com um charset explícito, é onde "café" continua correto pelos próximos cem anos, e é o hábito mais importante de todo este artigo. A próxima seção é dedicada a isso, porque a alternativa é o bug clássico de mojibake.

Duas notas sobre a segunda linha. encodeToString() retorna uma String construída a partir dos bytes codificados; o javadoc explica que ele constrói o resultado usando o charset ISO-8859-1, o que na prática é irrelevante, porque todo caractere de saída Base64 é ASCII puro e parece idêntico em Latin-1, UTF-8 e na maior parte do resto do zoológico de charsets. E se você prefere ter o buffer de saída você mesmo, encode(byte[]) retorna um byte[] fresco, e encode(byte[] src, byte[] dst) escreve num destino que você fornece, retornando a contagem (e lançando IllegalArgumentException: Output byte array is too small for encoding all input bytes se o destino for curto, sem escrever um único byte).

A decisão de charset

Vamos tornar o passo de string-para-bytes concreto com o caso clássico. A palavra "café" é uma palavra, mas em bytes depende inteiramente do charset que você escolheu:

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: o acento é dois bytes em UTF-8, um em Latin-1
    System.out.println(Base64.getEncoder().encodeToString(utf8));
    // Y2Fmw6k=
    System.out.println(Base64.getEncoder().encodeToString(latin1));
    // Y2Fm6Q==
  }
}

Duas strings Base64 diferentes para uma palavra, e ambas estão "corretas" desde que o leitor seja avisado de qual charset usar. A lição inteira cabe em uma linha: o codificador é fiel aos bytes que você dá a ele, e você é responsável pelos bytes. Na prática isso significa: combine UTF-8 com o seu par, passe StandardCharsets.UTF_8 explicitamente, e escreva o charset na especificação, no schema ou na mensagem de commit, porque ninguém do lado receptor consegue adivinhar ele do Base64 sozinho. O gêmeo desse bug no lado do decodificador é o tema do guia irmão.

Uma nota de versão, porque ela muda o modo de falha de código preguiçoso. O new String(bytes) sem argumentos e o String.getBytes() sem charset usam o charset padrão da plataforma, que historicamente era Cp1252 no Windows e algo dependente do locale no Linux. Desde o JDK 18 (JEP 400, "UTF-8 by Default") o padrão é UTF-8 em toda plataforma, então num JVM moderno a forma preguiçosa acontece de estar certa. Isso não a torna segura: o seu código vai sobreviver ao JDK para o qual foi escrito, e a pessoa que herdá-lo não deveria precisar saber qual é o padrão. Escreva o charset.

Um detalhe de design relacionado: não existe overload encode(String) em lugar nenhum da API, e isso é deliberado. Todo outro passo do pipeline (arrays, buffers, streams) recebe bytes, e um método que aceitasse String teria que escolher um charset por você, que é exatamente a decisão que o JDK se recusa a tomar. O único método com tipo String que existe, encodeToString, está no lado da saída, onde a questão do charset não existe: a saída Base64 é ASCII puro. A forma inteira da API é um pequeno argumento para "decida os seus bytes de propósito".

Padding, quebra de linha e o controle do MIME

Os codificadores do Java tomam duas decisões de formato por você por padrão, e as duas valem a pena entender porque as duas são controles que você pode girar. A primeira é o padding: todo codificador adiciona os caracteres = que fazem a saída ser múltiplo de quatro, como o RFC 4648 pede: implementações DEVEM incluir caracteres de padding apropriados no final dos dados codificados, a menos que a especificação referenciada diga o contrário. A segunda é a quebra de linha: só o codificador MIME quebra, em 76 caracteres com carriage return e line feed, e ele não adiciona um separador de linha depois da última linha parcial, um detalhe que o javadoc aponta explicitamente e outras ferramentas erram:

Codificador Adiciona padding Quebra linhas Separador de linha
getEncoder() sim não n/a
getUrlEncoder() sim não n/a
getMimeEncoder() sim sim, 76 caracteres CRLF
getMimeEncoder(64, "\n") sim sim, 64 caracteres LF

O controle do MIME é a parte mais útil da API para quem herda formatos de outras pessoas. O construtor padrão é getMimeEncoder() (76, CRLF, direto do RFC 2045); a versão de dois argumentos, getMimeEncoder(int lineLength, byte[] lineSeparator), deixa você reproduzir outras convenções. Os dois truques para saber: o comprimento de linha é "arredondado para baixo para o múltiplo de 4 mais próximo", então pedir 77 silenciosamente te dá 76, e um valor arredondado que não for positivo te dá nenhuma quebra de linha; e o separador não pode conter nenhum caractere do alfabeto Base64, ou o construtor lança uma IllegalArgumentException na hora, porque um separador que pode ser confundido com dados é um bug esperando para acontecer. Aqui está o controle em ação, padrão MIME e com sabor 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));
    // linhas de 76 caracteres, CRLF entre elas
    System.out.println(pem.encodeToString(data));
    // linhas de 64 caracteres, LF puro entre elas
  }
}

Duas notas práticas. Se o seu consumidor espera que uma string quebrada termine com uma quebra de linha (alguma ferramenta de e-mail faz isso), adicione você mesmo depois do encode: o JDK para deliberadamente depois da última linha parcial. E se você está produzindo dados que vão morar numa URL ou num token, quebra de linha é o controle errado por completo; esses consumidores querem uma linha longa e geralmente sem padding, que é a próxima seção.

base64url e o controle sem padding

O Base64 padrão termina o alfabeto com + e /, e são exatamente os dois caracteres que não se comportam em URLs: um + numa query string já é um espaço antes do servidor fazer o parse, uma / é um separador de caminho, e um = pendurado quer percent-encoding para virar um monstro de três caracteres. A seção 5 do RFC 4648 desenha a correção: o alfabeto seguro para URLs e nomes de arquivo, onde + vira -, / vira _, e o padding = final é tipicamente descartado quando o comprimento é conhecido implicitamente. O RFC é insistente sobre o nome: esta codificação "não deve ser considerada a mesma que a codificação base64", e o nome que você vai ouvir é base64url. JSON Web Tokens, parâmetros de state do OAuth, IDs de sessão de API e IDs de vídeo de onze caracteres moram todos neste dialeto.

O Java te dá o alfabeto com getUrlEncoder(), mas aqui está o controle que pega as pessoas: o codificador URL-safe ainda adiciona padding por padrão, e os padrões de token não querem padding. O RFC 7515 é explícito de que as partes JWS usam base64url "com todos os caracteres finais '=' omitidos ... e sem a inclusão de qualquer quebra de linha, espaço em branco ou outros caracteres adicionais". Então a receita canônica de JWT no Java é uma cadeia de dois métodos:

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

A chamada withoutPadding() retorna uma nova instância de codificador que se comporta identicamente, exceto por omitir os pads finais; o original fica intacto, e o javadoc diz isso com precisão. O lado do decodificador aceita entrada com padding e sem, então um valor que você produzir sem padding ainda será legível por um decodificador estrito, e é por isso que sem padding é a escolha segura para qualquer coisa que cruze uma fronteira de API. Agora, um grande aviso: as duas partes acima são as metades sem assinatura de um JWT. Um token de verdade precisa de uma assinatura calculada sobre "header.payload", e isso é criptografia, não codificação. Para produção, emita e verifique tokens com uma biblioteca JOSE: JJWT (0.13.0) ou nimbus-jose-jwt (10.9.1). O artefato de API do JJWT, por exemplo, está a uma coordenada de distância:

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-api</artifactId>
  <version>0.13.0</version>
</dependency>
<!-- adicione jjwt-impl e jjwt-jackson em runtime, conforme a documentação do projeto -->

IDs do YouTube são a outra cara deste controle: onze caracteres de base64url sem padding, um identificador que precisa sobreviver a ser colado em qualquer lugar onde URL é permitida. Se o seu sistema gera identificadores que viajam em URLs, a cadeia withoutPadding() acima é a forma a copiar.

Codificando arquivos

O trabalho de arquivo do dia a dia é o espelho do favorito do decodificador: lê um arquivo, codifica, escreve o texto. Quatro linhas com 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());
  }
}

Aquele último println é a conta de 33 por cento, feita visível. Um arquivo de 1 MB vira mais ou menos 1,33 MB de texto (4/3 do original, mais no máximo dois caracteres de padding), e se você quebrou em estilo MIME, as quebras de linha adicionam mais uns tantos por cento: a matemática antiga da era do e-mail, ainda válida, é 4/3 vezes 78/76, ou mais ou menos 1,37 vezes o original para um payload MIME quebrado. Duas consequências. Primeira, dimensione qualquer armazenamento ou campo de mensagem pelo comprimento codificado, não pelo cru: uma coluna VARCHAR(255) que guarda feliz um valor cru de 192 bytes vai rejeitar a codificação dele de 256 caracteres. Segunda, a direção da codificação é a que piora a memória, então para arquivos grandes a versão de array é a ferramenta errada e a seção de streaming é a certa. Uma pequena alegria para a turma de arquivos: porque os primeiros caracteres de saída são uma função pura dos primeiros bytes de entrada, todo PNG codificado em Base64 começa com iVBORw0K e todo GIF codificado com R0lGOD; você pode reconhecer o tipo de arquivo antes de um único byte ser decodificado.

JSON, APIs e data URIs

Um dos lugares mais comuns onde a saída codificada mora na rede.

Um: binário dentro de JSON. Endpoints de upload de arquivo, APIs de conteúdo, cofres de segredos e webhooks embutem binário como texto Base64 dentro de JSON, porque bytes crus quebrariam o escaping de string do JSON. O lado do codificador é uma linha só na fronteira, e a única decisão é qual dialeto a especificação pede:

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"));
    // A especificação diz base64url, sem padding:
    String field = Base64.getUrlEncoder().withoutPadding().encodeToString(image);
    // Entregue "field" para a sua biblioteca JSON como um valor de string simples.
    System.out.println(field.length());
  }
}

A armadilha não é codificar, é ler a especificação. Algumas APIs querem Base64 padrão com padding, algumas querem base64url sem, e algumas poucas são tolerantes com ambos. Quando a especificação cala, o conserto mais barato é olhar um valor de exemplo do outro lado: um - ou _ em qualquer lugar resolve o alfabeto, e = finais resolvem o padding. Errar o dialeto geralmente não faz o outro lado quebrar, geralmente corrompe o arquivo, que é o tipo mais lento de bug para encontrar.

Dois: data URIs. A string data:image/png;base64,... que embute uma imagem no HTML ou CSS é o data URI do RFC 2397: data:, um media type opcional, uma flag ;base64 opcional, uma vírgula, então os dados. Montar um é concatenação de strings, e a única decisão é se a flag está lá (sem flag, o payload é texto percent-encoded, o que ninguém quer para binário):

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

O próprio conselho do RFC se aplica com interesse: data URIs são para valores curtos. Embutir um ícone de 50 KB é uma troca normal (uma requisição a menos), embutir uma foto de 5 MB é um bug de desempenho vestindo figurino de conveniência. Mantenha a flag, mantenha o media type honesto, e mantenha os bytes pequenos.

Montando o header de autenticação Basic

O header de autenticação mais antigo da web ainda é o caso de uso de Base64 mais fácil no Java, porque é exatamente uma chamada de encode. Segundo o RFC 7617, uma requisição Basic envia Authorization: Basic seguido da codificação Base64 de username:password; o próprio exemplo do RFC, QWxhZGRpbjpvcGVuIHNlc2FtZQ==, é "Aladdin:open sesame" disfarçado. No lado do cliente, montar o header são duas linhas de Base64 mais uma chamada 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());
  }
}

Três cautelas pertencem a este header. Primeira, o RFC é explícito de que Basic é codificação, não proteção: as credenciais são legíveis por qualquer pessoa que consiga ver os pacotes, então este header é tão forte quanto o HTTPS por baixo dele, e é uma má ideia em qualquer coisa que não seja TLS. Segunda, o charset: o RFC espera credenciais US-ASCII (UTF-8 para qualquer outra coisa, e o parâmetro de autenticação charset é consultivo), então escolha StandardCharsets.UTF_8 e mantenha consistência nas duas pontas. Terceira, uma nota sobre versões: o cliente java.net.http é do Java 11, num JVM mais antigo o mesmo header vai num HttpURLConnection com uma chamada setRequestProperty, e a linha do Base64 é idêntica nos dois casos. No lado do servidor do mesmo header, fazer o parse e decodificar é o exemplo do guia irmão, com o split no primeiro dois-pontos e a comparação em tempo constante. Os dois lados são duas chamadas da mesma API, e essa é a elegância silenciosa deste header.

Valores em configs, variáveis de env e colunas

Base64 é um recipiente de texto, e é por isso que aparece em lugares onde você não esperaria: um DSN de banco de dados com ponto-e-vírgulas num arquivo de env, uma senha com aspas num arquivo de properties, um certificado multi-linha num config map, um blob binário numa coluna TEXT porque o schema foi desenhado antes de alguém considerar BLOBs. O lado da codificação é uma chamada, e o enquadramento honesto é o que ele é: um truque de segurança de formato, não um truque de sigilo:

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

Duas regras mantêm isso honesto. Primeira, nunca armazene um segredo como Base64 e chame de criptografado: Base64 não adiciona entropia e não remove informação, no momento em que um desenvolvedor lê o arquivo ele decodifica o valor em uma chamada, e a seção de segurança do RFC aponta exatamente para esta falha, pessoas revelando credenciais colando trocas de protocolo "codificadas". Se o valor é secreto, cifre primeiro, e só então empacote o texto cifrado em Base64 se o canal exigir texto. Segunda, faça o orçamento do tamanho: o valor armazenado é cerca de um terço maior que o original, e uma coluna ou campo que coubesse o valor cru não vai caber o codificado. E quando o valor voltar, decodifique ele na fronteira e mantenha como bytes (para binário) ou string com charset explícito (para texto); aquela direção é terreno do guia irmão.

Streaming para dados grandes

Codificar é a direção que piora a memória, então a história de arquivo grande aqui é sobre manter o conjunto de trabalho pequeno. A versão de array do exemplo da seção de arquivos serve até o ponto em que o arquivo para de caber confortavelmente na memória; além disso, o adaptador de stream é o lance. wrap(OutputStream) retorna um output stream que codifica enquanto você escreve, então um arquivo de vários gigabytes nunca é segurado como um único array de bytes:

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

Há um comportamento neste stream que merece um destaque, porque o próprio javadoc aponta para ele: o stream embrulhado pode segurar alguns bytes sobrando internamente, e a prática recomendada é "fechar prontamente o output stream retornado depois do uso, durante o qual ele vai fazer o flush de todos os bytes sobrando possíveis para o output stream subjacente". Se você parar de escrever e ler o arquivo de saída antes de fechar, a cauda dos seus dados ainda está sentada no codificador, e o arquivo parece truncado. É por isso que o exemplo fecha packed antes de qualquer outra coisa tocar no arquivo, e em produção você colocaria os dois streams num bloco try-with-resources. Pegue o hábito: no stream de codificação, fechar faz parte de codificar.

Encontrando a guarda velha

Codebases herdadas estão cheias de APIs de Base64 que têm mais idade que o java.util.Base64, e reconhecê-las te poupa de mistérios tipo "por que isso quebra a minha saída". As quatro que você vai realmente encontrar:

API Onde você vai encontrar O que fazer
sun.misc.BASE64Encoder / BASE64Decoder Código anterior ao Java 8 Migre para java.util.Base64; removido no Java 9
javax.xml.bind.DatatypeConverter Código da era XML, web services antigos Removido no Java 11 (JEP 320); migre
org.apache.commons.codec.binary.Base64 Código que precisa rodar em JVMs pré-8 Mantenha para suporte pré-8, caso contrário a classe do JDK é o padrão
com.google.common.io.BaseEncoding Stacks pesados em Guava e big data Funciona bem; a classe do JDK não tem dependências

O par sun.misc é o que tem drama. Era uma API interna, não suportada (o tipo que compila bem no JDK da época e some sem um aviso de deprecação), e a saída dela tinha hábitos próprios, como quebrar o texto codificado em linhas, que é de onde vem um número surpreendente de bugs do tipo "o meu Base64 tem quebras de linha dentro". Quando o Java 9 chegou em setembro de 2017, a limpeza do sistema de módulos removeu ele, e o guia oficial de migração não economiza nas palavras: "Notavelmente, sun.misc.BASE64Encoder e sun.misc.BASE64Decoder foram removidas. Em vez disso, use a classe java.util.Base64 suportada, que foi adicionada no JDK 8". Se você rodar jdeps em código que ainda referencia as classes antigas, a ferramenta marca a dependência como "JDK removed internal API", que é o mais perto de um cone de trânsito que o JDK chega. O DatatypeConverter do JAXB teve uma vida mais longa mas similar, deprecado com os módulos Java EE na era do Java 9 e removido de vez no Java 11 pela JEP 320, "Remover os módulos Java EE e CORBA". As duas migrações são mecânicas: as chamadas antigas printBase64Binary e BASE64Encoder().encode mapeiam um-para-um sobre getEncoder().encodeToString, descontadas as diferenças de quebra de linha, e uma vez que o código está no java.util.Base64 ele roda em todo JDK de 8 a 26 sem mais pensamento.

Segurança e velocidade

A seção de segurança é curta, porque o trabalho do codificador não pode falhar nos dados, mas não está vazia. Base64 não é criptografia, e o padrão diz isso em palavras: a codificação Base "oculta visualmente informações que de outra forma seriam facilmente reconhecidas, como senhas, mas não fornece qualquer confidencialidade computacional", e a mesma seção nota que isso "já foi conhecido por causar incidentes de segurança". Os corolários práticos para o lado do codificador: não codifique um segredo para deixá-lo seguro (ele agora está menos seguro, porque cabe em mais canais); se o valor é secreto, cifre primeiro e codifique o texto cifrado; e mantenha na memória o gêmeo da maleabilidade, onde um receptor pode trocar uma grafia válida por outra (padding diferente, lixo nos bits sobrando) sem mudar os dados decodificados. Um codificador determinístico ajuda aqui: java.util.Base64 produz exatamente uma saída para exatamente uma entrada, então se o seu próprio sistema tanto escreve quanto lê um valor, a grafia é estável, e são os valores externos na fronteira de confiança que precisam de verificação de forma canônica.

Sobre velocidade, o lado do codificador tem a mesma história que o lado do decodificador: num JVM moderno a implementação embutida é rápida o bastante para que Base64 quase nunca seja o gargalo, e ela é o ponto de referência do benchmark. O mesmo benchmark de 2025 do gRPC-java mencionado no guia irmão (issue 11857, JMH no JDK 17 e 21) colocou o codificador do JDK com mais ou menos 2,5 a 3,8 vezes a vazão do do Guava, com a maior diferença no x86. Duas notas práticas: para caminhos quentes, compartilhe uma instância de codificador (a fábrica já retorna a mesma compartilhada) e prefira encode(byte[], byte[]) num array pré-dimensionado para pular a alocação; para dados enormes, a seção de streaming é a história de memória, e o custo da quebra de linha é ruído perto do disco. O único imposto de desempenho real no Base64 é o tamanho em si, e nenhuma implementação, incluindo esta, consegue negociá-lo para baixo.

A lista de verificação de armadilhas

Toda armadilha reunida num só lugar, todas específicas de Java:

  • O charset que falta. text.getBytes() sem charset explícito usa o padrão da plataforma: certo por acidente no JDK 18+, errado em qualquer coisa mais antiga, e errado em princípio em todo lugar. Passe StandardCharsets.UTF_8 e escreva o charset na especificação.
  • O JWT com padding. getUrlEncoder() adiciona padding por padrão, e tokens não querem padding. A chamada withoutPadding() faz parte da receita, não é um extra opcional; um token com = finais é um token que alguns validadores vão rejeitar e outros vão massacrar.
  • A saída quebrada. O codificador MIME quebra em 76 com CRLF e não adiciona quebra de linha final. Se o consumidor espera uma quebra final, adicione; se o consumidor não espera quebras de jeito nenhum, não use o codificador MIME.
  • O encode duplicado. Codificar um valor que já é Base64 produz uma string perfeitamente válida, perfeitamente inútil. A causa clássica: um campo chega pré-codificado de uma API e o seu código "ajudadoramente" codifica de novo. Confira antes de codificar.
  • Sinais de mais em URLs. A saída Base64 padrão contém +, que é um espaço numa query string antes do servidor vê-lo. Se um valor de alfabeto padrão precisa viajar numa URL, percent-encode ele, ou gere no alfabeto URL-safe desde o começo.
  • A conta de 33 por cento. Um valor que cabe na coluna cru não vai caber na codificada. Dimensione armazenamento, campos de mensagem e headers a partir de 4 * ceil(n / 3), e lembre-se de que a saída MIME quebrada adiciona alguns por cento por cima.
  • O stream sem fechar. O output stream embrulhado segura bytes sobrando até fechar. Ler o arquivo antes de fechar te dá uma codificação truncada. Try-with-resources, toda vez.
  • Segredos à vista de todos. Base64 é fita adesiva, não um cadeado. Credenciais codificadas num arquivo de config, num log ou numa variável de env são credenciais legíveis. Cifre primeiro, ou não cifre nunca.
  • O muro Android. No Android, java.util.Base64 só existe a partir do nível de API 26, abaixo disso a classe do framework é android.util.Base64 com as próprias constantes de flag (NO_PADDING, URL_SAFE e NO_WRAP). Fixar uma por código sem checar pode quebrar exatamente nos dispositivos que você nunca testou.
  • O truque do comprimento de linha. getMimeEncoder(77, ...) silenciosamente quebra em 76, porque o comprimento é arredondado para baixo para um múltiplo de quatro, e pedir 3 ou menos desativa a quebra por completo. Se o seu formato exige um comprimento de linha ímpar, o controle do MIME não é a ferramenta.

De sun.misc para a biblioteca padrão

A história do Java é curta, com um antes e depois claros. Antes de 2014, se você precisava de Base64 dentro do JDK, recebia o par interno sun.misc.BASE64Encoder e sun.misc.BASE64Decoder, sem suporte desde o primeiro dia, com os próprios hábitos de quebra de linha em 76 caracteres, ou recorria ao javax.xml.bind.DatatypeConverter em código XML, ou adicionava Apache Commons Codec ou Guava ao build, e foi assim que muita codebase empresarial acabou com três implementações de Base64 e nenhuma ideia de qual era qual. Em 18 de março de 2014, o Java 8 entregou o java.util.Base64: uma classe, três alfabetos, as regras do RFC 4648 e do RFC 2045 implementadas corretamente, o padrão de fábrica, os controles de padding e quebra de linha, e adaptadores de stream nas duas direções. Era o Base64 que a linguagem deveria ter tido desde o começo, e o javadoc diz Since: 1.8 desde então.

A limpeza veio em duas ondas. O Java 9 (21 de setembro de 2017) removeu o par sun.misc como parte da limpeza do sistema de módulos, com o guia de migração apontando todo desenvolvedor para a classe do JDK 8, e o Java 11 removeu o módulo JAXB e seu DatatypeConverter junto (JEP 320). O Java 18 (22 de março de 2022) trouxe a JEP 400, "UTF-8 by Default", que não tocou em Base64 de jeito nenhum, mas mudou o modo de falha das chamadas preguiçosas de getBytes() que alimentam-no: o charset padrão da plataforma virou UTF-8 em todo sistema operacional, então os padrões antigos de mojibake simplesmente pararam de se reproduzir em JVMs novas. Desde o 1.8 a API pública não mudou um único método. O que se moveu é o motor por baixo: correções de bug e trabalho de desempenho, e é por isso que benchmarks da comunidade continuam encontrando a versão da biblioteca padrão superando as bibliotecas legado que ela substituiu. Hoje, em qualquer JDK de 8 a 26, a resposta para "como eu faço Base64 disso no Java" é um import e uma chamada de fábrica, e já é há mais de uma década.

Alguns prazeres de nerd

Porque um manual deveria terminar num sorriso, aqui vão alguns fatos específicos de Java que são simplesmente divertidos:

  • O javadoc diz Since: 1.8, e está verdadeiro há doze anos. Nenhum método adicionado, nenhum removido, nenhum comportamento alterado: uma das superfícies de API mais congeladas por mais tempo da linguagem, e você usa sem pensar.
  • encodeToString constrói a String do resultado com o charset ISO-8859-1, segundo o javadoc. É um detalhe completamente desnecessário na prática, porque a saída Base64 é ASCII puro e parece igual em Latin-1, UTF-8 e na maior parte do resto do zoológico de charsets, mas o javadoc te conta do mesmo jeito, e isso é o JDK sendo o JDK.
  • O codificador MIME não adiciona separador de linha depois da última linha parcial. Outras ferramentas, incluindo algumas bibliotecas de e-mail muito famosas, terminam a saída quebrada com um CRLF final. Se o seu diff contra uma implementação de referência tem exatamente dois caracteres a mais no fim, você encontrou este truque.
  • Pede ao getMimeEncoder linhas de 77 caracteres e ele te dá 76: o comprimento de linha é arredondado para baixo para o múltiplo de quatro mais próximo, silenciosamente, porque uma quebra que divide um grupo de quatro caracteres produziria lixo. A API se recusa a construir uma linha quebrada em vez de pedir a sua permissão.
  • Base64.getEncoder() == Base64.getEncoder() é verdadeiro. Os métodos de fábrica retornam a mesma instância compartilhada em toda chamada, então a API "pegue uma nova" é um figurino para um singleton, e a promessa de thread-safety é só uma descrição do que o JVM já está fazendo.
  • No Android, a API gêmea android.util.Base64 expõe as mesmas decisões como flags: NO_PADDING, URL_SAFE, NO_WRAP. Duas APIs, uma tabela de decisões, que é um testemunho silencioso de quão assentado o design do Base64 é por estes dias.
  • A seção 5 do RFC 4648 é onde o nome "base64url" nasce: a especificação diz que a codificação URL-safe "pode ser referida como base64url" e avisa que "não deve ser considerada a mesma que a codificação base64". A origem dele é anotada em nota de rodapé como um post de 2001 numa mailing list de P2P-hackers, então o nome em toda URL que você cola tem linhagem de mailing list.
  • Codifique a palavra base64 e você recebe YmFzZTY0, sem padding, porque seis é múltiplo de três. Um formato se descrevendo é o equivalente técnico de um espelho que fala em Morse, e este é o próprio reflexo do espelho.
  • Rode jdeps -jdkinternals em código anterior ao Java 8 e veja ele marcar sun.misc.BASE64Encoder como "JDK removed internal API". O exemplo da ferramenta no guia oficial de migração é uma classe de Base64, que é o JDK apontando para os seus imports e dizendo "a gente conversou sobre isso".
  • O fator 1,37. Todo payload MIME quebrado custa mais ou menos 1,37 vezes o tamanho original (4/3 pelo alfabeto, 78/76 pelo ritmo do CRLF), uma fração tão estável que a matemática antiga de e-mail ainda a cita: a taxa que a infraestrutura de e-mail dos anos 1990 cobrava em todo anexo é exatamente a conta que o getMimeEncoder() cobra hoje.

Seguindo a outra direção

Essa é a parte do codificador da história, e é a mais calma das duas: o trabalho nunca falha nos dados, as armadilhas são sobre as suas decisões (charset, padding, quebra de linha, dialeto) em vez de surpresas de outras pessoas, e a API inteira cabe em um import. A outra direção é onde o Base64 para de ser conveniente e começa a ser adversarial, porque decodificar é onde você encontra as escolhas de padding dos outros, as quebras de linha deles, os charsets deles e a blindagem deles, com uma IllegalArgumentException de pé entre você e a verdade. Decodificação Base64 em Java, linkada desta página, cobre o decodificador na mesma profundidade: as três personalidades de decodificador, as mensagens de erro exatas, as regras de padding, base64url e JWTs, MIME e PEM, e as armadilhas específicas de Java reunidas num só lugar. Leia as duas como par e o assunto inteiro é seu.

Última atualização: 2026-09-08

Artigo relacionado: Decodificação Base64 em Java: um guia completo