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,关于图片的循环目前好像暂不支持,只支持书签的方式,期待后续的跟新吧

JAVA Freemarker + Word 模板 生成 Word 文档 (普通的变量替换,数据的循环,表格数据的循环,以及图片的东替换)的更多相关文章

  1. JAVA Asponse.Word Office 操作神器,借助 word 模板生成 word 文档,并转化为 pdf,png 等多种格式的文件

    一,由于该 jar 包不是免费的, maven 仓库一般不会有,需要我们去官网下载并安装到本地 maven 仓库 1,用地址   https://www-evget-com/product/564  ...

  2. Java多种方式动态生成doc文档

    转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/5280272.html 本来是要在Android端生成doc的(这需求...),最后方法没有好的方法能够在An ...

  3. java基础---->使用Itext生成数据库文档

    这里简单的介绍一下使用Itext生成数据库表的文档.于是我们领教了世界是何等凶顽,同时又得知世界也可以变得温存和美好. 生成数据库的文档 一.maven项目需要引入的jar依赖 <depende ...

  4. JAVAWEB使用FreeMarker利用ftl把含有图片的word模板生成word文档,然后打包成压缩包进行下载

    这是写的另一个导出word方法:https://www.cnblogs.com/pxblog/p/13072711.html 引入jar包,freemarker.jar.apache-ant-zip- ...

  5. java通过word模板生成word文档

    介绍 上次公司项目需要一个生成word文档的功能,有固定的模板根据业务填充数据即可,由于从来没做过,项目也比较着急于是去网上找有没有合适的工具类,找了好几种,看到其中有freeMark模板生成比较靠谱 ...

  6. freemarker根据模板生成word文件实现导出功能

    一.准备工作 1.创建一个03的word文档,动态的数据用占位符标志占位(如testname).然后另存为word2003的xml文件. 2.格式化xml文件,占位符的位置用${testname}代替 ...

  7. 使用java Apache poi 根据word模板生成word报表

    项目开发过程中,客户提出一堆导出报表的需求,需要导出word格式,页眉还需要加上客户公司的logo,试了几种方案,最后选择了用 Apache poi 加上自定义标签的方式实现. 目前功能还比较简单,一 ...

  8. JAVA使用itext根据模板生成PDF文档

    1.制作PDF模板 网址打开:https://www.pdfescape.com/open/ 我们这里先在线上把基础的内容用word文档做好,然后转成PDF模板,直接上传到网站上,这样方便点 假设我们 ...

  9. 根据指定Word模板生成Word文件

    最近业务需要批量打印准考证信息 1.根据Table数据进行循环替换,每次替换的时候只替换Word中第一个Table的数据, 2.每次替换之后将Word中第一个Table数据进行复制,将复制Table和 ...

随机推荐

  1. vim-go 安装

    vim-go 安装 https://studygolang.com/articles/3229

  2. supervisor支持python虚拟环境venv

    在项目中使用supervisor时,如何在虚拟环境下启动一直存在些小问题. 比如我要写 Cesi程序的监听,我是手动安装的执行之前 要先加载环境source venv/bin/activate 所以写 ...

  3. Qt applendPlainText()/append() 多添加一个换行解决方法

    Qt applendPlainText()/append() 多添加一个换行解决方法 void ConsoleDialog::appendMessageToEditor(const QString & ...

  4. update_engine-整体结构(一)

    update_engine简介 update_engine是A/B升级的核心逻辑.理解了update_engine就理解了在Android系统中A/B升级是如何运行的.它的代码放在源码目录下syste ...

  5. Sql Server查看死锁及堵塞脚本

    --每秒死锁数量 SELECT * FROM sys.dm_os_performance_counters WHERE counter_name LIKE 'Number of Deadlocksc% ...

  6. monitor.sh

    #!bin/bash message_counts="" succeed_counts="" all_succeed_counts="" f ...

  7. 工控随笔_10_西门子_WinCC的VBS脚本_01_基础入门

    很多人都认为VB语言或者VBS脚本语言是一种很low的语言,从心里看不起VB或者VBS, 但是其实VBS不仅可以做为系统管理员的利器,同样在工控领域VBS语言大有用武之地. 西门子的WinCC提供了两 ...

  8. Webpack 模块处理

    webpack模块处理 1. ES6 静态Import ES6的import会被转化为commonjs格式或者是AMD格式,babel默认会把ES6的模块转化为commonjs规范的. import ...

  9. Java Exception 和Error

    (事先声明:该文章并非完全是我自己的产出,更多的是我个人在看到资料后通过理解并记录下来,作为自己阅读后的一个笔记:我现在试图对自己多年工作中的知识点做一个回顾,希望能融会贯通) (此文参考<Ja ...

  10. SXWIN7X64EN_20181104_NET_msu_LITE英文精简版

    SXWIN7X64EN_20181104_NET_msu_LITE英文精简版该版本为英文精简版!该版本为英文精简版!该版本为英文精简版!一.前言:关于极限精简版的说明 本系统为极限精简版,极限精简版系 ...