💡 Exemplo completo em funcionamento disponível no GitHub:
sign-documents-in-docker-fonts-java
O Serviço de Assinatura de Contratos que Funcionou por Nove Meses
O provisionamento de fontes no contêiner é a etapa que decide se um serviço de assinatura Java funciona em produção ou apenas nos testes que você acabou de escrever. Isso importa porque a falha é programada: uma imagem JRE fornece cobertura de fontes suficiente para parecer correta e, em seguida, retém o restante até que um documento específico chegue.
Considere o cenário. Um fluxo de trabalho de documentos assina contratos, implantado em eclipse-temurin:17-jre, e funciona. Nove meses depois, a empresa assina seu primeiro cliente no Japão, o nome entra no texto da assinatura e o trabalho falha com Specified font file was not found. Nada mudou no serviço. A imagem nunca teve cobertura CJK; nenhum documento havia solicitado isso.
A causa técnica é curta. eclipse-temurin:17-jre inclui 8 arquivos de fonte DejaVu para AWT, que cobrem Latin, Greek e Cyrillic. GroupDocs.Signature não substitui uma família ausente, portanto uma solicitação por uma fonte compatível com japonês falha em vez de degradar, e deixar a fonte não definida não ajuda porque a biblioteca então solicita Times New Roman, que também está ausente.
Por Que Isso É Pior Que uma Imagem Sem Fontes
As imagens base .NET e Python enviam zero fontes. Essa é uma falha melhor: a primeira assinatura falha, no primeiro teste, e alguém a corrige antes que o serviço seja entregue.
Uma imagem JVM falha parcialmente, o que é a versão mais cara. O bug vive em código que já está em produção, é acionado por dados do cliente em vez de por algo na implantação, e a pessoa de plantão vê um erro de fonte de um serviço que ninguém tocou há meses. O custo do incidente não é a correção – a correção é uma camada do Dockerfile – é a hora antes de alguém acreditar que fontes estão envolvidas.
Essa assimetria é o argumento para tratar a cobertura de fontes como algo que você afirma na inicialização, e não como algo que você descobre.
Isso também muda quem paga. Uma imagem sem fontes custa ao desenvolvedor vinte minutos durante a configuração. Uma imagem parcialmente coberta custa a um engenheiro de plantão uma hora em um horário inconveniente, mais o que o contrato atrasado valia, mais a revisão que segue um incidente que ninguém pode atribuir a uma mudança. A diferença técnica entre as duas são quatro pacotes em um Dockerfile.
Quanto Custa o Provisionamento
Quatro pacotes Debian na fase de runtime:
RUN apt-get update && apt-get install -y --no-install-recommends \
fontconfig \
fonts-dejavu-core \
fonts-liberation \
fonts-noto-cjk \
&& fc-cache -f \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
O tamanho da imagem é a objeção usual, e vale a pena ser específico: o pacote CJK é o grande, os outros três são pequenos, e nenhum deles é opcional se seus documentos podem conter nomes não‑Latin. Instale apenas o que seu conjunto de documentos realmente precisa e verifique com uma leitura de volta em vez de cortar por instinto.
fontconfig é o resolvedor mais fc-list para depuração. fonts-dejavu-core duplica o que o JRE já inclui, o que é deliberado: mantém a imagem honesta se a imagem base mudar. fonts-liberation importa porque documentos criados no Windows referenciam Arial e Times New Roman pelo nome e esperam renderização compatível metricamente. fonts-noto-cjk é o que o incidente acima precisou.
Resolvendo uma Família Em Vez de Nomear Uma
Provisionar sozinho não basta, porque o código ainda precisa nomear uma família que exista. A forma portátil é perguntar à biblioteca: tentar uma assinatura descartável por candidato, manter a primeira que não lançar exceção.
for (String candidate : candidates) {
if (tryFamily(sourcePath, candidate) == null) {
return candidate;
}
}
return null;
A sondagem em si é uma chamada de assinatura comum no diretório temporário, com a falha convertida em um valor em vez de exceção:
SignatureFont font = new SignatureFont();
font.setFamilyName(familyName);
font.setSize(10);
options.setFont(font);
signature.sign(scratch.getAbsolutePath(), options);
return null;
A detecção por nome de arquivo é o atalho que parece equivalente e não é. O fonts-noto-cjk do Debian instala NotoSansCJK-Regular.ttc, cujo nome de família é Noto Sans CJK JP, de modo que combinar nomes de arquivos tanto perde fontes quanto relata famílias que não serão resolvidas.
Degradando Honestamente
Com a resolução em vigor, as duas classes de falha se separam claramente. Nenhuma família Latin significa que a imagem não pode assinar de forma alguma, o que deve parar o contêiner. Nenhuma família CJK significa que uma assinatura é pulada e a execução continua com um aviso:
List<SignOptions> options = new ArrayList<>();
options.add(buildTextOptions(LATIN_TEXT, latinFamily, 50));
if (cjkFamily != null) {
options.add(buildTextOptions(CJK_TEXT, cjkFamily, 120));
}
SignResult result = signature.sign(outputPath, options);
A distinção importa operacionalmente. Um contêiner que sai na inicialização com “no usable font family” é um problema de implantação, capturado por quem o implantou. Uma assinatura que desaparece silenciosamente de um documento entregue é um problema de conformidade, capturado pelo destinatário. Encaminhar o caso fatal para um código de saída diferente de zero mantém as falhas na primeira categoria.
Então leia o resultado de volta, porque uma assinatura CJK escrita sem cobertura CJK pode ser renderizada como caixas vazias sem gerar nenhum erro:
TextSearchOptions options = new TextSearchOptions();
options.setAllPages(true);
List<TextSignature> found = signature.search(TextSignature.class, options);
Onde Isso Deixa uma Equipe que Já Lançou?
Adicione a camada de fontes, adicione a resolução na inicialização e registre ambos os resultados na primeira linha do serviço, onde o próximo engenheiro realmente os verá. A mudança é uma edição do Dockerfile mais ou menos trinta linhas, e converte um incidente acionado por cliente em um contêiner que ou inicia com cobertura conhecida ou se recusa a iniciar. Documentos já assinados permanecem inalterados; apenas os novos ganham o caminho CJK.
Verificando uma Imagem que Você Já Executa
Antes de mudar qualquer coisa, vale a pena saber o que sua imagem atual possui. Dois comandos respondem isso de fora:
docker run --rm your-image sh -c "ls -R /usr/share/fonts | head"
docker run --rm your-image sh -c "fc-list : family | sort -u | head -20"
O primeiro lista arquivos de fonte, o segundo lista os nomes de família que um resolvedor retornaria, e a diferença entre eles é a razão pela qual a correspondência por nome de arquivo falha. Se fc-list estiver ausente, essa é a própria resposta: fontconfig não está instalado, e qualquer busca de família está operando às cegas.
Dentro do serviço, a verificação equivalente deve estar no log de inicialização ao lado das famílias resolvidas. Uma linha como fonts on disk: 8, latin: DejaVu Sans, cjk: (none) informa à próxima pessoa exatamente o que este contêiner pode e não pode assinar, o que é mais útil que qualquer exceção que eles leriam às três da manhã.
O Detalhe da JVM que Ninguém Espera
Mais um ponto que morde especificamente no Java, e não tem a ver com fontes. O artefato Maven GroupDocs é um fat jar assinado. Reempacotá‑lo em um shaded jar produz NoClassDefFoundError: com/groupdocs/signature/options/search/SearchOptions, e o remédio usual de excluir META-INF/*.SF|RSA|DSA é insuficiente: MANIFEST.MF carrega cerca de 19 MB de digests por entrada e também deve ser truncado para sua seção principal. O exemplo evita o problema executando contra um classpath simples com um diretório dependency/ em vez de sombrear qualquer coisa.
Menciono isso porque ambos – a cobertura parcial de fontes e o jar assinado – compartilham um padrão: o caminho da JVM falha de uma forma que parece ser seu código e não é. Ambos também são baratos de defender uma vez identificados: fixe o layout de classpath que você sabe que funciona e afirme a cobertura de fontes na inicialização em vez de confiar na imagem base. Nenhum deles custa uma reformulação, e ambos eliminam uma classe de incidentes que de outra forma seria indistinguível de um bug de aplicação.
Conclusão
Um serviço de assinatura Java em contêiner está a uma camada de Dockerfile e a uma verificação de inicialização de distância de ser previsível. Instale fontconfig, DejaVu, Liberation e Noto CJK; resolva a família sondando em vez de presumir; ignore o que não puder ser incorporado; verifique lendo de volta. O repositório de exemplo entrega ambas as imagens, de modo que a diferença entre cobertura e ausência de cobertura leva duas compilações para ser vista em vez de um incidente para aprender.