Java生成PDF的若干坑
轉(zhuǎn)自知乎@鞠騫:
https://zhuanlan.zhihu.com/p/34519204

知乎搜索“開源 PDF 嵌入字體 Java”的結(jié)果
本文首發(fā)于個人微信公眾號《andyqian》,期待你的關(guān)注~
前言
在工作中,有使用Java生成大量的電子憑證,也就是PDF。在之前的文章中,有寫過,如何通過Java生成PDF。這里就不再描述,寫這篇文章主要是為了記錄我在使用Java生成PDF過程中犯過的錯,以及踩過的坑。
內(nèi)存溢出
之前在使用生成方案的時候,我們一開始采用的是《PD4ML》框架,這個框架相對來說,要小巧些。但是不足的是:它是閉源的。在批量生成PDF文件時會出現(xiàn)內(nèi)存泄漏的現(xiàn)象。每每我們在分析內(nèi)存原因時,都止步于下面這一段代碼:
?private void createPdfByHtml(String html, File file) throws Exception{ ? ? ? ?FileOutputStream fos = new FileOutputStream(file); ? ? ? ? ?... ? ? ? ?// 這一行 ? ? ? ?pd4ml.render(sr, fos, new URL("http://"), "UTF-8"); ? ? ?}
因此,我們不得不考慮換一個第三方庫。目前我們使用的是開源庫《iText》,到目前為止,穩(wěn)定運(yùn)行。
(我們在選擇第三方框架時,盡量選擇開源的,因為在出現(xiàn)問題后,我們可以通過查看源代碼分析,甚至可以修改源代碼進(jìn)行優(yōu)化)。
字體路徑
這個問題是這樣的,我們生成PDF時為了支持中文字體,需要通過路徑加載指定的字體文件。而開發(fā)環(huán)境是windows,測試/生產(chǎn)環(huán)境均是Linux。字體放在
/resources/fonts/
因為:
在Window下獲取Thread.currentThread()為null值
在Linux下獲取this.class.getClass().getResource(“/“).getPath();獲取為null值
所以在加載路徑時,我們通過下述方法進(jìn)行了兼容處理。
/** ? ? * 應(yīng)用場景: ? ? * 1.在windows下,使用Thread.currentThread()獲取路徑時,出現(xiàn)空對象,導(dǎo)致不能使用 ? ? * 2.在linux下,使用PdfUtils.class獲取路徑為null, ? ? * 獲取字體路徑 ? ? * @return ? ? */ ? ?private static String getFontPath(){ ? ? ? ?String path=""; ? ? ? ?// 1. 生產(chǎn)環(huán)境路徑 ? ? ? ?ClassLoader classLoader= Thread.currentThread().getContextClassLoader(); ? ? ? ?URL url = (classLoader==null)?null:classLoader.getResource("/"); ? ? ? ?String threadCurrentPath = (url==null)?"":url.getPath(); ? ? ? ?// 2. 如果線程獲取為null,則使用當(dāng)前PdfUtils.class加載路徑 ? ? ? ?if(StringUtil.isBlank(threadCurrentPath)){ ? ? ? ? ? ? path = PdfUtils.class.getClass().getResource("/").getPath(); ? ? ? ?} ? ? ? ?// 3.拼接字體路徑 ? ? ? ?StringBuffer stringBuffer = new StringBuffer(path); ? ? ? ?stringBuffer.append("/fonts/SIMKAI.TTF"); ? ? ? ?path = stringBuffer.toString(); ? ? ? ?logger.info("getFontPath threadCurrentPath: {} ?path: {}",threadCurrentPath,path); ? ? ? ?return path; ? ?}
從這個方面也能體現(xiàn)一個程序員常見現(xiàn)象。
為什么在我電腦上是好的,在你電腦上不行,一定是你電腦問題。
不應(yīng)該啊,在我電腦上是好的。絕對沒問題。
小伙伴們知道這個梗嗎? 這也是很多程序員使用與生產(chǎn)環(huán)境一致的系統(tǒng)作為開發(fā)機(jī)的原因。
字符編碼
問題是這樣的。是由于在生成PDF文件時轉(zhuǎn)換字節(jié)流時沒有指定編碼導(dǎo)致的: 如下所示:
public static void createdPdfByItextHtml(String htmlContent,File file){ ? ? ? ? ? ?... ? ? ? ? ? ?try { ? ? ? ? ? ? ? ?inputStream= new ByteArrayInputStream(htmlContent.getBytes()); ? ? ? ? ? ? ? ?outputStream = new FileOutputStream(file); ? ? ? ? ? ?...
此處省略掉部分代碼,此時htmlContent.getBytes()
該方法沒有指定默認(rèn)的編碼導(dǎo)致的。具體的原因,在《初探JDK源碼之默認(rèn)字符集》這篇文章中有詳細(xì)的描述。
生僻字
這個坑是這樣的,因為正式公文的字體是:Kaiti_GB2312
的字體。我們使用過一段時間發(fā)現(xiàn),Kaiti_GB2312
字體對于生僻字支持不友好。
例如:
例如: 陳垚,通過Kaiti_GB2312生成后就只能顯示為: 陳
其中垚
字就沒有正常顯示。這個問題是非常嚴(yán)重的。如果電子憑證中姓名錯誤,也就代表,這份電子憑證是沒有法律效應(yīng)的。在經(jīng)過測試后,我們最終使用了Kaiti
字體,代替了Kaiti_GB2312
。(這個坑,還真的不容易發(fā)現(xiàn))
5. 枚舉傳遞類型
如果Dubbo接口中使用枚舉作為參數(shù)時,如果只有單方面更新枚舉的值。會導(dǎo)致序列化出錯的問題。也就是說:
調(diào)用方在更新枚舉后,如果接受方不同時更新枚舉,會導(dǎo)致接收方的參數(shù)為空對象。
注意點:
Dubbo服務(wù)調(diào)用之間,盡量不要使用enum類型。
嚴(yán)格的HTML語義標(biāo)簽
使用過itext
的朋友應(yīng)該知道。HTML內(nèi)容是需要嚴(yán)格的標(biāo)簽的。也就是說,一個開始標(biāo)簽,對應(yīng)一個結(jié)束標(biāo)簽。如果不匹配,則會生成失敗。(像<img src="www.baidu.com"/>
) 標(biāo)簽除外。
錯誤的html:
<html> ?<head> ? ? <title></title> ?</head> ?<body> ? ?<span>Hello World!</span></span> ?</body> </html>
上面這段html內(nèi)容,就多了一個</span>
結(jié)束標(biāo)簽,就屬于非嚴(yán)格的HTM語義標(biāo)簽。
執(zhí)行后,就會有下面這類錯誤:
com.itextpdf.tool.xml.exceptions.RuntimeWorkerException: Invalid nested tag span found, expected closing tag body. ? ?at com.itextpdf.tool.xml.XMLWorker.endElement(XMLWorker.java:134) ? ?at com.itextpdf.tool.xml.parser.XMLParser.endElement(XMLParser.java:396)
最后
有些系統(tǒng),看起來容易。做起來可并非易事。在實現(xiàn)期間可能會遇到各種各樣的奇葩問題。但恰恰,就是這些怪問題。給我們積累了經(jīng)驗。我一直覺得,在寫代碼方面,是一個“倒霉”的人,因為各種奇怪的問題,都能讓我遇到。
公眾號內(nèi)回復(fù):【PDF】,即可獲取項目。記得更換字體哦!
相關(guān)閱讀:
《說說面試那些事》
《Java PDF生成方案之iText》
《一個Java細(xì)節(jié)!》
《記一個有趣的Java OOM!》