💡 Pełny działający przykład dostępny na GitHubie:
sign-documents-in-docker-fonts-java
Usługa podpisywania umów, która działała przez dziewięć miesięcy
Provisioning czcionek w kontenerze to krok, który decyduje, czy usługa podpisywania w Javie działa w produkcji, czy tylko w testach, które przypadkowo napisano. Ma to znaczenie, ponieważ awaria jest zaplanowana: obraz JRE zapewnia wystarczające pokrycie czcionek, aby wyglądało poprawnie, a resztę wstrzymuje, dopóki nie pojawi się konkretny dokument.
Rozważmy jej kształt. Przepływ pracy dokumentów podpisuje umowy, wdrożony na eclipse-temurin:17-jre, i działa. Po dziewięciu miesiącach firma podpisuje pierwszego klienta w Japonii, imię pojawia się w tekście podpisu i zadanie kończy się błędem Specified font file was not found. Nic nie zmieniło się w usłudze. Obraz nigdy nie miał pokrycia CJK; żaden dokument o to nie prosił.
Techniczna przyczyna jest krótka. eclipse-temurin:17-jre zawiera 8 plików czcionek DejaVu dla AWT, które obejmują łaciński, grecki i cyrylicę. GroupDocs.Signature nie podstawia brakującej rodziny, więc żądanie czcionki obsługującej japoński kończy się niepowodzeniem zamiast degradacji, a pozostawienie czcionki nieustawionej nie pomaga, ponieważ biblioteka wtedy prosi o Times New Roman, który również jest nieobecny.
Dlaczego to gorsze niż obraz bez czcionek
Obrazy bazowe .NET i Pythona nie zawierają żadnych czcionek. To lepsza awaria: pierwszy podpis nie udaje się w pierwszym uruchomieniu testu i ktoś to naprawia, zanim usługa zostanie wypuszczona.
Obraz JVM zawodzi częściowo, co jest kosztowną wersją. Błąd tkwi w kodzie, który już jest w produkcji, jest wywoływany danymi klienta, a nie czymś w wdrożeniu, i osoba na dyżurze widzi błąd czcionki z usługi, której nikt nie dotykał od miesięcy. Koszt incydentu to nie naprawa – naprawa to jedna warstwa Dockerfile – to godzina, zanim ktokolwiek uwierzy, że problemem są czcionki.
Ta asymetria jest argumentem za traktowaniem pokrycia czcionek jako czegoś, co weryfikuje się przy starcie, a nie czegoś, co odkrywa się później.
Zmienia to także, kto płaci. Obraz bez czcionek kosztuje dewelopera dwadzieścia minut podczas konfiguracji. Obraz częściowo pokryty kosztuje inżyniera na dyżurze godzinę w nieodpowiednim czasie, plus wartość opóźnionego kontraktu, plus przegląd po incydencie, którego nikt nie potrafi powiązać ze zmianą. Techniczna różnica między nimi to cztery pakiety w Dockerfile.
Co tak naprawdę kosztuje provisioning
Cztery pakiety Debiana w etapie 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/*
Rozmiar obrazu jest zwykle wymienianą zastrzeżeniem i warto być konkretnym: pakiet CJK jest tym dużym, pozostałe trzy są małe i żaden z nich nie jest opcjonalny, jeśli Twoje dokumenty mogą zawierać nazwy niełacińskie. Zainstaluj to, czego naprawdę potrzebuje Twój zestaw dokumentów i zweryfikuj to odczytem zwrotnym, zamiast przycinać „na oko”.
fontconfig to resolver plus fc-list do debugowania. fonts-dejavu-core duplikuje to, co już zawiera JRE, co jest zamierzone: utrzymuje obraz w zgodzie, jeśli obraz bazowy się zmieni. fonts-liberation ma znaczenie, ponieważ dokumenty tworzone w Windows odwołują się do Arial i Times New Roman po nazwie i oczekują renderowania zgodnego metrycznie. fonts-noto-cjk to ten, którego potrzebował opisany wyżej incydent.
Rozwiązywanie rodziny zamiast nazywania jednej
Provisioning sam w sobie nie wystarczy, ponieważ kod nadal musi podać nazwę istniejącej rodziny. Przenośny sposób to zapytać bibliotekę: spróbować jednorazowego podpisu dla każdego kandydata, zachować pierwszy, który nie rzuci wyjątku.
for (String candidate : candidates) {
if (tryFamily(sourcePath, candidate) == null) {
return candidate;
}
}
return null;
Sam sondowanie to zwykłe wywołanie podpisu w katalogu tymczasowym, przy czym niepowodzenie jest konwertowane na wartość zamiast wyjątku:
SignatureFont font = new SignatureFont();
font.setFamilyName(familyName);
font.setSize(10);
options.setFont(font);
signature.sign(scratch.getAbsolutePath(), options);
return null;
Wykrywanie nazwy pliku to skrót, który wydaje się równoważny, a nie jest. Pakiet Debian fonts-noto-cjk instaluję NotoSansCJK-Regular.ttc, którego nazwa rodziny to Noto Sans CJK JP, więc dopasowywanie nazw plików zarówno pomija czcionki, jak i zgłasza rodziny, które nie zostaną rozwiązane.
Uczciwe degradowanie
Po wprowadzeniu rozwiązywania, dwie klasy niepowodzeń rozdzielają się czysto. Brak rodziny łacińskiej oznacza, że obraz nie może w ogóle podpisywać, co powinno zatrzymać kontener. Brak rodziny CJK oznacza, że jeden podpis jest pomijany i uruchomienie kontynuuje się z ostrzeżeniem:
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);
Rozróżnienie ma znaczenie operacyjne. Kontener, który kończy działanie przy starcie z komunikatem „no usable font family”, to problem wdrożeniowy, wykryty przez osobę, która go wdrożyła. Podpis, który cicho brakuje w dostarczonym dokumencie, to problem zgodności, wykryty przez odbiorcę. Powiązanie fatalnego przypadku z niezerowym kodem wyjścia utrzymuje awarie w pierwszej kategorii.
Następnie odczytaj wynik, ponieważ podpis CJK zapisany bez pokrycia CJK może renderować się jako puste kwadraty bez podnoszenia żadnego błędu:
TextSearchOptions options = new TextSearchOptions();
options.setAllPages(true);
List<TextSignature> found = signature.search(TextSignature.class, options);
Gdzie to zostawia zespół, który już wypuścił produkt?
Dodaj warstwę czcionek, dodaj rozwiązywanie przy starcie i zaloguj oba wyniki w pierwszej linii usługi, gdzie kolejny inżynier faktycznie je zobaczy. Zmiana to edycja Dockerfile plus około trzydzieści linii, a przekształca incydent wywołany przez klienta w kontener, który albo startuje ze znanym pokryciem, albo odmawia uruchomienia. Istniejące podpisane dokumenty pozostają niezmienione; tylko nowe zyskują ścieżkę CJK.
Sprawdzanie obrazu, który już uruchamiasz
Zanim cokolwiek zmienisz, warto wiedzieć, co aktualnie zawiera Twój obraz. Dwa polecenia odpowiadają na to z zewnątrz:
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"
Pierwsze wymienia pliki czcionek, drugie wypisuje nazwy rodzin, które zwróciłby resolver, a luka między nimi jest powodem, dla którego dopasowywanie nazw plików zawodzi. Jeśli fc-list jest nieobecny, to już odpowiedź: fontconfig nie jest zainstalowany i każde wyszukiwanie rodziny odbywa się na ślepo.
Wewnątrz usługi równoważne sprawdzenie powinno znajdować się w logu startowym obok rozwiązanych rodzin. Linia fonts on disk: 8, latin: DejaVu Sans, cjk: (none) informuje następną osobę dokładnie, co ten kontener może, a czego nie może podpisać, co jest bardziej użyteczne niż jakikolwiek wyjątek, który mogliby przeczytać o trzeciej nad ranem.
Szczegół JVM, którego nikt się nie spodziewa
Jeszcze jedna rzecz, która gryzie specyficznie Javę, i nie dotyczy czcionek. Artefakt Maven GroupDocs jest podpisanym „fat jar”. Przebudowanie go do „shaded jar” powoduje NoClassDefFoundError: com/groupdocs/signature/options/search/SearchOptions, a zwykłe usunięcie META-INF/*.SF|RSA|DSA jest niewystarczające: MANIFEST.MF zawiera 19 MB sum kontrolnych per‑entry i musi być także przycięty do sekcji głównej. Przykład unika problemu, uruchamiając się przeciwko zwykłej ścieżce klas z katalogiem dependency/ zamiast cieniowania czegokolwiek.
Wspominam o tym, ponieważ oba te problemy – częściowe pokrycie czcionek i podpisany jar – mają wspólny kształt: ścieżka JVM zawodzi w sposób, który wygląda jak błąd Twojego kodu, a nie jest. Oba są też tanie do obrony, gdy zostaną nazwane: ustal układ classpath, który wiesz, że działa, i asertywnie sprawdzaj pokrycie czcionek przy starcie zamiast ufać obrazowi bazowemu. Żaden nie wymaga przeprojektowania, a oba usuwają klasę incydentów, które inaczej są nieodróżnialne od błędu aplikacji.
Wnioski
Usługa podpisywania w Javie w kontenerze jest o jedną warstwę Dockerfile i jeden kontrolny test przy starcie od przewidywalności. Zainstaluj fontconfig, DejaVu, Liberation i Noto CJK; rozwiąż rodzinę przez sondowanie, a nie przez założenia; pomijaj to, co nie może być osadzone; weryfikuj odczytem zwrotnym. Repozytorium przykładu dostarcza oba obrazy, więc różnica między pokryciem a brakiem pokrycia wymaga dwóch buildów, a nie jednego incydentu, aby się ujawnić.