💡 Voll funktionsfähiges Beispiel auf GitHub verfügbar:
sign-documents-in-docker-fonts-java

Der Vertragsunterzeichnungs‑Dienst, der neun Monate funktionierte

Die Bereitstellung von Schriftarten im Container ist der Schritt, der entscheidet, ob ein Java‑Signing‑Dienst in der Produktion funktioniert oder nur in den Tests, die Sie zufällig geschrieben haben. Das ist wichtig, weil das Versagen geplant ist: Ein JRE‑Image liefert genug Schriftabdeckung, um korrekt auszusehen, hält dann aber den Rest zurück, bis ein bestimmtes Dokument eintrifft.

Betrachten wir das Szenario. Ein Dokumenten‑Workflow signiert Verträge, läuft auf eclipse-temurin:17-jre und funktioniert. Nach neun Monaten unterschreibt das Unternehmen seinen ersten Kunden in Japan, der Name erscheint im Signaturtext, und der Job schlägt mit Specified font file was not found fehl. An dem Dienst hat sich nichts geändert. Das Image hatte nie CJK‑Abdeckung; kein Dokument hatte danach gefragt.

Die technische Ursache ist kurz: eclipse-temurin:17-jre bündelt 8 DejaVu‑Schriftdateien für AWT, die Lateinisch, Griechisch und Kyrillisch abdecken. GroupDocs.Signature ersetzt keine fehlende Familie, sodass eine Anforderung an eine japanisch‑fähige Schrift fehlschlägt, anstatt zu degradieren, und das Nicht‑Setzen der Schrift hilft nicht, weil die Bibliothek dann nach Times New Roman fragt, das ebenfalls fehlt.

Warum das schlimmer ist als ein bild ohne Schriftarten

Die .NET‑ und Python‑Basis‑Images enthalten keinerlei Schriftarten. Das ist ein besseres Versagen: Die allererste Signatur schlägt im ersten Testlauf fehl, und jemand behebt das, bevor der Dienst ausgeliefert wird.

Ein JVM‑Image schlägt teilweise fehl, was die teurere Variante ist. Der Bug steckt im bereits produktiven Code, wird durch Kundendaten ausgelöst und nicht durch etwas in der Bereitstellung, und die Person im Bereitschaftsdienst sieht einen Schriftarten‑Fehler von einem Dienst, an dem seit Monaten niemand mehr gearbeitet hat. Die Kosten des Vorfalls sind nicht die Behebung – die Behebung ist nur eine Dockerfile‑Ebene – sondern die Stunde, bevor jemand überhaupt an Schriftarten denkt.

Diese Asymmetrie ist das Argument dafür, Schriftabdeckung beim Start zu prüfen, anstatt sie erst zu entdecken.

Sie ändert auch, wer zahlt. Ein Bild ohne Schriftarten kostet einen Entwickler zwanzig Minuten bei der Einrichtung. Ein teilweise abgedecktes Bild kostet einen Bereitschafts‑Engineer eine Stunde zu einer ungünstigen Zeit, plus den Wert des verzögerten Vertrags, plus die Nachbearbeitung, die einem Vorfall folgt, den niemand einer Änderung zuordnen kann. Der technische Unterschied zwischen den beiden besteht aus vier Paketen in einem Dockerfile.

Was die Bereitstellung tatsächlich kostet

Vier Debian‑Pakete in der Runtime‑Phase:

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/*

Die Bildgröße ist das übliche Argument, und es lohnt sich, konkret zu sein: Das CJK‑Paket ist das große, die anderen drei sind klein, und keines von ihnen ist optional, wenn Ihre Dokumente nicht‑lateinische Namen enthalten können. Installieren Sie nur das, was Ihr Dokumentensatz tatsächlich benötigt, und verifizieren Sie mit einem Rücklesen, anstatt aus Instinkt zu kürzen.

fontconfig ist der Resolver plus fc-list zum Debuggen. fonts-dejavu-core dupliziert, was das JRE bereits bündelt, was beabsichtigt ist: Es hält das Image ehrlich, falls das Basis‑Image sich ändert. fonts-liberation ist wichtig, weil Dokumente, die unter Windows erstellt wurden, Arial und Times New Roman per Namen referenzieren und eine metrisch kompatible Darstellung erwarten. fonts-noto-cjk ist das, was der oben beschriebene Vorfall benötigte.

Eine Familie auflösen statt einen Namen zu verwenden

Allein die Bereitstellung reicht nicht, weil der Code immer noch eine existierende Familie benennen muss. Der portable Weg ist, die Bibliothek zu fragen: Einen Wegwerf‑Signaturversuch pro Kandidat durchführen und die erste zurückgeben, die keinen Fehler wirft.

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

Der Probe‑Aufruf ist ein gewöhnlicher Signaturaufruf in das temporäre Verzeichnis, wobei der Fehler in einen Rückgabewert umgewandelt wird, anstatt eine Ausnahme zu werfen:

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

Die Dateinamenerkennung ist die Abkürzung, die gleichwertig aussieht, es aber nicht ist. Debian’s fonts-noto-cjk installiert NotoSansCJK-Regular.ttc, dessen Familienname Noto Sans CJK JP lautet, sodass das Vergleichen von Dateinamen sowohl Schriftarten verfehlt als auch Familien meldet, die nicht aufgelöst werden können.

Ehrlich degradieren

Mit der Auflösung im Platz trennen sich die beiden Fehlertypen sauber. Keine lateinische Familie bedeutet, dass das Image überhaupt nicht signieren kann, was den Container stoppen sollte. Keine CJK‑Familie bedeutet, dass eine Signatur übersprungen wird und die Ausführung mit einer Warnung weiterläuft:

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

Die Unterscheidung ist betrieblich wichtig. Ein Container, der beim Start mit „no usable font family“ beendet, ist ein Deploy‑Problem, das von demjenigen, der ihn bereitgestellt hat, erkannt wird. Eine stillschweigend fehlende Signatur in einem ausgelieferten Dokument ist ein Compliance‑Problem, das vom Empfänger bemerkt wird. Das Verknüpfen des fatalen Falls mit einem Exit‑Code ungleich Null hält Fehler in der ersten Kategorie.

Dann lesen Sie das Ergebnis zurück, weil eine CJK‑Signatur, die ohne CJK‑Abdeckung geschrieben wurde, als leere Kästchen erscheinen kann, ohne etwas auszulösen:

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

Wo lässt das ein bereits ausgeliefertes Team stehen?

Fügen Sie die Schrift‑Ebene hinzu, fügen Sie die Auflösung beim Start hinzu und protokollieren Sie beide Ergebnisse in der ersten Zeile des Dienstes, wo der nächste Engineer sie tatsächlich sieht. Die Änderung besteht aus einer Dockerfile‑Bearbeitung plus etwa dreißig Zeilen und wandelt einen kunden­ausgelösten Vorfall in einen Container um, der entweder mit bekannter Abdeckung startet oder den Start verweigert. Bereits signierte Dokumente bleiben unverändert; nur neue erhalten den CJK‑Pfad.

Überprüfung eines bereits laufenden Images

Bevor Sie etwas ändern, lohnt es sich zu wissen, was Ihr aktuelles Image enthält. Zwei Befehle beantworten das von außen:

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"

Der erste listet Schriftdateien auf, der zweite listet die Familiennamen, die ein Resolver zurückgeben würde, und die Lücke dazwischen ist der Grund, warum das Dateinamen‑Matching fehlschlägt. Wenn fc-list fehlt, ist das bereits die Antwort: fontconfig ist nicht installiert, und jede Familien‑Suche läuft blind.

Im Service selbst gehört die entsprechende Prüfung in das Start‑Log neben die aufgelösten Familien. Eine Zeile wie fonts on disk: 8, latin: DejaVu Sans, cjk: (none) sagt der nächsten Person exakt, was dieser Container signieren kann und was nicht, und ist nützlicher als jede Ausnahme, die sie sonst um drei Uhr morgens lesen würden.

Das JVM‑Detail, das niemand erwartet

Ein weiteres Problem, das speziell Java betrifft, und das hat nichts mit Schriftarten zu tun. Das GroupDocs‑Maven‑Artefakt ist ein signiertes Fat‑Jar. Das Umwandeln in ein Shaded‑Jar erzeugt NoClassDefFoundError: com/groupdocs/signature/options/search/SearchOptions, und das übliche Vorgehen, META-INF/*.SF|RSA|DSA zu löschen, reicht nicht aus: MANIFEST.MF enthält rund 19 MB an Eintrag‑Digests und muss ebenfalls auf den Hauptabschnitt gekürzt werden. Das Beispiel umgeht das Problem, indem es gegen einen einfachen Klassenpfad mit einem dependency/‑Verzeichnis läuft, anstatt irgendetwas zu shade.

Ich erwähne das, weil beide – die partielle Schrift‑Abdeckung und das signierte Jar – eine Form teilen: Der JVM‑Pfad schlägt fehl auf eine Weise, die wie Ihr Code aussieht, es aber nicht ist. Beide lassen sich leicht abwehren, sobald sie benannt sind: Pinnen Sie das Klassenpfad‑Layout, von dem Sie wissen, dass es funktioniert, und prüfen Sie die Schrift‑Abdeckung beim Start, anstatt dem Basis‑Image zu vertrauen. Keines von beidem erfordert ein Redesign, und beide entfernen eine Vorfall‑Klasse, die sonst nicht von einem Anwendungs‑Bug zu unterscheiden ist.

Fazit

Ein Java‑Signing‑Dienst im Container ist nur eine Dockerfile‑Ebene und ein Start‑Check von vorhersehbarer Funktionsweise entfernt. Installieren Sie fontconfig, DejaVu, Liberation und Noto CJK; lösen Sie die Familie durch Proben statt durch Annahmen; überspringen Sie, was nicht eingebettet werden kann; verifizieren Sie durch Rücklesen. Das Beispiel‑Repository liefert beide Images, sodass der Unterschied zwischen Abdeckung und keiner Abdeckung zwei Builds erfordert, um ihn zu sehen, anstatt einen Vorfall, um ihn zu lernen.

Weitere Ressourcen