zoukankan      html  css  js  c++  java
  • java freemarker + word 模板 生成 word 文档 (变量替换,数据的循环,表格数据的循环,以及图片的替换)

    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,关于图片的循环目前好像暂不支持,只支持书签的方式,期待后续的跟新吧

  • 相关阅读:
    HDU2027 统计元音 一点点哈希思想
    湖南工业大学第一届ACM竞赛 数字游戏 字符串处理
    湖南工业大学第一届ACM竞赛 我素故我在 DFS
    HDU3293sort
    HDU2082 找单词 母函数
    HDU1018 Big Number 斯特林公式
    湖南工业大学第一届ACM竞赛 分糖果 位操作
    UVA 357 Let Me Count The Ways
    UVA 147 Dollars
    UVA 348 Optimal Array Multiplication Sequence
  • 原文地址:https://www.cnblogs.com/lovling/p/10791139.html
Copyright © 2011-2022 走看看