word下载

news/2024/11/14 14:18:59/

在实际开发中,经常会遇到将数据库的数据写到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);//生成新的wordFile 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 {@Autowiredprivate 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,关于图片的循环目前好像暂不支持,只支持书签的方式,期待后续的跟新吧


http://www.ppmy.cn/news/351363.html

相关文章

【离散数学】集合论

目录 集合的基本概念 集合的表示 集合与集合之间的关系 几个特殊的集合 集合的运算 无限集、可数集、不可数集 与集合相关的应用 计数问题 集合的基本概念 集合就是由一些元素组成的整体&#xff0c;每一个元素可以是任何东西——数字、字母、词语、图片等。集合中的元…

leetcode198. 打家劫舍(java-动态规划)

打家劫舍 leetcode198. 打家劫舍题目描述 暴力递归解题思路代码演示 递归 缓存解题思路代码演示 动态规划解题思路代码演示 动态规划专题 leetcode198. 打家劫舍 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode.cn/problems/house-r…

菜场

这是鲁谷小区一个比较大的菜场了&#xff0c;冬天蔬菜的种类并不多&#xff0c;卖菜的人看上去也有点蔫。 畅销的大白菜&#xff0c;很多人推着小车来买&#xff0c;然后晒在院子里&#xff0c;晒成一团团暗绿色的大球。 胡萝卜&#xff0c;我最不爱吃的东西之一。同样是萝卜&a…

Tips_PMP

http://www.eetchina.com/DG/eec_dg_free_reply.php?disc_grp_id10010&topic_id1000011527《Intel与AMD争先恐后&#xff0c;芯片巨头比拼PMP市场》//参考设计 INTEL PMP参考设计的全套资料 AMD AU1200我也有全套参考设计资料 //解决方案 目前国内基于AU1200的解决方案…

IT行业,电脑产品,做什么生意最赚钱?

IT行业,电脑产品&#xff0c;做什么生意最赚钱&#xff1f; 最赚钱的行业是什么&#xff1f; 毫无疑问人们会回答&#xff1a;房地产啊、教育啊、汽车啊、能源啊、IT数码产品啊。显然这样的回答毫无意义&#xff0c;因为绝大多数商人既然已在船上&#xff0c;就不大容易改行跳上…

(转)最赚钱的行业是什么?

最赚钱的行业是什么&#xff1f; 毫无疑问人们会回答&#xff1a;房地产啊、教育啊、汽车啊、能源啊、IT数码产品啊。显然这样的回答毫无意义&#xff0c;因为绝大多数商人既然已在船上&#xff0c;就不大容易改行跳上另外的贼船&#xff1b;何况这些“最赚钱的生意”仅仅是使从…

选购数码相机的基本要领

选购数码相机的基本要领 一、选购数码相机的基本要领——选机购机九要素 数码相机和传统相机在光学机械结构、电子曝光控制等方面都相当类似&#xff0c;二者最大差异就在于成像介质和成像原理的不同&#xff0c;数码相机在成像和工作原理上要比传统相机复杂得多。也正因为此&a…

社会这些赚钱的行业...

最赚钱的行业是什么&#xff1f; 毫无疑问人们会回答&#xff1a;房地产啊、教育啊、汽车啊、能源啊、IT数码产品啊。显然这样的回答毫无意义&#xff0c;因为绝大多数商人既然已在船上&#xff0c;就不大容易改行跳上另外的贼船&#xff1b;何况这些“最赚钱的生意”仅仅是使从…