java生成word文档

java生成word文档

最近得到一个需求:按用户提供的模板生成分析报告,并让用户可以在网页上导出。这个功能以前没做过,但是好像听说过freemarker。于是乎,开始了我的百度之旅。

一、word文档的本质

我也是最近才知道,word文档的本质原来是一个压缩文件。不信你看,将.docx文件修改文件后缀为.zip

然后解压缩得到了这些文件,这些就是组成word文档的所有文件。其中word文件夹下是主要内容

​其中,document.xml中是关于文档内容的设置,相当于网页里面的html文件一样。_rels文件夹下的document.xml.rels文件是图片配置信息。media文件夹下是文档中所有图片的文件,其他的应该是类似于网页里面的CSS文件,设置样式的。所以document.xml就是我们要修改的了。这样的操作就相当于网页已经编写好了,只差从后台传送数据到前端展示了。

二、创建freemarker模板

freemarker是一个模板引擎,百度是这样介绍的:

FreeMarker是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页、电子邮件配置文件源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

​说白了就是跟前端页面的变量绑定是一样的,用过vuejs的都知道,前端使用“{{name}}”双括号括起来的变量可以通过传值改变页面上数据。freemarker也是这样,通过在document.xml中使用“${name}”dollar符大括号括起来的变量也可以通过传值改变模板文件内容。清楚了这点就好办了。

将document.xml文件放到IDEA项目中的templates文件夹下,然后按Ctrl+Alt+L键格式化xml内容,将需要动态修改的地方用“${}”括起来,如下

有的时候变量会被拆分成两个,就要麻烦点把两个中间的多余部分全都删掉,然后在用符号括起来。这点相信大家都能理解。

以上模板创建就结束了,是不是很简单。

三、代码实现

模板搞定了,怎么根据模板生成文档呢?关键步骤来了。

1.首先我们要将模板中的变量赋值,生成新的文件。

2.将生成的文件写入压缩文件。上面已经说了,word文档本质就是压缩文件。

3.将.docx文档的其他内容也写入压缩文件。

4.将压缩文件写入word文档,这就是最后生成的文档。

导入依赖:

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

主要用到下面的工具类:


public class FreeMarkUtils {
    private static Logger logger = LoggerFactory.getLogger(FreeMarkUtils.class);

    public static Configuration getConfiguration(){
        //创建配置实例
        Configuration configuration = new Configuration(Configuration.VERSION_2_3_28);
        //设置编码
        configuration.setDefaultEncoding("utf-8");
        configuration.setClassForTemplateLoading(FreeMarkUtils.class, "/templates");//此处设置模板存放文件夹名称
        return configuration;
    }

    /**
     * 获取模板字符串输入流
     * @param dataMap   参数,键为绑定变量名,值为变量值
     * @param templateName  模板名称
     * @return
     */
    public static ByteArrayInputStream getFreemarkerContentInputStream(Map dataMap, String templateName) {
        ByteArrayInputStream in = null;
        try {
            //获取模板
            Template template = getConfiguration().getTemplate(templateName);
            StringWriter swriter = new StringWriter();
            //生成文件
            template.process(dataMap, swriter);
            in = new ByteArrayInputStream(swriter.toString().getBytes("utf-8"));//这里一定要设置utf-8编码 否则导出的word中中文会是乱码
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("模板生成错误!");
        }
        return in;
    }
    //outputStream 输出流可以自己定义 浏览器或者文件输出流
    public static void createDocx(Map dataMap, OutputStream outputStream) {
        ZipOutputStream zipout = null;
        try {
            //内容模板,传值生成新的文件输入流documentInput
            ByteArrayInputStream documentInput = FreeMarkUtils.getFreemarkerContentInputStream(dataMap, "document.xml");
            //最初设计的模板,原word文件生成File对象
            File docxFile = new File(WordUtils.class.getClassLoader().getResource("templates/demo.docx").getPath());//模板文件名称
            if (!docxFile.exists()) {
                docxFile.createNewFile();
            }
            ZipFile zipFile = new ZipFile(docxFile);//获取原word文件的zip文件对象,相当于解压缩了word文件
            Enumeration<? extends ZipEntry> zipEntrys = zipFile.entries();//获取压缩文件内部所有内容
            zipout = new ZipOutputStream(outputStream);
            //开始覆盖文档------------------
            int len = -1;
            byte[] buffer = new byte[1024];
            while (zipEntrys.hasMoreElements()) {//遍历zip文件内容
                ZipEntry next = zipEntrys.nextElement();
                InputStream is = zipFile.getInputStream(next);
                if (next.toString().indexOf("media") < 0) {
                    zipout.putNextEntry(new ZipEntry(next.getName()));//这步相当于创建了个文件,下面是将流写入这个文件
                    if ("word/document.xml".equals(next.getName())) {//如果是word/document.xml由我们输入
                        if (documentInput != null) {
                            while ((len = documentInput.read(buffer)) != -1) {
                                zipout.write(buffer, 0, len);
                            }
                            documentInput.close();
                        }
                    } else {
                        while ((len = is.read(buffer)) != -1) {
                            zipout.write(buffer, 0, len);
                        }
                        is.close();
                    }
                }else{//这里设置图片信息,针对要显示的图片
                    zipout.putNextEntry(new ZipEntry(next.getName()));
                        while ((len = is.read(buffer)) != -1) {
                            zipout.write(buffer, 0, len);
                        }
                        is.close();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("word导出失败:"+e.getStackTrace());
        }finally {
            if(zipout!=null){
                try {
                    zipout.close();
                } catch (IOException e) {
                    logger.error("io异常");
                }
            }
            if(outputStream!=null){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    logger.error("io异常");
                }
            }
        }
    }
}

四、插入图片

​通过传值已经可以基本完成生成文档的功能了,但是用户还要求要在文档中生成统计分析图。知道文档本质的我马上想出了办法,但是这个就稍微有点麻烦了。刚刚说了,media文件夹下存放的是文档中所有的图片。我可以在word文档要生成统计图的地方先用图片占好位置,调好大小。然后解压后看这个图片在media文件夹中叫什么名字。最后在代码生成的时候,跳过原文件图片的写入替换成我生成的图片就可以了。核心代码如下:

  while (zipEntrys.hasMoreElements()) {
                ZipEntry next = zipEntrys.nextElement();
                InputStream is = zipFile.getInputStream(next);
                if (next.toString().indexOf("media") < 0) {
                   ...省略
                }else{//这里设置图片信息,针对要显示的图片
                    zipout.putNextEntry(new ZipEntry(next.getName()));
                    if(next.getName().indexOf("image1.png")>0){//例如要用一张图去替换模板中的image1.png
                        if(dataMap.get("image1")!=null){
                            byte[] bys = Base64Util.decode(dataMap.get("image1").toString());
                            zipout.write(bys,0,bys.length);
                        }else{
                            while ((len = is.read(buffer)) != -1) {
                                zipout.write(buffer, 0, len);
                            }
                            is.close();
                        }
                    }
                }
            }

注意:这个Base64Util是一个将图片文件转化为base64编码字符串和将base64编码字符串转换为字节数组的工具类。这里涉及到图片文件的本质,图片的本质是一个二进制文件,二进制文件可以转换成字节数组,而字节数组又可以和字符串互相转换。这个知道就好,这里我把生成的图片的字符串存入了集合中,写入文档的时候将字节数组写入。这样原图片就被替换成了生成的图片了。这里直接将新的图片文件写入也是一样的效果。

posted @ 2021-01-15 12:33  康帝  阅读(2255)  评论(0编辑  收藏  举报