1,最近有個需求,動態生成 Word 文當並供前端下載,網上找了一下,發現基本都是用 word 生成 xml 然后用模板替換變量的方式
1.1,這種方式雖然可行,但是生成的 xml 是在是太亂了,整理就得整理半天,而且一旦要修改模板,那簡直就是災難,而且據說還不兼容 WPS
1.2,所以筆者找到了以下可以直接用 word 文檔作為模板的方法,這里做以下筆記,以下代碼依賴於 JDK8 以上
2,pom.xml 相應依賴
<!-- 文檔模板操作依賴 --> <dependency> <groupId>fr.opensagres.xdocreport</groupId> <artifactId>fr.opensagres.xdocreport.document.docx</artifactId> <version>2.0.1</version> </dependency> <dependency> <groupId>fr.opensagres.xdocreport</groupId> <artifactId>fr.opensagres.xdocreport.template.freemarker</artifactId> <version>2.0.1</version> </dependency>
3,使用該模板的操作主要是 IXDocReport 和 IContext 對象,封裝兩個工具類來對他們進行獲取和操作
3.1,存放和設置插入到模板中的數據的模型類 ExportData,設置一般數據或者循環集合的時候比較簡單,直接用 IContent 的 put(key,value)即可
但是設置 表格循環數據和圖片等特殊數據就比較麻煩了,詳情看下面 setTable 和 setImg
package com.hwq.utils.export; import com.hwq.utils.model.SoMap; import fr.opensagres.xdocreport.document.IXDocReport; import fr.opensagres.xdocreport.document.images.ByteArrayImageProvider; import fr.opensagres.xdocreport.document.images.IImageProvider; import fr.opensagres.xdocreport.template.IContext; import fr.opensagres.xdocreport.template.formatter.FieldsMetadata; import org.springframework.core.io.ClassPathResource; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.List; public class ExportData { private IXDocReport report; private IContext context; /** * 構造方法 * @param report * @param context */ public ExportData(IXDocReport report, IContext context) { this.report = report; this.context = context; } /** * 設置普通數據,包括基礎數據類型,數組,試題對象 * 使用時,直接 ${key.k} 或者 [#list d as key] * @param key 健 * @param value 值 */ public void setData(String key, Object value) { context.put(key, value); } /** * 設置表格數據,用來循環生成表格的 List 數據 * 使用時,直接 ${key.k} * @param key 健 * @param value List 集合 */ public void setTable(String key, List<SoMap> maps) { FieldsMetadata metadata = report.getFieldsMetadata(); metadata = metadata == null ? new FieldsMetadata() : metadata; SoMap map = maps.get(0); for (String kk : map.keySet()) { metadata.addFieldAsList(key + "." + kk); } report.setFieldsMetadata(metadata); context.put(key, maps); } /** * 設置圖片數據 * 使用時 直接在書簽出 key * @param key 健 * @param url 圖片地址 */ public void setImg(String key, String url) { FieldsMetadata metadata = report.getFieldsMetadata(); metadata = metadata == null ? new FieldsMetadata() : metadata; metadata.addFieldAsImage(key); report.setFieldsMetadata(metadata); try ( InputStream in = new ClassPathResource(url).getInputStream(); ) { IImageProvider img = new ByteArrayImageProvider(in); context.put(key, img); } catch (IOException ex) { throw new RuntimeException(ex.getMessage()); } } /** * 獲取文件流數據 * @return 文件流數組 */ public byte[] getByteArr() { try ( ByteArrayOutputStream out = new ByteArrayOutputStream(); ) { report.process(context, out); return out.toByteArray(); } catch (Exception ex) { ex.printStackTrace(); throw new RuntimeException(ex.getMessage()); } } }
3.2,生成 IXDocReport 和 IContext 的工具類
package com.hwq.utils.export; import fr.opensagres.xdocreport.core.XDocReportException; import fr.opensagres.xdocreport.document.IXDocReport; import fr.opensagres.xdocreport.document.registry.XDocReportRegistry; import fr.opensagres.xdocreport.template.IContext; import fr.opensagres.xdocreport.template.TemplateEngineKind; import org.springframework.core.io.ClassPathResource; import java.io.ByteArrayOutputStream; import java.io.InputStream; public class WordUtil { /** * 獲取 Word 模板的兩個操作對象 IXDocReport 和 IContext * @param url 模板相對於類路徑的地址 * @return 模板數據對象 */ public static ExportData createExportData(String url) { try { ByteArrayOutputStream out = new ByteArrayOutputStream(); IXDocReport report = createReport(url); IContext context = report.createContext(); return new ExportData(report, context); } catch (XDocReportException ex) { throw new RuntimeException(ex.getMessage()); } } /** * 加載模板的方法,主要是指定模板的路徑和選擇渲染數據的模板 * @param url 模板相對於類路徑的地址 * @return word 文檔操作類 */ private static IXDocReport createReport(String url) { try ( InputStream in = new ClassPathResource(url).getInputStream(); ) { IXDocReport ix = XDocReportRegistry.getRegistry().loadReport(in, TemplateEngineKind.Freemarker); return ix; } catch (Exception ex) { throw new RuntimeException(ex.getMessage()); } } }
4,讓我們編輯一個 word 模板,方到資源路徑下的 export 目錄下, 全路徑為 export/template.docx 內容如下
4.1,我們可以發現上面的模板有些數據的兩端有兩個尖括號,就是我們需要替換數據的地方,插入方式如下
4.2,打開 word 文檔,光標選中需要替換的位置 如上圖的 1 號位 =》 Ctrl + F9 生成域 =》右鍵點擊 =》選擇編輯域 =》選擇郵件合並 =》加上變量 ${model.order}
4.3,依次如下,注意輸入變量的時候不要動 MERGEFIELD 這個單詞,在他的后面空一格輸入
4.4,IF 判斷的寫法,需要三個域,每一個的創建方式和上面相同 內容為 "[#if 1 == 1]" 文檔內容 " [#else]" 文檔內容 " [/#if]" , 注意要加中括號,兩端最好在加上引號
4.5,循環的寫法 [#list list as item] [/#list] 依然是要注意兩端的中括號,最好兩端在加引號括起來
4.6,圖片的插入方式和上面的不太相同,首先我們點擊圖片,選擇插入,選擇書簽,輸入一個任意的變量名如 img
4.7,這樣我們就編輯了一個包含了多種元素的 word 文檔,需要注意的點是 域的 內容必須在 右鍵 編輯域 郵件合並 處填寫,不要直接修改,否則無效
4.8,圖片的比列最好不要調整,否則替換的圖片可能會失真等,可以調大小,但是比列不要改
5,接下來我們測試一下,首先創建一個 SpringBoot 項目
5.1 創建數據模型類 UserModel(依賴於 lombok)
package com.hwq.doc.export.model; import lombok.Getter; import lombok.Setter; @Getter @Setter public class UserModel { private Integer order; private String code; private String name; }
5.2,創建業務邏輯類 UserService
package com.hwq.doc.export.service; import com.hwq.doc.export.model.UserModel; import com.hwq.utils.export.ExportData; import com.hwq.utils.export.WordUtil; import com.hwq.utils.model.SoMap; import org.springframework.stereotype.Service; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.UUID; @Service public class UserService { private final static String rootPath = "E:/text/file/"; // 保存文件的地址 public byte[] downWord() { // 准備數據 List<SoMap> list = new ArrayList<SoMap>(); UserModel user0 = new UserModel(); UserModel user1 = new UserModel(); UserModel user2 = new UserModel(); user0.setOrder(1); user0.setCode("00300.SS"); user0.setName("愛誰誰"); user1.setOrder(2); user1.setCode("00300.SS"); user1.setName("愛誰誰"); user2.setOrder(3); user2.setCode("00300.SS"); user2.setName("愛誰誰"); list.add(new SoMap(user0)); list.add(new SoMap(user1)); list.add(new SoMap(user2)); // 向模板中插入值 ExportData evaluation = WordUtil.createExportData("export/template.docx"); evaluation.setData("model", user0); evaluation.setData("list", list); evaluation.setTable("table", list); evaluation.setImg("img", "export/coney.png"); // 獲取新生成的文件流 byte[] data = evaluation.getByteArr(); // 可以直接寫入本地的文件 String fileName = rootPath + UUID.randomUUID().toString().replaceAll("-", "") + ".docx"; try ( FileOutputStream fos = new FileOutputStream(fileName); ) { fos.write(data, 0, data.length); } catch (IOException ex) { throw new RuntimeException(ex.getMessage()); } return data; } }
5.3,創建控制器 Usercontroller
package com.hwq.doc.export.controller; import com.hwq.doc.export.service.UserService; import com.hwq.utils.http.ResUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @RequestMapping("/word") public Object getTemplate(HttpServletRequest request) { byte[] data = userService.downWord(); return ResUtil.getStreamData(request, data, "文件下載", "docx"); } }
5.4,以上還用到了我自己封裝的工具類,SoMap 和 ResUtil 如下
package com.hwq.utils.model; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.HashMap; public class SoMap extends HashMap<String, Object> { public SoMap() { } /** * 構造方法,將任意實體類轉化為 Map * @param obj */ public SoMap(Object obj) { Class clazz = obj.getClass(); Field[] fields = clazz.getDeclaredFields(); try { for (Field field : fields) { field.setAccessible(true); this.put(field.getName(), field.get(obj)); } } catch (IllegalAccessException ex) { throw new RuntimeException(ex.getMessage()); } } /** * 將 Map 轉化為 任意實體類 * @param clazz 反射獲取類字節碼對象 * @return */ public <T> T toEntity(Class<T> clazz) { Field[] fields = clazz.getDeclaredFields(); try { Constructor constructor = clazz.getDeclaredConstructor(); T t = (T) constructor.newInstance(); for (Field field : fields) { field.setAccessible(true); field.set(t, this.get(field)); } return t; } catch (Exception ex) { throw new RuntimeException(ex.getMessage()); } } /** * 從集合中獲取一個字段的方法,如果字段不存在返回空 * @param key 字段的唯一標識 * @param <T> 字段的類型,運行時自動識別,使用時無需聲明和強轉 * @return 對應字段的值 */ public <T> T get(String key) { return (T) super.get(key); } }
package com.hwq.utils.http; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import java.io.UnsupportedEncodingException; public class ResUtil { /** * 生成下載文件,瀏覽器直接訪問為下載文件 * @param request 請求對象 * @param data 數據流數組 * @param prefix 下載的文件名 * @param suffix 文件后綴 * @return 瀏覽器可以直接下載的文件流 */ public static ResponseEntity<byte[]> getStreamData( HttpServletRequest request, byte[] data, String prefix, String suffix ) { HttpHeaders headers = new HttpHeaders(); prefix = StringUtils.isEmpty(prefix) ? "未命名" : prefix; suffix = suffix == null ? "" : suffix; try { String agent = request.getHeader("USER-AGENT"); boolean isIE = null != agent, isMC = null != agent; isIE = isIE && (agent.indexOf("MSIE") != -1 || agent.indexOf("Trident") != -1); isMC = isMC && (agent.indexOf("Mozilla") != -1); prefix = isMC ? new String(prefix.getBytes("UTF-8"), "iso-8859-1") : (isIE ? java.net.URLEncoder.encode(prefix, "UTF8") : prefix); headers.setContentDispositionFormData("attachment", prefix + "." + suffix); headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); return new ResponseEntity<byte[]>(data, headers, HttpStatus.OK); } catch (UnsupportedEncodingException ex) { ex.printStackTrace(); throw new RuntimeException(ex.getMessage()); } } }
6,我們把模板和一張圖片存放到項目的資源文件夾下 的 export 下, 圖片是用來替換模板中的圖片的
7,啟動項目,我們訪問上面編寫的控制器,效果如下,一切 OK(注意該種方式對於字段的要求比較嚴苛,只要在模板中編寫的變量一定要設置值,否則拋異常)
8,新版本我們在生成表格數據時,也可以不使用 metadata.addFieldAsList 而在在 list 標簽前面添加 @before-row 和 @after-row,這樣就支持了表格的嵌套循環,如:
9,關於圖片的循環目前好像暫不支持,只支持書簽的方式,期待后續的跟新吧