Java後端使用Freemarker導出word文檔的各類細節

1.前言

最近在項目中,因客戶要求,須要作一個導出成word的功能(好比月度報表等),技術選型也考慮過幾種,好比easypoi,itext,但發現這兩種在實現起來有困難,因此最終仍是選Freemarker模板進行導出,靈活性比較好。html

2.實現步驟

1.準備好標準文檔的word,標題格式間距什麼的先設計好,這是減小後面修改模板文很重要一步;前端

2.打開word原件把須要動態修改的內容替換成***,若是有圖片,儘可能選擇較小的圖片幾十K左右,並調整好位置;java

3.另存爲,選擇保存類型Word 2003 XML 文檔(*.xml)【這裏說一下爲何用Microsoft Office Word打開且要保存爲Word 2003XML,本人親測,用WPS找不到Word 2003XML選項,若是保存爲Word XML,會有兼容問題,避免出現導出的word文檔不能用Word 2003打開的問題】,還有保存的文件名儘可能不要是中文;linux

4.用NotePad打開文件,notepad預先裝好xml的插件,而後格式化,固然也能夠用Firstobject free XML editor打開文件,選擇Tools下的Indent【或者按快捷鍵F8】格式化文件內容。看我的喜歡;web

notepad xml插件下載地址:https://sourceforge.net/projects/npp-plugins/files/XML%20Tools/windows

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>app

  注意:echarts

         (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 2018-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(exists.compareAndSet(false,true)){
           imgsMap.clear();
           imgsMap = map;
       }*/
}

前端js

var system = require('system');  
var page = require('webpage').create();

// 若是是windows,設置編碼爲gbk,防止中文亂碼,Linux自己是UTF-8
var 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事件。

這裏有一個帖子的詳細說明:https://blog.csdn.net/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下載地址:http://www.firstobject.com/dn_editor.htm

freemarker 官網:http://freemarker.org/ 

phantomjs下載  http://phantomjs.org/download.html

相關文章
相關標籤/搜索