轻量级导出 Excel 标准格式

一般业务系统中都有导出到 Excel 功能,其实质就是把数据库里面一条条记录转换到 Excel 文件上。Java 常用的第三方类库有 Apache POI 和阿里巴巴开源的 EasyExcel 等。另外也有通过 Web 模板技术渲染 Excel 文件导出,这实质是 MVC 模式的延伸,数据转为成不同的视图罢了。

网上很多文章介绍用 Freemarker 模板渲染,应用这一机制的问题不大,本文也是遵循此思路,但没有依赖 Freemarker,而是 Java Servlet 原生的 JSP 模板机制,更加轻量级。

常见的问题

网上文章都介绍模板来自 Excel 另存为 xml 格式的,渲染然后改扩展名为 xls,xml 是文本文件当然可以轻易修改。但致命的问题是,Office Excel 打开的话会有对话框的警告提示,对用户而言非常错愕,用户自然觉得此 Excel 有什么问题,但确认后又可正常显示。在 WPS/LiberOffice 却没有这警告。

在这里插入图片描述
有没有办法绕过这提示呢?直接的方法好像没有,只要是 xml 纯文本的格式就绕不过。我想到了导出 word,同样也是 Freemarker 渲染,但更高明地,使用 zip 压缩包的文档格式,而非 xml 纯文本。我想,能不能在 Excel 上面亦如此炮制呢?

可惜的是,搜遍全网也没发现有类似的思路。但皇天不负有心人,我多次尝试后,亦发现此法可在 Excel 上成功。

使用步骤

新建 Excel 模板

新建 Excel 文档,有标题和模板填充占位符。我喜欢用 LiberOffice 的 Calc,亦无问题。

在这里插入图片描述
诸如${item.orderNo},显然是 JSP 的 EL 表达式。别告诉我你不会,这是最基础的 Java Web 开发内容。item 是固定的,后面的实际字段取值 key。

当然 EL 表达式能够支持的,这里你也同样可以写,如${xxx == 1 ? 'yes' : 'no'},不过建议在前面的数据层面就处理,这里直接显示了。

编辑好模板之后,保存为xlsx格式,注意是 xlsx 而非 xls,因为 xlsx 是 ZIP 压缩包而 xls 不是。

xlsx 文件等下还需要被使用的,将其放到工程的资源目录下。
在这里插入图片描述

提取模板

解压缩这个 xlsx 包,强制解压。这里我用 PeaZip,其他 7Zip、WinRaR 的工具一样。
在这里插入图片描述
找到目录xl/worksheets这里的文件sheet1.xml,1 表示第一个工作簿,如此类推。

在这里插入图片描述
复制这个 sheet1.xml 到 Web 模板可读取的位置。什么意思呢?就是 Servlet 可以渲染此模板,填充数据的目录。这个 xml 是变成 JSP 文件的。根据 Servlet 3.0 规范,META-INF/resources就是 WebRoot,可以放置 HTM/CSS/JS/JSP,就算打包成 SpringBoot 的 jar 包可以。所以,一般这个 xml 就放到META-INF/resources中。

在这里插入图片描述
但又因,这里相当于 WebRoot,浏览器可以直接访问的,那么,放到META-INF/resources/WEB-INF/下似乎更好。

修改模板

当前模板还是 xml,先别急,用代码编辑器(如 VS-code)格式化下先,再改名 jsp 不迟。

然后加入文件头:<%@ page trimDirectiveWhitespaces="true" contentType="text/html; charset=UTF-8" import="java.util.*"%>,不然你会中文乱码的。
在这里插入图片描述
找到刚输入的 EL 表达式部分,要重新梳理下。因为 Excel SharedStrings 的缘故,你很可能找不到那些 EL 表达式字符串,没关系,大概就是节点<sheetData>下的第二个<row>节点(第一个是表头)。

重新梳理后的结果如下:

在这里插入图片描述
列表循环,这里的for很好理解,就是基础 Web 开发知识。

  <%List<Map<String, Object>> list = (List<Map<String, Object>>) request.getAttribute("list");for(Map<String, Object> map : list) {request.setAttribute("item", map);%>

记得for后面的结束括号,别忘了加:
在这里插入图片描述
这里为什么要request.setAttribute("item", map);然后通过 EL 表达式取值呢?为什么不用<%=map.get("xxx")%>? 后者方式也行,但如果是 null 值就会显示 null,${item.statusName}的方式不会。

此时模板就搞定了。

渲染

有模板有数据就可以渲染了。假设是数据是List<Map<String, Object>> list,另外要有对象HttpServletRequest req, HttpServletResponse resp,下面就可渲染了。

Export e = new Export();
e.setIsXsl(true);
e.setIsOfficeZipInRes(true);
e.setTplJsp("/short-trade-new.jsp");
e.setOfficeZip("short-trade.xlsx");
e.setRespOutput(resp, "交易流水 " + DateUtil.now(DateUtil.DATE_FORMAT_SHORTER) + ".xlsx");
e.renderOffice(list, req, resp);

这是渲染到Response的,就是浏览器会直接提示下载的。如果你想保存到文件而非下载。去掉setRespOutput()并设置setOutputPath()保存路径即可。

看看这个单测,就是读取 xml 模板生成 xlsx 的

public static ByteArrayOutputStream p(String path) {File file = new File(path);try (FileInputStream fis = new FileInputStream(file); ByteArrayOutputStream bos = new ByteArrayOutputStream()) {byte[] buffer = new byte[1024];int len;while ((len = fis.read(buffer)) != -1)bos.write(buffer, 0, len);return bos;} catch (IOException e) {e.printStackTrace();}return null;
}@Test
public void replaceXsl() {String newXml = "C:\\code\\car-short-rental\\src\\main\\resources\\META-INF\\resources\\short-trade-new.xml";Export e = new Export();e.setIsXsl(true);e.setOfficeZip("C:\\code\\car-short-rental\\src\\main\\resources\\short-trade.xlsx");e.setOutputPath("C:\\temp\\test.xlsx");e.zip(p(newXml));
//        e.zip(new ByteArrayServletOutputStream(p(newXml)));
}

源码

这个 Office 导出工具包,不但可以导出 Excel 还可以导出 Word 的,三个类去掉注释才 200 多行源码,足够精简。

package com.ajaxjs.tools.office_export;import com.ajaxjs.util.io.Resources;
import com.ajaxjs.util.io.StreamHelper;
import com.ajaxjs.util.logger.LogHelper;
import lombok.Data;import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;/*** Office 导出*/
@Data
public class Export {private static final LogHelper LOGGER = LogHelper.getLog(Export.class);/*** 模板 XML 文件*/private String tplXml;/*** 模板 JSP 文件,必须 / 开头,以及 .jsp 结尾*/private String tplJsp;/*** 原始 docx/xlsx 文档,其实是个 zip 包,我们取其结构,会替换里面的 xml*/private String officeZip;/*** 是否在资源文件目录*/private Boolean isOfficeZipInRes;private File officeZipRes;/*** 导出的 docx/xlsx 位置*/private String outputPath;/*** true=Excel 文件*/private Boolean isXsl;/*** 浏览器下载文件。如果设置该属性,表示浏览器下载文件,否则保存到文件*/private HttpServletResponse respOutput;/*** 浏览器下载文件。如果设置该属性,表示浏览器下载文件,否则保存到文件** @param respOutput 响应对象* @param fileName   下载的文件名*/public void setRespOutput(HttpServletResponse respOutput, String fileName) {this.respOutput = respOutput;respOutput.setContentType("application/vnd.ms-excel");respOutput.setHeader("Content-Disposition", "attachment; filename=\"" + Utils.encodeFileName(fileName) + "\"");}public void renderOffice(Object data, HttpServletRequest req, HttpServletResponse resp) {if (isXsl) {List<Map<String, Object>> list = (List<Map<String, Object>>) data;req.setAttribute("list", list); // 内容数据} else {Map<String, Object> map = (Map<String, Object>) data;for (String key : map.keySet())req.setAttribute(key, map.get(key)); // 内容数据}if (!tplJsp.startsWith("/"))throw new IllegalArgumentException("参数 tplJsp 必须以 / 开头");RequestDispatcher rd = req.getServletContext().getRequestDispatcher(tplJsp);try (ByteArrayServletOutputStream stream = new ByteArrayServletOutputStream();PrintWriter pw = new PrintWriter(new OutputStreamWriter(stream.getOut(), StandardCharsets.UTF_8));) {rd.include(req, new HttpServletResponseWrapper(resp) {@Overridepublic ServletOutputStream getOutputStream() {return stream;}@Overridepublic PrintWriter getWriter() {return pw;}});pw.flush();officeZipRes = input2file(officeZip);zip(stream);officeZipRes.delete();} catch (IOException | ServletException e) {LOGGER.warning(e);}}/*** 替换 Zip 包中的 XML** @param stream 文件流*/void zip(ByteArrayServletOutputStream stream) {zip(stream.getOut());}/*** 替换 Zip 包中的 XML** @param stream 文件流*/void zip(ByteArrayOutputStream stream) {int len;byte[] buffer = new byte[1024];try (ZipFile zipFile = isOfficeZipInRes ? new ZipFile(officeZipRes) : new ZipFile(officeZip); // 原压缩包ZipOutputStream zipOut = new ZipOutputStream(respOutput == null ? Files.newOutputStream(Paths.get(outputPath)) : respOutput.getOutputStream()) /* 输出的 */) {Enumeration<? extends ZipEntry> zipEntry = zipFile.entries();
//            ByteArrayInputStream imgData = img((List<Map<String, Object>>) dataMap.get("picList"), zipOut, dataMap, resXml);String targetXml = isXsl ? "xl/worksheets/sheet1.xml" : "word/document.xml";
//// 开始覆盖文档------------------while (zipEntry.hasMoreElements()) {ZipEntry entry = zipEntry.nextElement();try (InputStream is = zipFile.getInputStream(entry)) {zipOut.putNextEntry(new ZipEntry(entry.getName()));if (entry.getName().indexOf("document.xml.rels") > 0) { //如果是document.xml.rels由我们输入
//                        if (documentXmlRelsInput != null) {
//                            while ((len = documentXmlRelsInput.read(buffer)) != -1) zipOut.write(buffer, 0, len);
//
//                            documentXmlRelsInput.close();
//                        }while ((len = is.read(buffer)) != -1) zipOut.write(buffer, 0, len);} else if (targetXml.equals(entry.getName())) {//如果是word/document.xml由我们输入stream.writeTo(zipOut);} else {while ((len = is.read(buffer)) != -1) zipOut.write(buffer, 0, len);}}}} catch (IOException e) {LOGGER.warning(e);}}/*** 从资源目录中获取文件对象,兼容 JAR 包的方式** @param resourcePath 资源文件* @return 文件对象*/public static File input2file(String resourcePath) {try {File outputFile = File.createTempFile("outputFile", ".docx");// 创建临时文件// 创建输出流try (InputStream input = Resources.getResource(resourcePath);OutputStream output = Files.newOutputStream(outputFile.toPath())) {StreamHelper.write(input, output, false);}return outputFile;} catch (IOException e) {LOGGER.warning(e);}return null;}
}

完整的代码在这里。

参考

  • 使用Freemarker填充模板导出复杂Excel,其实很简单哒!
  • OOXML:详解Excel共享字符串(sharedStrings)
  • 掀开面纱,看看Excel文件到底是什么
  • 使用Freemarker模版导出xls文件使用excel打开提示文件损坏
  • 一次大数据量导出优化–借助xml导出xls、xlsx文件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/112280.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

nfs+rpcbind实现服务器之间的文件共享

NFS简介 NFS服务及Network File System&#xff0c;用于在网络上共享存储&#xff0c;分为2,3,4三个版本&#xff0c;最新为4.1版本。NFS基于RPC协议&#xff0c;RPC为Remote Procedure Call的简写。 应用场景&#xff1a;用于A,B,C三台机器上需要保证被访问到的文件是一样…

2023阿里云双十一到底有没有活动?去年就没有

2023阿里云双十一到底有没有活动&#xff1f;根据以往经验&#xff0c;阿里云双11是一次大型促销活动&#xff0c;但是去年好像就没有&#xff0c;印象里去年阿里云没推出双十一活动&#xff0c;因为阿里云一直都活动&#xff0c;没有单独推出双11优惠&#xff0c;阿里云百科给…

发现一不错的编程助手 Amazon CodeWhisperer

Amazon CodeWhisperer 是一款 AI 编程助手&#xff0c;旨在为开发人员提供智能化的编程辅助工具。作为一款基于人工智能的编程助手&#xff0c;CodeWhisperer 的目标是提高开发人员的生产效率、降低开发成本&#xff0c;并提供高质量的编程解决方案。 1.安装过程参考官网 htt…

驱动实现LED点灯

demo.c #include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/io.h> #include "head.h" //定义三个指针指向映射后的虚拟内存 unsigned int *vir_moder; unsigned …

YOLOv8改进实战 | 更换主干网络Backbone(四)之轻量化模型MobileNetV3

前言 轻量化网络设计是一种针对移动设备等资源受限环境的深度学习模型设计方法。下面是一些常见的轻量化网络设计方法: 网络剪枝:移除神经网络中冗余的连接和参数,以达到模型压缩和加速的目的。分组卷积:将卷积操作分解为若干个较小的卷积操作,并将它们分别作用于输入的不…

【Qt之布局】QVBoxLayout、QHBoxLayout、QGridLayout、QFormLayout介绍及使用

在Qt中&#xff0c;布局管理器&#xff08;Layout&#xff09;用于管理窗口中的控件的位置和大小&#xff0c;以适应不同大小的窗口。 常用的布局管理器包括QVBoxLayout、QHBoxLayout、QGridLayout和QFormLayout。 先放张布局UI&#xff1a; 1. QVBoxLayout&#xff08;垂直布…

百分点科技受邀参加“一带一路”国际合作高峰论坛

10月17-18日&#xff0c;第三届“一带一路”国际合作高峰论坛在北京成功举行。作为新一代信息技术出海企业代表&#xff0c;百分点科技董事长兼CEO苏萌受邀出席高峰论坛开场活动——“一带一路”企业家大会&#xff0c;与来自82个国家和地区的企业或机构、有关国际组织、经济机…

【IBIS 模型与仿真 - IBISWriter and Write_IBIS】

本文将介绍如何从用户设计中编写自定义IBIS模型。 本文是 SelectIO 解决方案中心&#xff08;Xilinx 答复 50924&#xff09;的设计助手部分&#xff08;Xilinx 答复 50926&#xff09;的一部分。 原文链接&#xff1a;https://support.xilinx.com/s/article/50957?languagee…

2022年亚太杯APMCM数学建模大赛E题有多少核弹可以摧毁地球求解全过程文档及程序

2022年亚太杯APMCM数学建模大赛 E题 有多少核弹可以摧毁地球 原题再现 1945年8月6日&#xff0c;第二次世界大战即将结束。为了尽快结束战争&#xff0c;美国在日本广岛投下了下一颗名为“小男孩”的原子弹。这样一颗原子弹在广岛炸死了20万人&#xff0c;广岛的所有建筑物都…

VueRouter 源码解析

重要函数思维导图 路由注册 在开始之前&#xff0c;推荐大家 clone 一份源码对照着看。因为篇幅较长&#xff0c;函数间的跳转也很多。 使用路由之前&#xff0c;需要调用 Vue.use(VueRouter)&#xff0c;这是因为让插件可以使用 Vue export function initUse(Vue: GlobalAP…

EDUSRC--简单打穿某985之旅

免责声明&#xff1a; 文章中涉及的漏洞均已修复&#xff0c;敏感信息均已做打码处理&#xff0c;文章仅做经验分享用途&#xff0c;切勿当真&#xff0c;未授权的攻击属于非法行为&#xff01;文章中敏感信息均已做多层打马处理。传播、利用本文章所提供的信息而造成的任何直…

idea leetcode配置

idea leetcode配置 配置页面如下图所示&#xff0c;根据需要&#xff0c;填入登录用户名、密码、文件存放路径&#xff0c;注意如果要使用自定义的代码结构配置&#xff0c;要勾选图中框出来的选项。 Code FileName&#xff1a; $!velocityTool.camelCaseName(${question.tit…

CentOS7.9离线安装 Nginx

1. 下载Nginx安装包 下载地址&#xff1a;http://nginx.org/download/nginx-1.20.1.tar.gzhttp://nginx.org/download/nginx-1.20.1.tar.gz 2. 找到Nginx安装时需要的依赖包 我这里是下载了CentOS7.9的安装镜像 阿里下载地址&#xff1a;centos-7.9.2009-isos-x86_64安装包…

Opencv之RANSAC算法用于直线拟合及特征点集匹配详解

Opencv之RANSAC算法用于直线拟合及特征点集匹配详解 讲述Ransac拟合与最小二乘在曲线拟合上的优缺点 讲述在进行特征点匹配时&#xff0c;最近邻匹配与Ransac匹配的不同之处 另外&#xff0c;Ransac也被用于椭圆拟合、变换矩阵求解等 1. 直线拟合 1.1 原理 RANSAC(RANdom …

CUDA 学习记录

1.关于volatile&#xff1a; 对于文章中这个函数&#xff0c; __global__ void reduceUnrollWarps8 (int *g_idata, int *g_odata, unsigned int n) {// set thread IDunsigned int tid threadIdx.x;unsigned int idx blockIdx.x * blockDim.x * 8 threadIdx.x;// convert…

操作系统:线程同步和调度

文章目录 线程同步和调度一、实验目的二、实验要求与内容、过程与结果 系列文章 线程同步和调度 一、实验目的 通过创建线程、分配线程优先级和终止线程的程序设计和调试操作&#xff0c;进一步熟悉操作系统的线程概念&#xff0c;理解Windows 2000线程的生命周期。 通过对事…

18-spring 事务

文章目录 1. xml和注解配置方式的对象2.spring事务传播特性3. 注解事务的初始化流程4. 创建事务信息流程图5. 事务回滚流程图1. xml和注解配置方式的对象 2.spring事务传播特性 事务传播行为类型说明PROPAGATION_REQUIRED如果当前没有事务,就新建一个事务,如果已经存在一个事…

RHEL 8.6 Kubespray 1.23.0 install kubernetes v1.27.5

文章目录 1. 预备条件2. download01 节点 安装 dockerdownload01 节点 介质下载下载 bastion01节点配置 yum 源bastion01 节点安装 docker5. 安装 docker insecure registrybastion01 部署 nginx 与 镜像入库13.1 配置 config.sh13.2 配置 setup-docker.sh13.3 配置 start-ngin…

Unity中Shader实现UI流光效果

文章目录 前言一、实现思路1&#xff1a;1、采集两张贴图&#xff0c;一张是主纹理&#xff0c;一张是扫光纹理2、在 v2f 定义一个二维变量 “uv2” 来存放 uv 偏移后的值3、在顶点着色器中&#xff0c;仿照之前的 uv 流动效果,与 _Time相乘后存放于 uv2 中4、最后&#xff0c;…

任务分配问题(回溯法)

算法设计 问题描述 有n&#xff08;n≥1&#xff09;个任务需要分配给n个人执行&#xff0c;每个任务只能分配给一个人&#xff0c;每个人只能执行一个任务。 第i个人执行第j个任务的成本是c[i][j]&#xff08;1≤i&#xff0c;j≤n&#xff09;。求出总成本最小的分配方案 …