JAVA Freemarker + Word 模板 生成 Word 文檔 (變量替換,數據的循環,表格數據的循環,以及圖片的東替換)

1,最近有個需求,動態生成 Word 文當並供前端下載,網上找了一下,發現基本都是用 word 生成 xml 而後用模板替換變量的方式前端

1.1,這種方式雖然可行,可是生成的 xml 是在是太亂了,整理就得整理半天,並且一旦要修改模板,那簡直就是災難,並且聽說還不兼容 WPSjava

1.2,因此筆者找到了如下能夠直接用 word 文檔做爲模板的方法,這裏作如下筆記,如下代碼依賴於 JDK8 以上web

2,pom.xml 相應依賴spring

        <!-- 文檔模板操做依賴 -->
        <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 和 setImgapp

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  的工具類dom

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 內容以下ide

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(注意該種方式對於字段的要求比較嚴苛,只要在模板中編寫的變量必定要設置值,不然拋異常)

 

 

 

相關文章
相關標籤/搜索