在实际开发中,经常会遇到将数据库的数据写到word模板中再下载的需求,开贴记录一下.
首先准备一个带有占位符的word模板并放到resources目录中
然后开始编写下载的方法
@ApiOperation(value = "下载零星工程审批表", notes = "下载零星工程审批表", httpMethod = "POST")
@RequestMapping(value = "/downloadSporadicProject/{passId}", produces= MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void downloadSporadicProject(@PathVariable("passId")String passId, HttpServletRequest request, HttpServletResponse response) {
// 查询数据
ManagementSporadicProject sporadicProject = managementSporadicProjectService.getById(passId);
Map<String, String> docMap = new HashMap<>();
docMap.put("projectName", sporadicProject.getProjectName());
docMap.put("location", sporadicProject.getLocation());
docMap.put("construction", sporadicProject.getConstruction());
docMap.put("supervision", sporadicProject.getSupervision());
docMap.put("basis", sporadicProject.getBasis());
docMap.put("detail", sporadicProject.getDetail());
docMap.put("invest", sporadicProject.getInvest());
docMap.put("oftime", sporadicProject.getOftime());
String inputUrl = "白鹤滩水电站零星工程申报(审批)表.docx";
// 临时文件
String outputUrl = "白鹤滩水电站零星工程申报(审批)表temp.doc";
// 替换模板中的占位符并写入到临时文件
Common.changWord(inputUrl, outputUrl, docMap);
// 将临时文件以流的方法输出
try {
//获取资源文件
FileInputStream fileInputStream = new FileInputStream(outputUrl);
//设置响应类型
response.setCharacterEncoding("UTF-8");
String type = new MimetypesFileTypeMap().getContentType(inputUrl);
response.setHeader("content-Type", type);
if(Objects.equals(getBrowser(request), "FF")){//如果是火狐,解决火狐中文名乱码问题
response.setHeader("Content-Disposition",
"attachment;fileName=" +new String(inputUrl.getBytes("UTF-8"),"iso-8859-1"));
}else{
response.setHeader("Content-Disposition",
"attachment;filename=" + URLEncoder.encode(inputUrl, "UTF-8"));
}
//获取资源文件输入流和httpServletResponse的输出流
try (InputStream inputStream =fileInputStream; ServletOutputStream servletOutputStream = response.getOutputStream()) {
//把资源文件的二进制流数据copy到response的输出流中
IOUtils.copy(inputStream, servletOutputStream);
//清除flush所有的缓冲区中已设置的响应信息至客户端
response.flushBuffer();
} catch (Exception e) {
//错误日志记录
log.error(e.getMessage());
}
} catch (Exception e) {
//自定义业务异常
throw new ProjectException("导出模板失败");
} finally {
// 删除临时文件
File file = new File(outputUrl);
file.delete();
}
}
// 判断浏览器种类的方法
private String getBrowser(HttpServletRequest request) {
String UserAgent = request.getHeader("USER-AGENT").toLowerCase();
if (UserAgent.contains("msie"))
return "IE";
if (UserAgent.contains("firefox"))
return "FF";
if (UserAgent.contains("safari"))
return "SF";
return null;
}
common方法
/**
* 根据模板生成新word文档
* 判断表格是需要替换还是需要插入,判断逻辑有$为替换,表格无$为插入
* @param inputUrl 模板存放地址
* @param outputUrl 新文档存放地址
* @param textMap 需要替换的信息集合
*/
public static boolean changWord(String inputUrl, String outputUrl,
Map<String, String> textMap) {
//模板转换默认成功
boolean changeFlag = true;
try {
ClassPathResource resource = new ClassPathResource(inputUrl);
InputStream inputStream = resource.getInputStream();
//获取docx解析对象
XWPFDocument document = new XWPFDocument(inputStream);
//解析替换文本段落对象
changeText(document, textMap);
//生成新的word
File file = new File(outputUrl);
FileOutputStream outputStream = new FileOutputStream(file);
document.write(outputStream );
outputStream .close();
} catch (IOException e) {
e.printStackTrace();
changeFlag = false;
}
return changeFlag;
}
/**
* 替换段落文本
* @param document docx解析对象
* @param textMap 需要替换的信息集合
*/
public static void changeText(XWPFDocument document, Map<String, String> textMap){
//获取段落集合
List<XWPFParagraph> paragraphs = document.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
//判断此段落时候需要进行替换
String text = paragraph.getText();
if(checkText(text)){
List<XWPFRun> runs = paragraph.getRuns();
for (XWPFRun run : runs) {
//替换模板原来位置
String textValue = changeValue(run.toString(), textMap);
run.setText(textValue,0);
}
}
}
}
/**
* 匹配传入信息集合与模板
* @param value 模板需要替换的区域
* @param textMap 传入信息集合
* @return 模板需要替换区域信息集合对应值
*/
public static String changeValue(String value, Map<String, String> textMap){
Set<Map.Entry<String, String>> textSets = textMap.entrySet();
for (Map.Entry<String, String> textSet : textSets) {
//匹配模板与替换值 格式${key}
String key = "${"+textSet.getKey()+"}";
if(value.contains(key)){
value = value.replace(key, textSet.getValue());//仅替换参数
}
}
//模板未匹配到区域替换为空
if(checkText(value)){
value = "";
}
return value;
}
前端
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
</head>
<body>
<input type="button" onclick="down()" value="点击">
</body>
<script type="text/javascript">
function down(){
// var url = window.encodeURI(this.axios.defaults.baseURL + '/ev/CommodityMaster/downloadTemp?fileName=D1120物资总清单导入模板.xlsx&token=' + localStorage.getItem("set_token"))
// window.location.href = url
// window.open("http://localhost:10000/download", '_blank')
window.location.href = 'http://ip:30020/workflows/managementsporadicproject/download/2020092519170120bd63a2e25c49cb96a38dd1f123a623'
}
</script>
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,关于图片的循环目前好像暂不支持,只支持书签的方式,期待后续的跟新吧