💡 ตัวอย่างทำงานเต็มที่พร้อมบน GitHub:
sign-documents-in-docker-fonts-java
บริการลงนามสัญญาที่ทำงานได้เป็นเวลาเก้าเดือน
การจัดหาแบบอักษรในคอนเทนเนอร์เป็นขั้นตอนที่กำหนดว่าเซอร์วิสลงนาม Java จะทำงานในสภาพการผลิตหรือเพียงในเทสต์ที่คุณเขียนขึ้นโดยบังเอิญหรือไม่ มันสำคัญเพราะความล้มเหลวถูกกำหนดไว้ล่วงหน้า: ภาพ JRE จะให้คุณครอบคลุมแบบอักษรพอเพียงเพื่อให้ดูถูกต้อง แล้วจึงเก็บส่วนที่เหลือไว้จนกว่าจะมีเอกสารเฉพาะเข้ามา
ลองพิจารณารูปแบบของมัน เวิร์กโฟลว์เอกสารทำการลงนามสัญญา ปรับใช้บน eclipse-temurin:17-jre และทำงานได้ หลังจากทำงานมาเก้าเดือน บริษัทได้ลงนามกับลูกค้ารายแรกในญี่ปุ่น ชื่อของลูกค้าถูกใส่ลงในข้อความลายเซ็น และงานล้มเหลวด้วยข้อความ Specified font file was not found ไม่มีอะไรเปลี่ยนแปลงในเซอร์วิส ภาพนั้นไม่มีการครอบคลุม CJK มาก่อน ไม่มีเอกสารใดขอใช้มัน
สาเหตุทางเทคนิคสั้น ๆ eclipse-temurin:17-jre มีไฟล์แบบอักษร DejaVu 8 ตัวสำหรับ AWT ซึ่งครอบคลุม Latin, Greek และ Cyrillic GroupDocs.Signature ไม่ได้ทำการแทนที่ฟอนต์ที่หายไป ดังนั้นการร้องขอฟอนต์ที่รองรับภาษาญี่ปุ่นจะล้มเหลวแทนที่จะลดระดับลง และการปล่อยให้ฟอนต์ว่างเปล่าไม่ได้ช่วยอะไรเพราะไลบรารีจะพยายามใช้ Times New Roman ซึ่งก็ไม่มีเช่นกัน
ทำไมเรื่องนี้จึงแย่กว่าภาพที่ไม่มีฟอนต์เลย
ภาพฐาน .NET และ Python ส่งมาพร้อมศูนย์ฟอนต์ นี่เป็นความล้มเหลวที่ดีกว่า: ลายเซ็นแรกสุดล้มเหลวในรอบเทสต์แรก และใครสักคนก็แก้ไขได้ก่อนที่เซอร์วิสจะออกสู่การใช้งาน
ภาพ JVM ล้มเหลวแบบบางส่วน ซึ่งเป็นเวอร์ชันที่มีค่าใช้จ่ายสูง บั๊กอยู่ในโค้ดที่กำลังทำงานอยู่แล้วในสภาพการผลิต มันถูกกระตุ้นโดยข้อมูลของลูกค้า แทนที่จะมาจากการตั้งค่าใด ๆ ในการปรับใช้ และผู้ที่อยู่บนสายดูก็เห็นข้อผิดพลาดฟอนต์จากเซอร์วิสที่ไม่มีใครแก้ไขมาหลายเดือน ค่าใช้จ่ายของเหตุการณ์ไม่ใช่การแก้ไข — การแก้ไขเป็นเพียงชั้น Dockerfile หนึ่ง — แต่เป็นชั่วโมงก่อนที่ใครจะเชื่อว่าฟอนต์มีส่วนเกี่ยวข้อง
ความไม่สมดุลนี้เป็นเหตุผลที่เราควรถือว่าการครอบคลุมฟอนต์เป็นสิ่งที่ต้องตรวจสอบตอนเริ่มต้น แทนที่จะค้นพบภายหลัง
มันยังเปลี่ยนผู้ที่ต้องจ่ายค่าใช้จ่ายด้วย ภาพที่ไม่มีฟอนต์ทำให้พัฒนาซอฟต์แวร์เสียเวลา 20 นาทีในขั้นตอนตั้งค่า ภาพที่ครอบคลุมบางส่วนทำให้วิศวกรที่อยู่บนสายต้องเสียเวลาเป็นชั่วโมงในเวลาที่ไม่สะดวก บวกกับมูลค่าของสัญญาที่ล่าช้า และการตรวจสอบที่ตามมาหลังเหตุการณ์ที่ไม่มีใครสามารถอธิบายเป็นการเปลี่ยนแปลง ความแตกต่างทางเทคนิคระหว่างสองกรณีคือสี่แพ็กเกจใน Dockerfile
ค่าใช้จ่ายของการจัดหา
สี่แพ็กเกจ Debian ในขั้นตอน 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/*
ขนาดภาพเป็นข้อโต้แย้งที่มักเจอ และควรระบุให้ชัดเจน: แพ็กเกจ CJK คือส่วนที่ใหญ่ ส่วนสามแพ็กเกจที่เหลือค่อนข้างเล็ก และไม่มีอันไหนเป็นทางเลือกถ้าเอกสารของคุณต้องรองรับชื่อที่ไม่ใช่ Latin ติดตั้งเฉพาะที่ชุดเอกสารของคุณต้องการจริง ๆ แล้วตรวจสอบด้วยการอ่านกลับ แทนการตัดทอนตามสัญชาตญาณ
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;
การตรวจสอบนี้เป็นการเรียก sign ธรรมดาไปยังไดเรกทอรีชั่วคราว โดยแปลงความล้มเหลวเป็นค่าแทนการโยนข้อยกเว้น:
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 ดังนั้นการจับคู่ตามชื่อไฟล์จะพลาดฟอนต์และรายงานฟอนต์ที่ไม่สามารถแก้ได้
การลดระดับอย่างซื่อสัตย์
เมื่อมีการแก้ไขแล้ว ประเภทความล้มเหลวสองประเภทจะแยกออกจากกันอย่างชัดเจน ไม่มีฟอนต์ Latin หมายความว่าภาพไม่สามารถลงนามได้เลย ซึ่งควรทำให้คอนเทนเนอร์หยุดทำงาน ไม่มีฟอนต์ 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” เป็นปัญหาการปรับใช้ที่ถูกจับโดยผู้ที่ทำการปรับใช้ ลายเซ็นที่หายไปโดยเงียบ ๆ จากเอกสารที่ส่งมอบเป็นปัญหาการปฏิบัติตามที่ผู้รับจะจับได้ การทำให้กรณีร้ายแรงออกด้วยค่า exit ไม่เป็นศูนย์ทำให้ความล้มเหลวอยู่ในประเภทแรก
จากนั้นอ่านผลกลับมา เพราะลายเซ็น 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 ที่เซ็นแล้ว การรีแพคเกจเป็น shaded JAR ทำให้เกิด NoClassDefFoundError: com/groupdocs/signature/options/search/SearchOptions และวิธีแก้ปกติคือการลบ META-INF/*.SF|RSA|DSA แต่ไม่พอ: MANIFEST.MF มีดิจิสต์ต่อรายการประมาณ 19 MB และต้องตัดให้เหลือแค่ส่วนหลัก ตัวอย่างหลีกเลี่ยงปัญหานี้โดยรันกับคลาสพาธธรรมดาที่มีไดเรกทอรี dependency/ แทนการทำ shading
ผมพูดถึงเรื่องนี้เพราะทั้งสองประเด็น — การครอบคลุมฟอนต์บางส่วนและ JAR ที่เซ็น — มีรูปแบบคล้ายกัน: เส้นทาง JVM ล้มเหลวในลักษณะที่ดูเหมือนโค้ดของคุณและไม่ใช่ ทั้งสองอย่างก็แก้ได้ง่ายเมื่อระบุชื่อแล้ว: กำหนดโครงสร้างคลาสพาธที่รู้ว่าทำงานได้ และตรวจสอบการครอบคลุมฟอนต์ตอนเริ่มต้นแทนการเชื่อใจภาพฐาน ไม่มีอันไหนต้องออกแบบใหม่ และทั้งสองช่วยขจัดประเภทเหตุการณ์ที่มักจะสับสนกับบั๊กของแอปพลิเคชัน
สรุป
เซอร์วิสลงนาม Java ในคอนเทนเนอร์ต้องการเพียงชั้น Dockerfile หนึ่งและการตรวจสอบตอนเริ่มต้นหนึ่งครั้งเพื่อให้ทำงานได้อย่างคาดการณ์ได้ ติดตั้ง fontconfig, DejaVu, Liberation และ Noto CJK; แก้ไขฟอนต์โดยการตรวจสอบแทนการสันนิษฐาน; ข้ามฟอนต์ที่ไม่สามารถฝังได้; ยืนยันโดยการอ่านกลับ ตัวอย่างรีโพสิตอรีส่งมาพร้อมทั้งสองภาพ ดังนั้นความแตกต่างระหว่างมีและไม่มีการครอบคลุมจะเห็นได้หลังการสร้างสองครั้ง แทนที่จะต้องรอเหตุการณ์หนึ่งครั้งเพื่อเรียนรู้