2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > Java后端使用Freemarker导出word文档的各种细节

Java后端使用Freemarker导出word文档的各种细节

时间:2022-01-17 07:18:50

相关推荐

Java后端使用Freemarker导出word文档的各种细节

独角兽企业重金招聘Python工程师标准>>>

1.前言

最近在项目中,因客户要求,需要做一个导出成word的功能(比如月度报表等),技术选型也考虑过几种,比如easypoi,itext,但发现这两种在实现起来有困难,所以最终还是选Freemarker模板进行导出,灵活性比较好。

2.实现步骤

1.准备好标准文档的word,标题格式间距什么的先设计好,这是减少后面修改模板文很重要一步;

2.打开word原件把需要动态修改的内容替换成***,如果有图片,尽量选择较小的图片几十K左右,并调整好位置;

3.另存为,选择保存类型Word XML 文档(*.xml)【这里说一下为什么用Microsoft Office Word打开且要保存为Word XML,本人亲测,用WPS找不到Word XML选项,如果保存为Word XML,会有兼容问题,避免出现导出的word文档不能用Word 打开的问题】,还有保存的文件名尽量不要是中文;

4.用NotePad打开文件,notepad预先装好xml的插件,然后格式化,当然也可以用Firstobject free XML editor打开文件,选择Tools下的Indent【或者按快捷键F8】格式化文件内容。看个人喜欢;

notepad xml插件下载地址:/projects/npp-plugins/files/XML%20Tools/

5.将文档内容中需要动态修改内容的地方,换成freemarker的标识。其实就是Map<String, Object>中key,如${userName};

6.在加入了图片占位的地方,会看到一片base64编码后的代码,把base64替换成${image},也就是Map<String, Object>中key,值必须要处理成base64;

代码如:<w:binData w:name="wordml://自定义.png" xml:space="preserve">${image}</w:binData>

注意:

(1)“>${image}<”这尖括号中间不能加任何其他的诸如空格,tab,换行等符号。

(2)如果是多张图片需要循环图片w:name 和v:imagedata 的src需要变化的

(3)如果图片的宽高最好是在后端自定义(我这里是固定宽然后高比例变化),不至于图片很宽导出的word图片变形

完整实例如下

<w:binData w:name="${"wordml://03000001"+ins_index+1+".jpg"}" xml:space="preserve">${ins.insHealthImg.code}</w:binData>

<v:shape id="图片 10" o:spid="_x0000_i1032" type="#_x0000_t75" style="width:${ins.insHealthImg.width}pt;height:${ins.insHealthImg.height}pt;visibility:visible;mso-wrap-style:square">

<v:imagedata src="${"wordml://03000001"+ins_index+1+".jpg"}" o:title=""/>

</v:shape>

7.标识替换完之后,模板就弄完了,另存为.ftl后缀文件即可。注意:一定不要用word打开ftl模板文件,否则xml内容会发生变化,导致前面的工作白做了。

3.代码实现

引入依赖

<dependency><groupId>org.freemarker</groupId><artifactId>freemarker</artifactId><version>2.3.28</version></dependency>

导出的工具类FreemarkerBase

import freemarker.template.Configuration;import freemarker.template.Template;import freemarker.template.TemplateException;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import javax.servlet.http.HttpServletResponse;import java.io.*;import java.util.Map;/*** @author lpf* @create -11-03 17:27**/public class FreemarkerBase {protected final Logger logger = LoggerFactory.getLogger(getClass());private Configuration configuration = null;/*** 获取freemarker的配置. freemarker本身支持classpath,目录和从ServletContext获取.*/protected Configuration getConfiguration() {if (null == configuration) {configuration = new Configuration(Configuration.VERSION_2_3_28);configuration.setDefaultEncoding("utf-8");//ftl是放在classpath下的一个目录configuration.setClassForTemplateLoading(this.getClass(), "/template/");}return configuration;}/*** 导出word** @param response* @param templateName* @param dataMap*/public void downLoad(HttpServletResponse response, String templateName, Map<String, Object> dataMap) throws IOException {OutputStream os = response.getOutputStream();Writer writer = new OutputStreamWriter(os, "utf-8");Template template = null;try {template = getConfiguration().getTemplate(templateName, "utf-8");template.process(dataMap,writer);os.flush();writer.close();os.close();} catch (TemplateException e) {logger.error("模板文件异常,请检查模板文件路径和文件名:" + e.getMessage());} catch (IOException e) {logger.error("IO异常,导出到浏览器出错:" + e.getMessage());}}}

这里因为是浏览器导出,使用输出流用的response,而网上一般的教程都是先生存临时文件在读取文件流输出,然后删除临时文件,我任务是多余的步骤;

导出代码

@RequestMapping(value = "/download")public void downWord(HttpServletRequest request, HttpServletResponse response) throws IOException {Map<String, Object> dataMap = this.getWordData(request);//封装数据的方法FreemarkerBase freemarkerBase = new FreemarkerBase();String fileName = "XXXXX.doc";response.setContentType("application/octet-stream");response.setHeader("Content-Disposition", "attachment;filename=" + new String(fileName.getBytes("gb2312"), "ISO8859-1"));freemarkerBase.downLoad(response, "templete_min.ftl", dataMap);}

核心代码就上面这些,当然一个比较复杂的word导出在封装数据的时候肯定会碰到问题

4.遇到的问题

1.图片数据来源

如果插入图片是本地已经存在的图片那很好办,读取图片转成base64即可,但是在项目中图片本地并没有而是在前端页面用echart生成的图片。

我的思路是利用phantomjs模拟浏览器请求前端页面利用echart生成图片将生成图片的base64传入后端

代码逻辑

前端请求下载word

@RequestMapping(value = "/download")public void download(HttpServletRequest request,HttpServletResponse response) throws IOException {String rptId = request.getParameter("rptId");User userInfo = (User) request.getSession().getAttribute("user");Long startTime= System.currentTimeMillis();Long currentTime = null;WordWrite.Domain(rptId);//模拟浏览器请求生成图片while (true){//if(WordWrite.imgsMap.get(rptId)!=null){//监听图片是否已经生成好reportWordService.downWord(request,response);WordWrite.imgsMap.remove(rptId);break;}else{currentTime = System.currentTimeMillis();if((currentTime-startTime)/1000>60){//添加下载超时的判断避免死循环break;}}}}

模拟浏览器请求方法

生成图片工具类

public static void Domain(String rptId) throws IOException {ReportService reportService = SpringContextHolder.getBean("reportService");List<Map<String, Object>> instanceList = reportService.getRelationInstanceByReportId(rptId);StringBuffer sb = new StringBuffer();for(int i =0;i<instanceList.size();i++){String _uid = (String)instanceList.get(i).get("target_id");sb.append(_uid+",");}String uids = sb.substring(0,sb.length()-1);String paramStr = "target_ids="+uids+";rptId="+rptId;paramStr = URLEncoder.encode(paramStr ,"UTF-8");propPath = WordWrite.class.getResource("/").toString();String[] ps = propPath.split("file:/")[1].split("/");String[] newPaths = Arrays.copyOfRange(ps, 0, ps.length-6);propPath = StringUtils.join(newPaths, "/") + "/conf";if(propPath.indexOf(":") == -1){propPath = "/"+propPath;System.out.println("propPath linux");}else if(propPath.indexOf(":") != -1){System.out.println("propPath windows");}System.out.println("phantomjs.properties文件所在目录:"+propPath+"/phantomjs.properties");FileInputStream in = new FileInputStream(propPath+"/phantomjs.properties");String[] _path = Arrays.copyOfRange(ps,0,ps.length-2);WordWritePath = StringUtils.join(_path, "/")+"/jsp/pages/";if(WordWritePath.indexOf(":") == -1){WordWritePath = "/"+WordWritePath;System.out.println("WordWritePath linux");}else if(WordWritePath.indexOf(":") != -1){System.out.println("WordWritePath windows");}System.out.println("截图时需要用到的js路径:"+WordWritePath);proper = new Properties();proper.load(in);in.close();// 生成月报图片dopng(proper,"month",paramStr);}

/*** 保存网页中的图片* @return* @throws IOException*/public static String dopng(Properties pro,String type, String jsParam) throws IOException{String jspUrl = pro.getProperty("jsp"); //"http://localhost:8080/RtManageCon/jsp/pages/nobrowserpages/chartsByNoBrowser.jsp";if(jsParam != null){jspUrl = jspUrl+"?"+jsParam;}String jsurl = "";switch (type) {case "day":jsurl = " "+WordWrite.WordWritePath+"phantomjs/"+pro.getProperty("dayjs")+" ";break;case "week":jsurl = " "+WordWrite.WordWritePath+"phantomjs/"+pro.getProperty("weekjs")+" ";break;case "month":jsurl = " "+WordWrite.WordWritePath+"phantomjs/"+pro.getProperty("monthjs")+" ";break;default:jsurl = " "+WordWrite.WordWritePath+"phantomjs/"+pro.getProperty("monthjs")+" ";break;}return downloadImage(jsurl,jspUrl);}

public static String downloadImage(String jsurl,String url) throws IOException {String cmdStr = PHANTOM_PATH + jsurl + url;//String cmdStr = "C:/develop/phantomjs-2.1.1-windows/bin/phantomjs.exe " + jsurl + url;System.out.println("命令行字符串:"+cmdStr);Runtime rt = Runtime.getRuntime();try {rt.exec(cmdStr);} catch (IOException e) {System.out.println("执行phantomjs的指令失败!请检查是否安装有PhantomJs的环境或配置path路径!");}return cmdStr;}

public static final ConcurrentMap<String,Object> imgsMap = new ConcurrentHashMap<>();用来接收图片的base64编码

//接收图片base64编码public static void doExecutoer(Map<String,Object> map){imgsMap.putAll(map);/*原子操作,如果期望值是false时,则执行赋值if(pareAndSet(false,true)){imgsMap.clear();imgsMap = map;}*/}

前端js

var system = require('system'); var page = require('webpage').create();// 如果是windows,设置编码为gbk,防止中文乱码,Linux本身是UTF-8var osName = system.os.name; console.log('os name:' + osName); if ('windows' === osName.toLowerCase()) { phantom.outputEncoding="gbk";}// 获取第二个参数(即请求地址url).var url = system.args[1]; console.log('url:' + url);// 显示控制台日志.page.onConsoleMessage = function(msg, lineNum, sourceId) { console.log('CONSOLE: ' + msg + ' (from line #' + lineNum + ' in "' + sourceId + '")');};//打开给定url的页面.var start = new Date().getTime(); // 页面大小 ------------------------------------------------------------------------------page.viewportSize={width:650,height:400}; // -----------------------------------------------------------------------------------------page.open(url, function(status) { if (status == 'success') {console.log('echarts页面加载完成,加载耗时:' + (new Date().getTime() - start) + ' ms');page.evaluate(function() {console.log("月报js");getAjaxRequest("month");//改方法去实现生成图片并传入后端});} else {console.log("页面加载失败 Page failed to load!");}// 5秒后再关闭浏览器.setTimeout(function() {phantom.exit();}, 15*1000);});

有不熟悉phantomjs的可以查找下资料大概了解就行。

2.导出的word比较大

用模版导出的方式,这个问题不可避免,因为模版是XML,本身带有大量的标签,注意在XML里写循环的时候注意不要生成不必要的标签,另外XML模版弄好后压缩一下,然后导出的word大小就减少很多啦。

3.由于下载时间长,避免重复下载,客户希望在前端有一个加载等待框

利用iframe实现下载等待,用iframe实现下载等待的原理是把下载的路径给iframe的src,然后监听iframe的onload事件,当后台处理完成并返回文件时,会触发iframe的onload事件。

这里有一个帖子的详细说明:/fgx_123456/article/details/79603455

但是我在项目中总是无法监听到onload事件。浏览器给的提示是请求一直没完成。后面也一直没找到原因,没有找到解决办法,不知道谁遇到过着个问题没。

后面没办法用了框架中的WebSocket主动向前端相应下载完成,等待加载结束。在上面下载接口的代码上改造如下

@RequestMapping(value = "/download")public void download(HttpServletRequest request,HttpServletResponse response) throws IOException {String rptId = request.getParameter("rptId");User userInfo = (User) request.getSession().getAttribute("user");Long startTime= System.currentTimeMillis();Long currentTime = null;WordWrite.Domain(rptId);while (true){if(WordWrite.imgsMap.get(rptId)!=null){reportWordService.downWord(request,response);for(WebSocketForJSP item: WebSocketForJSP.webSocketSet){if(userInfo.getUsername().equals(item.userName)){JSONObject resultObj = new JSONObject();resultObj.put("reportCode", 0);resultObj.put("msg", "月报表导出成功");item.sendMessage(resultObj.toJSONString());}}WordWrite.imgsMap.remove(rptId);break;}else{currentTime = System.currentTimeMillis();if((currentTime-startTime)/1000>60){for(WebSocketForJSP item: WebSocketForJSP.webSocketSet){if(userInfo.getUsername().equals(item.userName)){JSONObject resultObj = new JSONObject();resultObj.put("reportCode", -1);resultObj.put("msg", "月报表导出超时");item.sendMessage(resultObj.toJSONString());}}break;}}}}

WebSocket的一些实现代码就没贴了,有需要欢迎留言。

5.结束语

如果对Freemarker标签不熟的,可以在网上先学习下,了解文档结构,模板需要足够的耐心和仔细。

Firstobject free XML editor下载地址:/dn_editor.htm

freemarker 官网:/

phantomjs下载/download.html

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。