💡 Exemple complet fonctionnel disponible sur GitHub :
sign-documents-in-docker-fonts-java

Le service de signature de contrats qui a fonctionné pendant neuf mois

L’approvisionnement des polices dans le conteneur est l’étape qui détermine si un service de signature Java fonctionne en production ou seulement dans les tests que vous avez écrits par hasard. Cela compte parce que l’échec est programmé : une image JRE vous donne suffisamment de couverture de polices pour paraître correcte, puis retient le reste jusqu’à ce qu’un document spécifique arrive.

Considérez la forme du problème. Un flux de travail de documents signe des contrats, déployé sur eclipse-temurin:17-jre, et cela fonctionne. Neuf mois plus tard, l’entreprise signe son premier client au Japon, le nom apparaît dans le texte de la signature, et le job échoue avec Specified font file was not found. Rien n’a changé dans le service. L’image n’a jamais eu de couverture CJK ; aucun document ne l’avait demandée.

La cause technique est courte. eclipse-temurin:17-jre regroupe 8 fichiers de police DejaVu pour AWT, couvrant le latin, le grec et le cyrillique. GroupDocs.Signature ne substitue pas une famille manquante, donc une requête pour une police capable de gérer le japonais échoue au lieu de se dégrader, et laisser la police non définie n’aide pas parce que la bibliothèque demande alors Times New Roman, qui est également absent.

Pourquoi c’est pire qu’une image sans police

Les images de base .NET et Python n’incluent aucune police. C’est un échec meilleur : la toute première signature échoue, dès le premier test, et quelqu’un le corrige avant que le service ne soit mis en production.

Une image JVM échoue partiellement, ce qui est la version coûteuse. Le bug vit dans du code déjà en production, il est déclenché par les données du client plutôt que par quoi que ce soit dans le déploiement, et la personne de garde voit une erreur de police provenant d’un service que personne n’a touché depuis des mois. Le coût de l’incident n’est pas la correction – la correction n’est qu’une couche Dockerfile – c’est l’heure qui passe avant que quiconque ne réalise que les polices sont en cause.

Cette asymétrie est l’argument pour traiter la couverture de polices comme quelque chose que l’on affirme au démarrage plutôt que comme quelque chose que l’on découvre.

Cela change également qui paie. Une image sans police coûte à un développeur vingt minutes lors de la configuration. Une image partiellement couverte coûte à un ingénieur de garde une heure à un moment inopportun, plus la valeur du contrat retardé, plus la revue qui suit un incident que personne ne peut attribuer à un changement. La différence technique entre les deux se résume à quatre paquets dans un Dockerfile.

Ce que coûte réellement l’approvisionnement

Quatre paquets Debian dans l’étape d’exécution :

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 taille de l’image est l’objection habituelle, et il vaut la peine d’être précis : le paquet CJK est le gros, les trois autres sont petits, et aucun n’est optionnel si vos documents peuvent contenir des noms non latins. Installez ce dont votre jeu de documents a réellement besoin et vérifiez en relisant plutôt qu’en réduisant par instinct.

fontconfig est le résolveur ainsi que fc-list pour le débogage. fonts-dejavu-core duplique ce que le JRE regroupe déjà, ce qui est délibéré : cela garde l’image honnête si l’image de base change. fonts-liberation est important parce que les documents créés sous Windows font référence à Arial et Times New Roman par leur nom et attendent un rendu métriquement compatible. fonts-noto-cjk est celui dont l’incident ci‑dessus avait besoin.

Résoudre une famille au lieu d’en nommer une

L’approvisionnement seul ne suffit pas, car le code doit encore nommer une famille qui existe. La façon portable est de demander à la bibliothèque : essayer une signature jetable pour chaque candidate, garder la première qui ne lève pas d’exception.

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

Le test lui‑même est un appel de signature ordinaire dans le répertoire temporaire, avec l’échec converti en valeur plutôt qu’en exception :

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

La détection du nom de fichier est le raccourci qui semble équivalent mais ne l’est pas. Le paquet Debian fonts-noto-cjk installe NotoSansCJK-Regular.ttc, dont le nom de famille est Noto Sans CJK JP, ainsi la correspondance des noms de fichiers manque à la fois les polices et signale des familles qui ne résoudront pas.

Dégradation honnête

Avec la résolution en place, les deux classes d’échec se séparent proprement. L’absence de famille latine signifie que l’image ne peut pas signer du tout, ce qui devrait arrêter le conteneur. L’absence de famille CJK signifie qu’une signature est sautée et que l’exécution continue avec un avertissement :

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 distinction importe opérationnellement. Un conteneur qui s’arrête au démarrage avec « no usable font family » est un problème de déploiement, détecté par celui qui l’a déployé. Une signature manquante silencieusement dans un document livré est un problème de conformité, détecté par le destinataire. Brancher le cas fatal sur un code de sortie non nul maintient les échecs dans la première catégorie.

Puis relisez le résultat, car une signature CJK écrite sans couverture CJK peut s’afficher sous forme de cases vides sans lever d’erreur :

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

Où cela laisse‑t‑il une équipe qui a déjà livré ?

Ajoutez la couche de police, ajoutez la résolution au démarrage, et consignez les deux résultats sur la première ligne du service, où le prochain ingénieur les verra réellement. Le changement se résume à une modification du Dockerfile plus une trentaine de lignes, et il transforme un incident déclenché par le client en un conteneur qui démarre soit avec une couverture connue, soit refuse de démarrer. Les documents déjà signés restent inchangés ; seules les nouvelles signatures bénéficient du chemin CJK.

Vérifier une image que vous exécutez déjà

Avant de modifier quoi que ce soit, il est utile de savoir ce que votre image actuelle contient. Deux commandes le révèlent de l’extérieur :

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"

La première liste les fichiers de police, la seconde liste les noms de famille qu’un résolveur renverrait, et l’écart entre les deux explique pourquoi la correspondance par nom de fichier échoue. Si fc-list est absent, c’est déjà une réponse : fontconfig n’est pas installé, et toute recherche de famille se fait à l’aveugle.

Dans le service, le contrôle équivalent doit se trouver dans le journal de démarrage à côté des familles résolues. Une ligne du type fonts on disk: 8, latin: DejaVu Sans, cjk: (none) indique à la prochaine personne exactement ce que ce conteneur peut et ne peut pas signer, ce qui est plus utile que n’importe quelle exception qu’elle lirait à trois heures du matin.

Le détail JVM que personne n’attend

Une chose de plus qui mord spécifiquement sur Java, et ce n’est pas une question de polices. L’artifact Maven GroupDocs est un jar « fat » signé. Le reconditionner en jar ombré produit NoClassDefFoundError: com/groupdocs/signature/options/search/SearchOptions, et le remède habituel de suppression de META-INF/*.SF|RSA|DSA est insuffisant : MANIFEST.MF transporte 19 Mo de digestes par entrée et doit également être tronqué à sa section principale. L’exemple évite le problème en s’exécutant sur un classpath simple avec un répertoire dependency/ plutôt qu’en ombrant quoi que ce soit.

Je le mentionne parce que ces deux problèmes – la couverture partielle des polices et le jar signé – partagent une forme : le chemin JVM échoue d’une manière qui ressemble à votre code mais ne l’est pas. Tous deux sont également peu coûteux à contrer une fois nommés : fixez la disposition du classpath que vous savez fonctionnelle, et affirmez la couverture des polices au démarrage au lieu de faire confiance à l’image de base. Aucun ne nécessite une refonte, et les deux éliminent une classe d’incidents qui serait autrement indiscernable d’un bug d’application.

Conclusion

Un service de signature Java dans un conteneur n’est qu’une couche Dockerfile et une vérification au démarrage loin d’une prévisibilité totale. Installez fontconfig, DejaVu, Liberation et Noto CJK ; résolvez la famille en la sondant plutôt qu’en l’assumant ; sautez ce qui ne peut pas être incorporé ; vérifiez en relisant. Le dépôt d’exemple fournit les deux images, de sorte que la différence entre couverture et absence de couverture se voit en deux builds plutôt qu’en un incident à retenir.

Ressources supplémentaires