1,最近有个需求,动态生成 Word 文当并供前端下载,网上找了一下,发现基本都是用 word 生成 xml 然后用模板替换变量的方式
1.1,这种方式虽然可行,但是生成的 xml 是在是太乱了,整理就得整理半天,而且一旦要修改模板,那简直就是灾难,而且据说还不兼容 WPS
1.2,所以笔者找到了以下可以直接用 word 文档作为模板的方法,这里做以下笔记,以下代码依赖于 JDK8 以上
2,pom.xml 相应依赖
fr.opensagres.xdocreport
fr.opensagres.xdocreport.document.docx
2.0.1
fr.opensagres.xdocreport
fr.opensagres.xdocreport.template.freemarker
2.0.1
3,使用该模板的操作主要是IXDocReport 和 IContext 对象,封装两个工具类来对他们进行获取和操作
3.1,存放和设置插入到模板中的数据的模型类 ExportData,设置一般数据或者循环集合的时候比较简单,直接用 IContent 的 put(key,value)即可
但是设置 表格循环数据和图片等特殊数据就比较麻烦了,详情看下面 setTable 和 setImg
packagecom.hwq.utils.export;importcom.hwq.utils.model.SoMap;importfr.opensagres.xdocreport.document.IXDocReport;importfr.opensagres.xdocreport.document.images.ByteArrayImageProvider;importfr.opensagres.xdocreport.document.images.IImageProvider;importfr.opensagres.xdocreport.template.IContext;importfr.opensagres.xdocreport.template.formatter.FieldsMetadata;importorg.springframework.core.io.ClassPathResource;importjava.io.ByteArrayOutputStream;importjava.io.IOException;importjava.io.InputStream;importjava.util.List;public classExportData {privateIXDocReport report;privateIContext context;/*** 构造方法
*@paramreport
*@paramcontext*/
publicExportData(IXDocReport report, IContext context) {this.report =report;this.context =context;
}/*** 设置普通数据,包括基础数据类型,数组,试题对象
* 使用时,直接 ${key.k} 或者 [#list d as key]
*@paramkey 健
*@paramvalue 值*/
public voidsetData(String key, Object value) {
context.put(key, value);
}/*** 设置表格数据,用来循环生成表格的 List 数据
* 使用时,直接 ${key.k}
*@paramkey 健
*@paramvalue List 集合*/
public void setTable(String key, Listmaps) {
FieldsMetadata metadata=report.getFieldsMetadata();
metadata= metadata == null ? newFieldsMetadata() : metadata;
SoMap map= maps.get(0);for(String kk : map.keySet()) {
metadata.addFieldAsList(key+ "." +kk);
}
report.setFieldsMetadata(metadata);
context.put(key, maps);
}/*** 设置图片数据
* 使用时 直接在书签出 key
*@paramkey 健
*@paramurl 图片地址*/
public voidsetImg(String key, String url) {
FieldsMetadata metadata=report.getFieldsMetadata();
metadata= metadata == null ? newFieldsMetadata() : metadata;
metadata.addFieldAsImage(key);
report.setFieldsMetadata(metadata);try(
InputStream in= newClassPathResource(url).getInputStream();
) {
IImageProvider img= newByteArrayImageProvider(in);
context.put(key, img);
}catch(IOException ex) {throw newRuntimeException(ex.getMessage());
}
}/*** 获取文件流数据
*@return文件流数组*/
public byte[] getByteArr() {try(
ByteArrayOutputStream out= newByteArrayOutputStream();
) {
report.process(context, out);returnout.toByteArray();
}catch(Exception ex) {
ex.printStackTrace();throw newRuntimeException(ex.getMessage());
}
}
}
3.2,生成 IXDocReport 和 IContext 的工具类
packagecom.hwq.utils.export;importfr.opensagres.xdocreport.core.XDocReportException;importfr.opensagres.xdocreport.document.IXDocReport;importfr.opensagres.xdocreport.document.registry.XDocReportRegistry;importfr.opensagres.xdocreport.template.IContext;importfr.opensagres.xdocreport.template.TemplateEngineKind;importorg.springframework.core.io.ClassPathResource;importjava.io.ByteArrayOutputStream;importjava.io.InputStream;public classWordUtil {/*** 获取 Word 模板的两个操作对象 IXDocReport 和 IContext
*@paramurl 模板相对于类路径的地址
*@return模板数据对象*/
public staticExportData createExportData(String url) {try{
ByteArrayOutputStream out= newByteArrayOutputStream();
IXDocReport report=createReport(url);
IContext context=report.createContext();return newExportData(report, context);
}catch(XDocReportException ex) {throw newRuntimeException(ex.getMessage());
}
}/*** 加载模板的方法,主要是指定模板的路径和选择渲染数据的模板
*@paramurl 模板相对于类路径的地址
*@returnword 文档操作类*/
private staticIXDocReport createReport(String url) {try(
InputStream in= newClassPathResource(url).getInputStream();
) {
IXDocReport ix=XDocReportRegistry.getRegistry().loadReport(in, TemplateEngineKind.Freemarker);returnix;
}catch(Exception ex) {throw newRuntimeException(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)
packagecom.hwq.doc.export.model;importlombok.Getter;importlombok.Setter;
@Getter
@Setterpublic classUserModel {privateInteger order;privateString code;privateString name;
}
5.2,创建业务逻辑类 UserService
packagecom.hwq.doc.export.service;importcom.hwq.doc.export.model.UserModel;importcom.hwq.utils.export.ExportData;importcom.hwq.utils.export.WordUtil;importcom.hwq.utils.model.SoMap;importorg.springframework.stereotype.Service;importjava.io.FileOutputStream;importjava.io.IOException;importjava.util.ArrayList;importjava.util.List;importjava.util.UUID;
@Servicepublic classUserService {private final static String rootPath = "E:/text/file/"; //保存文件的地址
public byte[] downWord() {//准备数据
List list = new ArrayList();
UserModel user0= newUserModel();
UserModel user1= newUserModel();
UserModel user2= newUserModel();
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(newSoMap(user0));
list.add(newSoMap(user1));
list.add(newSoMap(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= newFileOutputStream(fileName);
) {
fos.write(data,0, data.length);
}catch(IOException ex) {throw newRuntimeException(ex.getMessage());
}returndata;
}
}
5.3,创建控制器 Usercontroller
packagecom.hwq.doc.export.controller;importcom.hwq.doc.export.service.UserService;importcom.hwq.utils.http.ResUtil;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importjavax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/user")public classUserController {
@AutowiredprivateUserService userService;
@RequestMapping("/word")publicObject getTemplate(HttpServletRequest request) {byte[] data =userService.downWord();return ResUtil.getStreamData(request, data, "文件下载", "docx");
}
}
5.4,以上还用到了我自己封装的工具类,SoMap 和 ResUtil如下
packagecom.hwq.utils.model;importjava.lang.reflect.Constructor;importjava.lang.reflect.Field;importjava.util.HashMap;public class SoMap extends HashMap{publicSoMap() { }/*** 构造方法,将任意实体类转化为 Map
*@paramobj*/
publicSoMap(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 newRuntimeException(ex.getMessage());
}
}/*** 将 Map 转化为 任意实体类
*@paramclazz 反射获取类字节码对象
*@return
*/
public T toEntity(Classclazz) {
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));
}returnt;
}catch(Exception ex) {throw newRuntimeException(ex.getMessage());
}
}/*** 从集合中获取一个字段的方法,如果字段不存在返回空
*@paramkey 字段的唯一标识
*@param 字段的类型,运行时自动识别,使用时无需声明和强转
*@return对应字段的值*/
public T get(String key) {return (T) super.get(key);
}
}
packagecom.hwq.utils.http;importorg.springframework.http.HttpHeaders;importorg.springframework.http.HttpStatus;importorg.springframework.http.MediaType;importorg.springframework.http.ResponseEntity;importorg.springframework.util.StringUtils;importjavax.servlet.http.HttpServletRequest;importjava.io.UnsupportedEncodingException;public classResUtil {/*** 生成下载文件,浏览器直接访问为下载文件
*@paramrequest 请求对象
*@paramdata 数据流数组
*@paramprefix 下载的文件名
*@paramsuffix 文件后缀
*@return浏览器可以直接下载的文件流*/
public static ResponseEntitygetStreamData(
HttpServletRequest request,byte[] data, String prefix, String suffix
) {
HttpHeaders headers= newHttpHeaders();
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(data, headers, HttpStatus.OK);
}catch(UnsupportedEncodingException ex) {
ex.printStackTrace();throw newRuntimeException(ex.getMessage());
}
}
}
6,我们把模板和一张图片存放到项目的资源文件夹下 的 export 下, 图片是用来替换模板中的图片的
7,启动项目,我们访问上面编写的控制器,效果如下,一切 OK(注意该种方式对于字段的要求比较严苛,只要在模板中编写的变量一定要设置值,否则抛异常)
8,新版本我们在生成表格数据时,也可以不使用metadata.addFieldAsList而在在 list 标签前面添加 @before-row 和 @after-row,这样就支持了表格的嵌套循环,如:
9,关于图片的循环目前好像暂不支持,只支持书签的方式,期待后续的跟新吧