背景:
之前做過一些pdf導出, 客戶提了一個特別急的需求, 要求根據一個模版跟一個csv的數據源, 批量生成PDF, 因為之前用過FOP, 知道調整樣式需要特別長的時間, 這個需求又特別急, 所以尋找了一個其他的方案。
優點:
生成快捷,代碼簡單, 樣式依賴模版,所見即所得
缺點:
模版難以調整
思路:
既然已經放棄FOP,那么就直接從模版生成新的word文檔, 并且將word文檔直接導出
第一版思路:
<dependency><groupId>org.docx4j</groupId><artifactId>docx4j-core</artifactId><version>8.3.9</version></dependency> <!-- :contentReference[oaicite:0]{index=0} --><!-- 內置 MOXy JAXB 實現 --><dependency><groupId>org.docx4j</groupId><artifactId>docx4j-JAXB-MOXy</artifactId><version>8.3.9</version></dependency> <!-- :contentReference[oaicite:1]{index=1} --><!-- <!– FO 導出,用于生成 XSL-FO –>--><dependency><groupId>org.docx4j</groupId><artifactId>docx4j-export-fo</artifactId><version>8.3.9</version></dependency> <!-- :contentReference[oaicite:2]{index=2} -->public static void main(String[] args) throws Exception {// 1. 加載模板InputStream tpl = Word2PDF.class.getResourceAsStream("/template.docx");if (tpl == null) {throw new RuntimeException("未找到模板 template.docx");}//這部分非必須, 是為了多次導出,不重復讀模版byte[] template = tpl.readAllBytes();// 2. 準備多條替換數據List<Map<String,String>> dataList = new ArrayList<>();Map<String,String> maps = new HashMap<>();maps.put("firstName","Alice");maps.put("context","測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測");maps.put("lastName","Wang");maps.put("date","2025-05-20");dataList.add(maps);maps = new HashMap<>();maps.put("firstName","Bob");maps.put("lastName","Li");maps.put("date","2025-05-21");dataList.add(maps);maps = new HashMap<>();maps.put("firstName","Carol");maps.put("lastName","Zhang");maps.put("date","2025-05-22");dataList.add(maps);// 3. 循環生成for (Map<String,String> row : dataList) {// 3.1 重新加載模板WordprocessingMLPackage pkg;try (InputStream tplStream = new ByteArrayInputStream(template)) {pkg = WordprocessingMLPackage.load(tplStream);}// 3.2 執行替換 (${key})MainDocumentPart mdp = pkg.getMainDocumentPart();mdp.variableReplace(row);// 替換 ${firstName}、${lastName}、${date} :contentReference[oaicite:2]{index=2}// 3.3 保存為 DOCXString name = row.get("firstName");String docxPath = "/Users/Documents/" + name + ".docx";pkg.save(new File(docxPath));try(OutputStream os = new FileOutputStream("/Users/Documents/" + name + ".pdf")) {Docx4J.toPDF(pkg, os);}}}
這種方式全部依賴docx4j的jar包,進行導出。?
缺點, 當模版有復雜模型,比如側邊欄時這種方式是無法導出的, 在網上找到的解決方案也是無效的。可能是因為JDK版本的升級。
版本2:
上面代碼的邏輯一樣,額外使用了documents4j的jar
<dependency><groupId>org.docx4j</groupId><artifactId>docx4j-core</artifactId><version>8.3.9</version></dependency> <!-- :contentReference[oaicite:0]{index=0} --><!-- 內置 MOXy JAXB 實現 --><dependency><groupId>org.docx4j</groupId><artifactId>docx4j-JAXB-MOXy</artifactId><version>8.3.9</version></dependency> <!-- :contentReference[oaicite:1]{index=1} --><!-- <!– FO 導出,用于生成 XSL-FO –>--><dependency><groupId>org.docx4j</groupId><artifactId>docx4j-export-fo</artifactId><version>8.3.9</version></dependency> <!-- :contentReference[oaicite:2]{index=2} -->//轉化為PDF的代碼使用//聲明轉換器,可重用
IConverter converter = LocalConverter.builder().baseFolder(new File(targetPath)).workerPool(5,15,30, TimeUnit.SECONDS).processTimeout (60, TimeUnit.SECONDS).build();
//聲明 轉換, 最后一步有schedule excute 兩種寫法, excute是直接生成,結果是boolean,是單條生成的,這種是為了批量運行
Future<Boolean> future = converter.convert(word).as(DocumentType.MS_WORD).to(new File(wordName + ".pdf")).as(DocumentType.PDF).schedule();//對應 schedule的運行
future.get();
這種方式可以達成所見即所得。
PS:
之前提出了模版難以修改,是因為模版中要使用${替換名稱}的方式, 但是word有時會自動截斷一個字符串, 導致實際上變成了${替 換名稱? }的樣式, 需要多改幾次試下,連續輸入試一下。
有一種比較簡單的方式,就是將word文件的后綴名改成zip ,然后拿出document.xml 可以在這個里面直接改,名稱改回后記得打開看是否報錯, 如果報錯,另存一下,就可以去掉報錯。