💡 Esempio completo funzionante disponibile su GitHub:
sign-documents-in-docker-fonts-java

Il servizio di firma dei contratti che ha funzionato per nove mesi

Il provisioning dei font nel container è il passaggio che decide se un servizio di firma Java funziona in produzione o solo nei test che hai scritto per caso. È importante perché il fallimento è programmato: un’immagine JRE ti fornisce una copertura di font sufficiente per apparire corretta, poi trattiene il resto fino a quando non arriva un documento specifico.

Considera la sua forma. Un flusso di lavoro di documenti firma contratti, distribuito su eclipse-temurin:17-jre, e funziona. Dopo nove mesi, l’azienda firma il suo primo cliente in Giappone, il nome entra nel testo della firma e il lavoro fallisce con Specified font file was not found. Nulla è cambiato nel servizio. L’immagine non ha mai avuto copertura CJK; nessun documento l’aveva richiesta.

La causa tecnica è breve. eclipse-temurin:17-jre include 8 file di font DejaVu per AWT, che coprono Latino, Greco e Cirillico. GroupDocs.Signature non sostituisce una famiglia mancante, quindi una richiesta di un font capace di giapponese fallisce invece di degradare, e lasciare il font non impostato non aiuta perché la libreria poi richiede Times New Roman, anch’esso assente.

Perché questo è peggiore di un’immagine senza font

Le immagini base .NET e Python non includono alcun font. Questo è un fallimento migliore: la prima firma fallisce, al primo test, e qualcuno la corregge prima che il servizio venga rilasciato.

Un’immagine JVM fallisce parzialmente, ed è la versione più costosa. Il bug vive in codice già in produzione, è attivato dai dati del cliente piuttosto che da qualcosa nel deployment, e la persona di turno vede un errore di font da un servizio che nessuno ha toccato da mesi. Il costo dell’incidente non è la correzione – la correzione è un singolo layer Dockerfile – è l’ora prima che chiunque creda che i font siano coinvolti.

Questa asimmetria è l’argomento per trattare la copertura dei font come qualcosa che si asserisce all’avvio anziché qualcosa che si scopre.

Cambia anche chi paga. Un’immagine senza font costa a uno sviluppatore venti minuti durante la configurazione. Un’immagine parzialmente coperta costa a un ingegnere di turno un’ora in un momento poco utile, più il valore del contratto ritardato, più la revisione che segue un incidente che nessuno può attribuire a una modifica. La differenza tecnica tra i due è quattro pacchetti in un Dockerfile.

Quanto costa realmente il provisioning

Quattro pacchetti Debian nello stage di 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/*

La dimensione dell’immagine è l’obiezione abituale, ed è utile essere specifici: il pacchetto CJK è quello grande, gli altri tre sono piccoli, e nessuno di essi è opzionale se i tuoi documenti possono contenere nomi non latini. Installa ciò di cui il tuo set di documenti ha realmente bisogno e verifica con una lettura di ritorno anziché tagliare per istinto.

fontconfig è il risolutore più fc-list per il debug. fonts-dejavu-core duplica ciò che il JRE già include, ed è deliberato: mantiene l’immagine onesta se l’immagine base cambia. fonts-liberation è importante perché i documenti creati su Windows fanno riferimento ad Arial e Times New Roman per nome e si aspettano un rendering metricamente compatibile. fonts-noto-cjk è quello di cui l’incidente sopra aveva bisogno.

Risolvere una famiglia invece di nominare una

Il provisioning da solo non è sufficiente, perché il codice deve comunque nominare una famiglia che esista. Il modo portabile è chiedere alla libreria: provare una firma di prova per ogni candidato, tenere la prima che non lancia eccezione.

for (String candidate : candidates) {
    if (tryFamily(sourcePath, candidate) == null) {
        return candidate;
    }
}
return null;

Il probe stesso è una normale chiamata di firma nella directory temporanea, con il fallimento convertito in un valore anziché in un’eccezione:

SignatureFont font = new SignatureFont();
font.setFamilyName(familyName);
font.setSize(10);
options.setFont(font);
signature.sign(scratch.getAbsolutePath(), options);
return null;

Il rilevamento del nome file è la scorciatoia che sembra equivalente ma non lo è. Il pacchetto Debian fonts-noto-cjk installa NotoSansCJK-Regular.ttc, il cui nome di famiglia è Noto Sans CJK JP, quindi il confronto dei nomi file sia manca i font sia segnala famiglie che non verranno risolte.

Degradare onestamente

Con la risoluzione in atto, le due classi di fallimento si separano nettamente. Nessuna famiglia latina significa che l’immagine non può firmare affatto, il che dovrebbe fermare il container. Nessuna famiglia CJK significa che una firma viene saltata e l’esecuzione continua con un avviso:

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

La distinzione è operativamente importante. Un container che esce all’avvio con “no usable font family” è un problema di deploy, catturato da chi lo ha distribuito. Una firma che manca silenziosamente da un documento consegnato è un problema di conformità, catturato dal destinatario. Collegare il caso fatale a un exit code diverso da zero mantiene i fallimenti nella prima categoria.

Poi leggi il risultato indietro, perché una firma CJK scritta senza copertura CJK può renderizzarsi come caselle vuote senza sollevare alcun errore:

TextSearchOptions options = new TextSearchOptions();
options.setAllPages(true);
List<TextSignature> found = signature.search(TextSignature.class, options);

Dove porta tutto questo un team che ha già rilasciato?

Aggiungi il layer dei font, aggiungi la risoluzione all’avvio e registra entrambi i risultati nella prima riga del servizio, dove il prossimo ingegnere li vedrà realmente. Il cambiamento è una modifica al Dockerfile più o meno trenta righe, e trasforma un incidente scatenato dal cliente in un container che o parte con copertura nota o rifiuta di avviarsi. I documenti già firmati rimangono invariati; solo i nuovi guadagnano il percorso CJK.

Verificare un’immagine che già utilizzi

Prima di cambiare qualsiasi cosa, è utile sapere cosa contiene attualmente la tua immagine. Due comandi rispondono dall’esterno:

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"

Il primo elenca i file dei font, il secondo elenca i nomi di famiglia che un risolutore restituirebbe, e il divario tra i due è il motivo per cui il matching dei nomi file fallisce. Se fc-list è assente, quella è già una risposta: fontconfig non è installato, e qualsiasi ricerca di famiglia avviene alla cieca.

All’interno del servizio, il controllo equivalente appartiene al log di avvio accanto alle famiglie risolte. Una riga del tipo fonts on disk: 8, latin: DejaVu Sans, cjk: (none) dice alla prossima persona esattamente cosa questo container può e non può firmare, il che è più utile di qualsiasi eccezione che altrimenti leggerebbero alle tre del mattino.

Il dettaglio JVM che nessuno si aspetta

Un’altra cosa che morde specificamente su Java, e non riguarda i font. L’artifact Maven di GroupDocs è un jar “fat” firmato. Ricrearlo in un jar ombreggiato produce NoClassDefFoundError: com/groupdocs/signature/options/search/SearchOptions, e il rimedio consueto di cancellare META-INF/*.SF|RSA|DSA è insufficiente: MANIFEST.MF contiene circa 19 MB di digest per voce e deve anch’esso essere troncato alla sua sezione principale. Il campione evita il problema eseguendosi su un classpath semplice con una directory dependency/ anziché ombreggiare nulla.

Lo segnalo perché entrambi – la copertura parziale dei font e il jar firmato – condividono una forma: il percorso JVM fallisce in un modo che sembra il tuo codice ma non lo è. Entrambi sono anche economici da difendere una volta identificati: fissa il layout del classpath che sai funzionare, e asserisci la copertura dei font all’avvio invece di fidarti dell’immagine base. Nessuno dei due richiede una riprogettazione, e entrambi rimuovono una classe di incidenti altrimenti indistinguibile da un bug dell’applicazione.

Conclusione

Un servizio di firma Java in un container è a un layer Dockerfile e a un controllo di avvio di distanza dalla prevedibilità. Installa fontconfig, DejaVu, Liberation e Noto CJK; risolvi la famiglia sondando anziché presumendo; salta ciò che non può essere incorporato; verifica leggendo indietro. Il repository di esempio fornisce entrambe le immagini, così la differenza tra copertura e nessuna copertura richiede due build per essere vista anziché un incidente da apprendere.

Risorse aggiuntive