💡 Plně funkční příklad je k dispozici na GitHubu:
sign-documents-in-docker-fonts-java

Služba pro podepisování smluv, která fungovala devět měsíců

Poskytování fontů v kontejneru je krok, který rozhoduje, zda Java podepisovací služba funguje v produkci, nebo jen v testech, které jste náhodou napsali. Má to význam, protože selhání je naplánované: JRE image vám poskytne dostatečné pokrytí fontů, aby výstup vypadal správně, a zbytek si ponechá, dokud nepřijde konkrétní dokument.

Představte si to takto. Pracovní tok dokumentů podepisuje smlouvy, nasazený na eclipse-temurin:17-jre, a funguje. Po devíti měsících společnost podepíše svého prvního zákazníka v Japonsku, jméno se objeví v textu podpisu a úloha selže s chybou Specified font file was not found. V službě se nic nezměnilo. Image nikdy neobsahovala podporu CJK; žádný dokument o to nepožadoval.

Technická příčina je jednoduchá. eclipse-temurin:17-jre balí 8 souborů fontů DejaVu pro AWT, které pokrývají latinku, řečtinu a cyrilici. GroupDocs.Signature nenahrazuje chybějící rodinu, takže požadavek na font schopný japonských znaků selže místo degradace, a ponechání fontu nenastaveného nepomůže, protože knihovna pak požaduje Times New Roman, který také chybí.

Proč je to horší než image bez fontů

Base image pro .NET a Python neobsahují žádné fonty. To je lepší selhání: první podpis selže hned v prvním testovacím běhu a někdo to opraví dříve, než služba odejde do produkce.

JVM image selhává jen částečně, což je dražší verze. Chyba žije v kódu, který už je v produkci, je vyvolána zákaznickými daty, nikoli nasazením, a osoba na pohotovosti vidí chybu fontu ze služby, kterou nikdo nezasahoval měsíce. Náklady incidentu nejsou v opravě – oprava je jen jedna vrstva Dockerfile – ale v hodině, než si kdokoli uvědomí, že jsou zapojeny fonty.

Tato asymetrie je argumentem pro to, aby se pokrytí fontů ověřovalo při startu místo toho, aby se objevovalo během běhu.

Mění to také, kdo platí. Image bez fontů stojí vývojáře dvacet minut během nastavení. Částečně pokrytý image stojí on‑call inženýra hodinu v nevhodnou dobu, plus co bylo zpožděno v kontraktu, plus revizi, která následuje po incidentu, který nikdo nedokáže přičíst ke změně. Technický rozdíl mezi těmito dvěma je čtyři balíčky v Dockerfile.

Co ve skutečnosti stojí poskytování

Čtyři Debian balíčky ve fázi 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/*

Velikost image je obvyklý námitek a stojí za to být konkrétní: balíček CJK je ten velký, ostatní tři jsou malé a žádný z nich není volitelný, pokud vaše dokumenty mohou nést ne‑latinská jména. Nainstalujte jen to, co vaše sada dokumentů skutečně potřebuje, a ověřte pomocí zpětného čtení místo ořezávání podle intuice.

fontconfig je resolver plus fc-list pro ladění. fonts-dejavu-core duplikuje to, co JRE už balí, což je úmyslné: udržuje image poctivou, pokud se základní image změní. fonts-liberation je důležitý, protože dokumenty vytvořené ve Windows odkazují na Arial a Times New Roman jménem a očekávají metricky kompatibilní vykreslení. fonts-noto-cjk je ten, který incident výše vyžadoval.

Vyhledání rodiny místo pojmenování jedné

Poskytování samo o sobě nestačí, protože kód stále musí pojmenovat existující rodinu. Přenosný způsob je zeptat se knihovny: pokusit se o jednorázový podpis pro každého kandidáta a zachovat první, který nevyhodí výjimku.

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

Samotný test je obyčejné volání podpisu do dočasného adresáře, přičemž selhání se převede na hodnotu místo výjimky:

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

Detekce názvu souboru je zkratka, která vypadá ekvivalentně, ale není. Debian fonts-noto-cjk instaluje NotoSansCJK-Regular.ttc, jehož název rodiny je Noto Sans CJK JP, takže porovnávání názvů souborů jak chybí fonty, tak hlásí rodiny, které se nevyřeší.

Poctivé degradování

S řešením na místě se dva typy selhání čistě oddělí. Žádná latinská rodina znamená, že image nemůže vůbec podepisovat, což by mělo zastavit kontejner. Žádná CJK rodina znamená, že jeden podpis se přeskočí a běh pokračuje s varováním:

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

Rozlišení má operační význam. Kontejner, který při startu skončí s „no usable font family“, je problém nasazení, zachytí ho ten, kdo jej nasadil. Podpis, který tiše chybí v doručeném dokumentu, je problém shody, zachytí ho příjemce. Propojení fatálního případu s nenulovým návratovým kódem udržuje selhání v první kategorii.

Pak si výsledek přečtěte zpět, protože CJK podpis napsaný bez CJK pokrytí se může vykreslit jako prázdné rámečky, aniž by něco vyvolalo:

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

Kam to vede tým, který už vydal produkt?

Přidejte vrstvu s fonty, přidejte řešení při startu a zaznamenejte oba výsledky v první řádce služby, kde je další inženýr skutečně uvidí. Změna je úprava Dockerfile plus zhruba třicet řádků a převádí incident vyvolaný zákazníkem na kontejner, který buď startuje s známým pokrytím, nebo odmítne startovat. Existující podepsané dokumenty zůstávají nedotčeny; jen nové získají CJK cestu.

Kontrola image, kterou již používáte

Než něco změníte, stojí za to vědět, co vaše aktuální image obsahuje. Dvě příkazy to zjistí zvenčí:

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"

První vypíše soubory fontů, druhý vypíše názvy rodin, které by resolver vrátil, a mezera mezi nimi je důvod, proč selhává porovnávání názvů souborů. Pokud fc-list chybí, je to samo o sobě odpověď: fontconfig není nainstalován a jakékoli vyhledávání rodiny probíhá naslepo.

Uvnitř služby patří ekvivalentní kontrola do startovacího logu vedle vyřešených rodin. Řádek jako fonts on disk: 8, latin: DejaVu Sans, cjk: (none) řekne dalšímu člověku přesně, co tento kontejner může a nemůže podepisovat, což je užitečnější než jakákoli výjimka, kterou by jinak četli ve třech ráno.

Detail JVM, který nikdo neočekává

Ještě jedna věc, která specificky štípá Java, a není to o fontech. Maven artefakt GroupDocs je podepsaný „fat jar“. Přepakování do „shaded jar“ způsobí NoClassDefFoundError: com/groupdocs/signature/options/search/SearchOptions, a obvyklý postup mazání META-INF/*.SF|RSA|DSA není dostatečný: MANIFEST.MF nese kolem 19 MB per‑entry digestů a musí být také zkrácen na hlavní sekci. Vzorové řešení se tomuto problému vyhýbá tím, že běží proti čisté classpath s adresářem dependency/ místo jakéhokoli shadingu.

Zmiňuji to, protože oba tyto problémy – částečné pokrytí fontů a podepsaný jar – mají společný tvar: cesta JVM selže způsobem, který vypadá jako chyba vašeho kódu, ale není. Oba jsou také levné na obranu, jakmile jsou pojmenovány: připněte layout classpath, který znáte, že funguje, a ověřte pokrytí fontů při startu místo důvěry v základní image. Žádný z nich nevyžaduje redesign a oba odstraňují třídu incidentů, která by jinak byla nerozeznatelná od aplikační chyby.

Závěr

Java podepisovací služba v kontejneru je jen jedna vrstva Dockerfile a jedno startovací ověření daleko od předvídatelnosti. Nainstalujte fontconfig, DejaVu, Liberation a Noto CJK; rodinu vyřešte probíháním místo předpokladu; přeskočte to, co nelze vložit; ověřte zpětným čtením. Vzorové úložiště dodává oba obrazy, takže rozdíl mezi pokrytím a jeho absencí vyžaduje dva buildy místo jednoho incidentu k naučení.

Další zdroje