💡 דוגמה מלאה עובדת זמינה ב‑GitHub:
sign-documents-in-docker-fonts-java
שירות החתימה על חוזים שעבד במשך תשעה חודשים
הקצאת גופנים במכולה היא הצעד שמחליט האם שירות החתימה ב‑Java יעבוד בייצור או רק בבדיקות שכתבת בטעות. זה חשוב מכיוון שהכשל מתוזמן: תמונת JRE מספקת לך כיסוי גופנים מספיק כדי להיראות נכונה, ואז מחזיקה את השאר עד שמסמך ספציפי מגיע.
חשוב להבין את המצב. זרימת עבודה של מסמכים חותמת על חוזים, מופעלת על eclipse-temurin:17-jre, והיא עובדת. אחרי תשעה חודשים, החברה חותמת על הלקוח הראשון שלה ביפן, השם נכנס לטקסט החתימה, והעבודה נכשלת עם Specified font file was not found. שום דבר לא השתנה בשירות. לתמונה מעולם לא היה כיסוי CJK; אף מסמך לא ביקש זאת.
הסיבה הטכנית קצרה. eclipse-temurin:17-jre כוללת 8 קבצי גופן DejaVu עבור AWT, המכסים לטינית, יוונית וקירילית. GroupDocs.Signature אינו מחליף משפחה חסרה, ולכן בקשה לגופן תומך ביפנית נכשלת במקום להתדרדר, והשארת הגופן ללא ערך אינה עוזרת מכיוון שהספרייה לאחר מכן מבקשת Times New Roman, שגם הוא חסר.
למה זה גרוע יותר מתמונת ללא גופנים
תמונות הבסיס של .NET ו‑Python משגררות אפס גופנים. זה כשל טוב יותר: החתימה הראשונה נכשלת, בריצה הראשונה של הבדיקה, ומישהו מתקן זאת לפני שהשירות נשלח.
תמונת JVM נכשלת חלקית, וזה הגרסה היקרה. הבאג נמצא בקוד שכבר נמצא בייצור, הוא מופעל על ידי נתוני לקוח ולא על ידי משהו בפריסה, והאדם במענה רואה שגיאת גופן משירות שמישהו לא נגע בו חודשים. עלות האירוע איננה בתיקון – התיקון הוא שכבת Dockerfile אחת – אלא בשעה שלפני שמישהו מאמין שהגופנים מעורבים.
האסימטריה הזו היא הטיעון לטפל בכיסוי גופנים כמשהו שאתה מאמת בזמן האתחול ולא כמשהו שאתה מגלה.
זה גם משנה מי משלם. תמונה ללא גופנים עולה למפתח עשרים דקות במהלך ההגדרה. תמונה עם כיסוי חלקי עולה למהנדס במענה שעה בזמן לא נוח, ועוד מה שהחוזה המושהה היה שווה, ועוד הסקירה שמגיעה אחרי אירוע שאף אחד לא יכול לייחס לשינוי. ההבדל הטכני בין השניים הוא ארבעה חבילות ב‑Dockerfile.
מה הקצאה באמת עולה
ארבע חבילות Debian שלב זמן הריצה:
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/*
גודל התמונה הוא ההתנגדות הרגילה, וכדאי להיות מדויק: חבילת CJK היא הגדולה, שלוש החבילות האחרות קטנות, ואין מהן אופציונלי אם המסמכים שלך יכולים לכלול שמות שאינם לטיניים. התקן רק את מה שהקבוצה שלך של מסמכים באמת צריכה וודא זאת באמצעות קריאה חוזרת במקום לחתוך על אינסטינקט.
fontconfig הוא הפותר בנוסף ל‑fc-list לצורך ניפוי. fonts-dejavu-core משכפלת מה שכבר כלול ב‑JRE, וזה מכוון: זה שומר על האמת של התמונה אם תמונת הבסיס משתנה. fonts-liberation חשוב מכיוון שמסמכים שנוצרו ב‑Windows מתייחסים ל‑Arial ול‑Times New Roman בשם ומצפים לרינדור תואם מבחינת מדדים. fonts-noto-cjk היא זו שהאירוע שלמעלה דרש.
מציאת משפחה במקום שם ספציפי
הקצאה לבדה אינה מספיקה, מכיוון שהקוד עדיין צריך לציין משפחה קיימת. הדרך הניידת היא לשאול את הספרייה: לנסות חתימה זמנית לכל מועמד, לשמור על הראשונה שלא זורקת חריגה.
for (String candidate : candidates) {
if (tryFamily(sourcePath, candidate) == null) {
return candidate;
}
}
return null;
הבדיקה עצמה היא קריאת חתימה רגילה לתיקייה זמנית, כאשר הכשל מומר לערך במקום לחריגה:
SignatureFont font = new SignatureFont();
font.setFamilyName(familyName);
font.setSize(10);
options.setFont(font);
signature.sign(scratch.getAbsolutePath(), options);
return null;
זיהוי שם קובץ הוא הקיצור שנראה שווה ערך אך אינו. fonts-noto-cjk של Debian מתקינה NotoSansCJK-Regular.ttc, שהשם המשפחתי שלו הוא Noto Sans CJK JP, ולכן התאמת שמות קבצים משאירה גם גופנים וגם מדווחת על משפחות שלא ייפתרו.
דעיכה בכנות
עם פתרון במקום, שתי קטגוריות הכשל נפרדות בבירור. חוסר משפחה לטינית אומר שהתמונה לא יכולה לחתום כלל, וזה צריך לעצור את המכולה. חוסר משפחה CJK אומר שחתימה אחת מדולגת והריצה ממשיכה עם אזהרה:
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);
ההבדל חשוב תפעולית. מכולה שיוצאת באתחול עם “no usable font family” היא בעיית פריסה, שנתפסת על ידי מי שהכניס אותה. חתימה חסרה בשקט ממסמך שהועבר היא בעיית ציות, שנתפסת על ידי המקבל. חיבור המקרה הקטלני ליציאה עם קוד שונה מאפס משאיר כשלים בקטגוריה הראשונה.
לאחר מכן קרא את התוצאה חזרה, מכיוון שחתימה CJK שנכתבה ללא כיסוי CJK יכולה להיראות כקופסאות ריקות מבלי לזרוק שום דבר:
TextSearchOptions options = new TextSearchOptions();
options.setAllPages(true);
List<TextSignature> found = signature.search(TextSignature.class, options);
איפה זה משאיר צוות שכבר שחרר?
הוסף את שכבת הגופנים, הוסף פתרון באתחול, ורשום את שני התוצאות בשורה הראשונה של השירות, שם המהנדס הבא יראה אותן בפועל. השינוי הוא עריכת Dockerfile של כ‑שלושים שורות, והוא ממיר אירוע שמופעל על ידי לקוח למכולה שמתחילה עם כיסוי ידוע או מסרבת להתחיל. מסמכים חתומים קיימים אינם מושפעים; רק חדשים מקבלים את נתיב ה‑CJK.
בדיקת תמונה שכבר מריצים
לפני שינוי כלשהו, כדאי לדעת מה יש בתמונה הנוכחית שלך. שני פקודות עונות על כך מבחוץ:
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"
הראשונה מציגה קבצי גופנים, השנייה מציגה את שמות המשפחות שהפותר היה מחזיר, והפער ביניהם הוא הסיבה שההתאמה לפי שם קובץ נכשלת. אם fc-list חסר, זו תשובה בפני עצמה: fontconfig לא מותקן, וכל חיפוש משפחה מתבצע בעיניים עצומות.
בתוך השירות, הבדיקה המקבילה שייכת ללוג האתחול ליד המשפחות שנפתרו. שורה שמציגה fonts on disk: 8, latin: DejaVu Sans, cjk: (none) אומרת לאדם הבא בדיוק מה המכולה הזו יכולה ולא יכולה לחתום, וזה יותר שימושי מכל חריגה שהם היו קוראים בשלוש לפנות בוקר.
פרטי ה‑JVM שאף אחד לא מצפה להם
דבר נוסף שמציק ספציפית ב‑Java, וזה לא קשור לגופנים. ארטיפקט Maven של GroupDocs הוא JAR שמן חתום. אריזתו מחדש ל‑JAR מוצלל מייצרת NoClassDefFoundError: com/groupdocs/signature/options/search/SearchOptions, והפתרון הרגיל של מחיקת META-INF/*.SF|RSA|DSA אינו מספיק: MANIFEST.MF מכיל כ‑19 MB של חותמות לכל קובץ ויש לקצץ אותו גם הוא לחלק הראשי. הדוגמה נמנעת מהבעיה על‑ידי ריצה נגד classpath רגיל עם תיקיית dependency/ במקום הצללה של משהו.
אני מזכיר זאת מכיוון ששניהם – כיסוי גופנים חלקי וה‑JAR החתום – חולקים צורה: נתיב ה‑JVM נכשל בצורה שנראית כמו הקוד שלך ואינה. שניהם גם זולים להגנה ברגע שמזהים אותם: קבע את מבנה ה‑classpath שאתה יודע שעובד, ואמת כיסוי גופנים באתחול במקום לסמוך על תמונת הבסיס. אף אחד מהם אינו דורש עיצוב מחדש, ושניהם מסירים מחלקת תקריות שלא ניתנת להבחנה מבאג אפליקציה.
סיכום
שירות חתימה ב‑Java במכולה הוא שכבת Dockerfile אחת ובדיקה אחת באתחול משם למצב צפוי. התקן fontconfig, DejaVu, Liberation ו‑Noto CJK; פתר את המשפחה על‑ידי חישה במקום הנחה; דלג על מה שלא ניתן לשבץ; אמת על‑ידי קריאה חוזרת. מאגר הדוגמה משגר שני תמונות, ולכן ההבדל בין כיסוי ללא כיסוי נצפה אחרי שני בניות במקום אחרי תקרית אחת ללמידה.