【原创】Java开发中遇到的动态导出word

思路:利用Freemarker模板生成docx格式的word文档
具体操作步骤:
1、
首先在pom.xml中添加freemarker依赖

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

需要过滤掉不需要编码的文件:过滤后缀为.zip的所有文件,不对其进行统一编码

<plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-resources-plugin</artifactId>
        <!--<version>3.1.0</version>-->
        <configuration>
          <encoding>UTF-8</encoding>
          <nonFilteredFileExtensions>
             <nonFilteredFileExtension>zip</nonFilteredFileExtension>
          </nonFilteredFileExtensions>
        </configuration>
</plugin>

2、
创建一个docx的word模板,图中英文为需要填充的内容(可随意)
【原创】Java开发中遇到的动态导出word
3、
将word文件后缀名docx改为zip,并解压到当前目录下
【原创】Java开发中遇到的动态导出word
4、
将之前改好的zip格式的文件和word目录下的document.xml文件拷贝到项目的templates目录下
【原创】Java开发中遇到的动态导出word
5、
nacos配置路径和文件名,和上图中的一致
【原创】Java开发中遇到的动态导出word
6、
修改document.xml文件内的参数,把meetingTitle改为${metingTitle},【注意:不要格式化】
【原创】Java开发中遇到的动态导出word
7、
在<w:tr>前面加上<#list meetingList as meeting>和对应的</w:tr>后面添加</#list>,用于循环填充

<#list meetingList as meeting>
            <w:tr>
                <w:tblPrEx>
                    <w:tblBorders>
                        <w:top w:val="single" w:color="auto" w:sz="4" w:space="0"/>
                        <w:left w:val="single" w:color="auto" w:sz="4" w:space="0"/>
                        <w:bottom w:val="single" w:color="auto" w:sz="4" w:space="0"/>
                        <w:right w:val="single" w:color="auto" w:sz="4" w:space="0"/>
                        <w:insideH w:val="single" w:color="auto" w:sz="4" w:space="0"/>
                        <w:insideV w:val="single" w:color="auto" w:sz="4" w:space="0"/>
                    </w:tblBorders>
                    <w:tblCellMar>
                        <w:top w:w="0" w:type="dxa"/>
                        <w:left w:w="108" w:type="dxa"/>
                        <w:bottom w:w="0" w:type="dxa"/>
                        <w:right w:w="108" w:type="dxa"/>
                    </w:tblCellMar>
                </w:tblPrEx>
                <w:tc>
                    <w:tcPr>
                        <w:tcW w:w="771" w:type="pct"/>
                    </w:tcPr>
                    <w:p>
                        <w:pPr>
                            <w:spacing w:after="0" w:line="220" w:lineRule="atLeast"/>
                            <w:jc w:val="left"/>
                            <w:rPr>
                                <w:rFonts w:hint="default" w:ascii="仿宋" w:hAnsi="仿宋" w:eastAsia="仿宋"/>
                                <w:sz w:val="32"/>
                                <w:szCs w:val="32"/>
                                <w:lang w:val="en-US" w:eastAsia="zh-CN"/>
                            </w:rPr>
                        </w:pPr>
                    </w:p>
                </w:tc>
                <w:tc>
                    <w:tcPr>
                        <w:tcW w:w="818" w:type="pct"/>
                        <w:vAlign w:val="center"/>
                    </w:tcPr>
                    <w:p>
                        <w:pPr>
                            <w:spacing w:after="0" w:line="220" w:lineRule="atLeast"/>
                            <w:jc w:val="left"/>
                            <w:rPr>
                                <w:rFonts w:hint="default" w:ascii="仿宋" w:hAnsi="仿宋" w:eastAsia="仿宋"/>
                                <w:sz w:val="32"/>
                                <w:szCs w:val="32"/>
                                <w:lang w:val="en-US" w:eastAsia="zh-CN"/>
                            </w:rPr>
                        </w:pPr>
                        <w:r>
                            <w:rPr>
                                <w:rFonts w:hint="eastAsia" w:ascii="仿宋" w:hAnsi="仿宋" w:eastAsia="仿宋"/>
                                <w:sz w:val="32"/>
                                <w:szCs w:val="32"/>
                                <w:lang w:val="en-US" w:eastAsia="zh-CN"/>
                            </w:rPr>
                            <w:t>${meeting.meetingTime}</w:t>
                        </w:r>
                    </w:p>
                </w:tc>
                <w:tc>
					<...省略...>
            </w:tr>
</#list>

8、
合并单元格在第一次循环时添加<w:vmerge w:val=“restart”/>,其他循环加上<w:vmerge w:val=“continue”/>

<w:tcPr>
	<w:tcW w:w="771" w:type="pct"/>
	<w:vMerge w:val="restart"/>
	<w:vAlign w:val="center"/>
</w:tcPr>

<w:tcPr>
    <w:tcW w:w="725" w:type="pct"/>
    <w:vMerge w:val="continue"/>
    <w:tcBorders/>
</w:tcPr>

9、
JAVA代码如下:

@Service
public class Test extends ServiceImpl<TestMapper, Test> implements TestService {
	@Resource
    private TestMapper testMapper;
 	@Value("${template.xmlParentPath}")
    private String xmlParentPath;
    @Value("${template.xmlTemplateName}")
    private String xmlTemplateName;
    @Value("${template.zipFileUrl}")
    private String zipFileUrl;
    
    @Override
    public void download(String id, HttpServletRequest request, HttpServletResponse response) {
        List<Map<String, Object>> mapList = testMapper.getData(id);
        Map<String, Object> map = testMapper.getMettingData(id);

        if (CollUtil.isEmpty(mapList) || map == null || map.size() == 0) {
            throw new GenericException(purchaseCodeEnum.DATA_IS_NULL.getCode(), purchaseCodeEnum.DATA_IS_NULL.getMsg());
        }
        map.put("mettingList",mapList);
        try {
            String fileName = fileService.encodeChineseDownloadFileName(request, "测试word.docx");
            WordTool.downWordDocxOrDoc(map, null, null,
                    xmlParentPath, false, xmlTemplateName,
                    zipFileUrl, false, fileName, request,response);
        } catch (Exception e) {
            Log.error(e.getMessage());
            throw new GenericException(purchaseCodeEnum.DOWNLOAD_FILE_ERROR.getCode(),
                    purchaseCodeEnum.DOWNLOAD_FILE_ERROR.getMsg());
        }
    }
}
/**
     * description:对文件流输出下载的中文文件名进行编码 屏蔽各种浏览器版本的差异性
     * @param request  http请求
     * @param fileName 文件名称
     * @return java.lang.String 新的zip名称
     * @author: lx
     * @date: 2022/2/22 22:57
     */
    public String encodeChineseDownloadFileName(HttpServletRequest request, String fileName) throws Exception {

        String newFileName = null;
        String agent = request.getHeader("USER-AGENT");
        if (null != agent) {
            if (-1 != agent.indexOf("Firefox")) {//Firefox
                newFileName =
                        "=?UTF-8?B?" + (new String(org.apache.commons.codec.binary.Base64.encodeBase64(fileName.getBytes(
                                "UTF-8")))) + "?=";
            } else if (-1 != agent.indexOf("Chrome")) {//Chrome
                newFileName = new String(fileName.getBytes(), "ISO8859-1");
            } else {//IE7+
                newFileName = java.net.URLEncoder.encode(fileName, "UTF-8");
                newFileName = newFileName.replace("+", "%20");
            }
        } else {
            newFileName = fileName;
        }
        return newFileName;
    }

public class WordTool {

    private static final Logger LOGGER = LoggerFactory.getLogger(WordTool.class);

    /**
     * 参数错误码
     */
    private static final Integer PARAM_ERROR_CODE = 500001;
    /**
     * 参数错误描述
     */
    private static final String PARAM_ERROR_DESC = "参数有误!";
    /**
     * 生成word错误码
     */
    private static final Integer WORD_CREATE_ERROR_CODE = 500002;
    /**
     * word生成失败描述
     */
    private static final String WORD_CREATE_ERROR_DESC = "word生成失败";

    /**
     * word转html错误码
     */
    private static final Integer WORD_TO_HTML_ERROR_CODE = 500003;
    /**
     * word转html描述
     */
    private static final String WORD_TO_HTML_ERROR_DESC = " word转html失败";

    /**
     * 默认的编码为utf-8
     */
    private static final String DEFAULT_ENCODING = "utf-8";
    
/**
     * 生成及下载word文件
     * description: 使用document.xml(由word模版解压而来)来创建docx或者doc后缀的word文件的方法,此方法可以生成docx及doc
     * 注意:这里document.xml不要进行格式化,否则会导致word如果有做预览的时候格式错乱
     * @param dataModel         word数据模型,可以是map
     * @param versionCode       版本号,可为空,默认为:2.3.23版本
     * @param encoding          编码,可为空,默认为:utf-8
     * @param xmlParentPath     document.xml文件的父路径, 相对路径或者绝对路径,相对路径时指:相对classpath的路径,
     *                          可为空,当表示绝对父路径时必填,相对父路径时可不填,不填默认加载配置文件中的:
     *                          spring.freemarker.template-loader-path=classpath:/templates/ 的配置
     * @param xmlParentPathIsAP document.xml文件的父路径是否是绝对路径,true-是,false-否
     * @param xmlTemplateName   document.xml文件的名称,需以.xml结尾
     * @param zipFileUrl        zip文件的路径(将模版docx或者doc文件后缀修改为.zip即可),需以.zip结尾,
     *                          相对路径或者绝对路径,相对路径时指:相对classpath的路径
     * @param zipFileUrlIsAp    zip文件是否是绝对路径,true-是,false-否
     * @param fileName          生成文件的名称,需以.docx或者.doc结尾
     * @return void 无
     * @author: lx
     * @date: 2022/2/22 22:52
     */
    public static synchronized void downWordDocxOrDoc(Object dataModel, String versionCode,
                                                      String encoding, String xmlParentPath,
                                                      boolean xmlParentPathIsAP,
                                                      String xmlTemplateName, String zipFileUrl,
                                                      boolean zipFileUrlIsAp,
                                                      String fileName,
                                                      HttpServletRequest request,
                                                      HttpServletResponse response) throws GenericException,IOException {
        Version version = null;

        if (StringUtils.isBlank(versionCode)) {
            //默认为2.3.23
            version = Configuration.VERSION_2_3_23;
        } else {
            version = new Version(versionCode);
        }
        if (StringUtils.isBlank(encoding)) {
            encoding = DEFAULT_ENCODING;
        }
        if (dataModel == null) {
            LOGGER.error("[xml生成word]数据为空");
            throw new GenericException(PARAM_ERROR_CODE, PARAM_ERROR_DESC);
        }
        if (StringUtils.isBlank(xmlParentPath) || StringUtils.isBlank(xmlTemplateName)
                || StringUtils.isBlank(fileName) || StringUtils.isBlank(zipFileUrl)) {
            LOGGER.error("[xml生成word]xml文件的名字(xmlTemplateName)、" +
                    "zip文件的路径(zipFileUrl)、输出文件名称(fileName)存在空的参数");
            throw new GenericException(PARAM_ERROR_CODE, PARAM_ERROR_DESC);
        }
        if (xmlParentPathIsAP && StringUtils.isBlank(xmlParentPath)) {
            LOGGER.error("[xml生成word]xml文件的父路径(xmlParentPath)不能为空");
            throw new GenericException(PARAM_ERROR_CODE, PARAM_ERROR_DESC);
        }
        //如果xml模版名字不是以.xml结尾的
        if (!xmlTemplateName.endsWith(".xml")) {
            LOGGER.error("[xml生成word]xml模版的名字(xmlTemplateName)名称有误,不是.xml后缀");
            throw new GenericException(PARAM_ERROR_CODE, PARAM_ERROR_DESC);
        }
        if (!zipFileUrl.endsWith(".zip")) {
            LOGGER.error("[xml生成word]zip文件的路径(zipFileUrl)有误,不是.zip后缀");
            throw new GenericException(PARAM_ERROR_CODE, PARAM_ERROR_DESC);
        }

        OutputStream outputStream = response.getOutputStream();
        //设置要下载的文件的名称
        response.setHeader("Content-disposition", "attachment;fileName=" + fileName);
        //设置MIME类型
        response.setContentType("application/msword;charset=UTF-8");

        ByteArrayInputStream in = null;
        ZipOutputStream zipOut = null;
//        FileOutputStream outputStream = null;
        InputStream inputStream = null;
        ZipInputStream zipInputStream = null;
        try {
            //获取模板
            Template template =
                    getConfiguration(version, encoding, xmlParentPath, xmlParentPathIsAP).getTemplate(xmlTemplateName);
            StringWriter swriter = new StringWriter();
            //生成文件
            template.process(dataModel, swriter);
            //这里一定要设置utf-8编码 否则导出的word中中文会是乱码
            in = new ByteArrayInputStream(swriter.toString().getBytes(encoding));
            //生成docx文件的地址
//            outputStream = new FileOutputStream(file);
            //重新打包的zip输出流(新的zip文件)
            zipOut = new ZipOutputStream(outputStream);

            int len = -1;
            byte[] buffer = new byte[1024];

            //如果是绝对路径
            if (zipFileUrlIsAp) {
                //zip文件(其实就是docx文件,改个后缀而已)
                File docxFile = new File(zipFileUrl);
                ZipFile zipFile = new ZipFile(docxFile);
                Enumeration<? extends ZipEntry> zipEntrys = zipFile.entries();
                //开始覆盖文档------------------
                while (zipEntrys.hasMoreElements()) {
                    ZipEntry next = zipEntrys.nextElement();
                    InputStream is = zipFile.getInputStream(next);
                    if (next.toString().indexOf("media") < 0) {
                        zipOut.putNextEntry(new ZipEntry(next.getName()));
                        //如果是word/document.xml由我们输入
                        if ("word/document.xml".equals(next.getName())) {
                            if (in != null) {
                                while ((len = in.read(buffer)) != -1) {
                                    zipOut.write(buffer, 0, len);
                                }
                                in.close();
                                in = null;
                            }
                        } else {
                            while ((len = is.read(buffer)) != -1) {
                                zipOut.write(buffer, 0, len);
                            }
                            is.close();
                        }
                    }
                }
            } else {
                //如果是相对路径,用流读取zip文件
                ClassPathResource resource = new ClassPathResource(zipFileUrl);
                inputStream = resource.getInputStream();
                zipInputStream = new ZipInputStream(inputStream);

                ZipEntry zipEntry = null;
                while ((zipEntry = zipInputStream.getNextEntry()) != null) {
                    if (zipEntry.toString().indexOf("media") < 0) {
                        zipOut.putNextEntry(new ZipEntry(zipEntry.getName()));
                        //如果是word/document.xml由我们输入
                        if ("word/document.xml".equals(zipEntry.getName())) {
                            if (in != null) {
                                while ((len = in.read(buffer)) != -1) {
                                    zipOut.write(buffer, 0, len);
                                }
                                in.close();
                                in = null;
                            }
                        } else {
                            while ((len = zipInputStream.read(buffer)) != -1) {
                                zipOut.write(buffer, 0, len);
                            }
                        }
                    }
                }
                zipInputStream.close();
                inputStream.close();
            }

            LOGGER.info("[xml生成word]生成word文件成功");
        } catch (Exception e) {
            LOGGER.error("[xml生成word]生成word出错", e);
            throw new GenericException(WORD_CREATE_ERROR_CODE, WORD_CREATE_ERROR_DESC);
        } finally {
            try {
                if (zipOut != null) {
                    zipOut.flush();
                    zipOut.close();
                }
                if (outputStream != null) {
                    outputStream.flush();
                    outputStream.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (Exception e) {
                LOGGER.error("", e);
                throw new GenericException(WORD_CREATE_ERROR_CODE, WORD_CREATE_ERROR_DESC);
            }
        }
    }
}
上一篇:Java中堆和栈的区别在哪?


下一篇:monstache同步mongo数据到es并保证高可用