java模板变量_java freemarker + word 模板 生成 word 文档 (变量替换,数据的循环,表格数据的循环,以及图片的替换)...

本文介绍了如何使用Java结合Freemarker和Word模板动态生成Word文档,详细讲解了如何设置普通数据、表格数据和图片,以及处理IF判断和循环。通过示例代码展示了如何创建和使用IXDocReport、IContext对象,以及模板的编辑方法和注意事项。
摘要由CSDN通过智能技术生成

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 内容如下

c2b7b14d66c6fec1abd99e6d74877682.png

4.1,我们可以发现上面的模板有些数据的两端有两个尖括号,就是我们需要替换数据的地方,插入方式如下

4.2,打开 word 文档,光标选中需要替换的位置 如上图的 1 号位  =》 Ctrl + F9 生成域  =》右键点击 =》选择编辑域 =》选择邮件合并 =》加上变量 ${model.order}

4.3,依次如下,注意输入变量的时候不要动 MERGEFIELD 这个单词,在他的后面空一格输入

aff9cb39ea8352caaf9609c01283e2f3.png

beeba4bd8c29dce6041fde49245f3b5c.png

4.4,IF 判断的写法,需要三个域,每一个的创建方式和上面相同 内容为   "[#if 1 == 1]"  文档内容  " [#else]"  文档内容  " [/#if]"  , 注意要加中括号,两端最好在加上引号

101f0b334b5729df702ba91dc06db186.png

4.5,循环的写法 [#list list as item]  [/#list]  依然是要注意两端的中括号,最好两端在加引号括起来

4.6,图片的插入方式和上面的不太相同,首先我们点击图片,选择插入,选择书签,输入一个任意的变量名如 img

2f7de3426d7355dbc671de98c928c753.png

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 下, 图片是用来替换模板中的图片的

b4d4e5fd33a6443409dfbd767b3f0869.png

7,启动项目,我们访问上面编写的控制器,效果如下,一切 OK(注意该种方式对于字段的要求比较严苛,只要在模板中编写的变量一定要设置值,否则抛异常)

2b73453601e9f89d3a3a351ebf8337be.png

e7dd4c8e465de62ad18b7b95b2bccd9f.png

995f3354cbbf404404655a5e302abe31.png

8,新版本我们在生成表格数据时,也可以不使用metadata.addFieldAsList而在在 list 标签前面添加 @before-row 和 @after-row,这样就支持了表格的嵌套循环,如:

5d6259f3ae3a25800df9df50aeadcd90.png

9,关于图片的循环目前好像暂不支持,只支持书签的方式,期待后续的跟新吧

SpringBoot_Freemarker生成Word_多个表格+两层嵌套循环; 步骤说明: 1.用Microsoft Office Word打开word原件;将文档中需要动态生成的内容,替换为属性名 ${name} 2.另存为,选择保存类型Word 2003 XML 文档(*.xml) 3.用Firstobject free XML editor打开文件,选择Tools下的Indent【或者按快捷键F8】格式化文件内容。左边是文档结构,右边是文档内容; 4. 文档生成后有时需要手动修改,查找第一步中设置的属性名,可能会产生类似${n.....ame}类似的样子,我们将将名字中间的标签删掉,恢复为${name} 5. word模板中有表格,需要循环的位置, 用 标签将第二对 标签(即除表头的w:tr标签后的一对)包围起来 同时表格内的属性例如${name},在这需要修改为${user.name} (userList是集合在dataMap中的key, user是集合中的每个元素, 类似), 如图: PLUS:若表格之外还有嵌套的循环,也需要用,注意这的标签不要和某对其他标签交叉,不可以出现这种 6. 标识替换完之后,另存为.ftl后缀文件即可。 代码是相对有一丢丢复杂的,两层嵌套循环; 总(dataMap) deptName 部门名 list(Table)表的集合 table1(map) table-名字 ${map.table} tableName-中文名 ${map.tableName} columnCount-字段数 ${map.columnCount} recordCount-记录数 ${map.recordCount} listA-List--表格1 map.listA column Model属性——字段名 ${model.column} columnName Model属性——字段中文名 ${model.column} rate Model属性——字段占比 ${model.rate} nullValueCount Model属性——字段空值数 ${model.nullValueCount} listB-List--表格2 map.listB …… listC-List--表格3 map.listC …… table2 table-名字 ${map.table} tableName-中文名 ${map.tableName} columnCount-字段数 ${map.columnCount} recordCount-记录数 ${map.recordCount} listA-List--表格1 map.listA column Model属性——字段名 ${model.column} columnName Model属性——字段中文名 ${model.column} rate Model属性——字段占比 ${model.rate} nullValueCount Model属性——字段空值数 ${model.nullValueCount} listB-List--表格2 map.listB …… listC-List--表格3 map.listC …… table3 ……
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值