当前位置: 首页 > news >正文

轻量级导出 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文件

相关文章:

轻量级导出 Excel 标准格式

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

蓝桥杯 (年号字串 C++)

思路&#xff1a; 1、看成10进制转化成26进制 。 2、A表示1、B表示2。以此类推&#xff0c;Z表示26. 代码&#xff1a; #include <iostream> using namespace std; int main() {char str[10]; int sum 2019, n, i 0; while (sum > 0) {str[i] sum % 26 64;sum / …...

软件测试01

一、认识软件及测试 1、什么是软件 控制计算机硬件工作的工具 2、软件的基本组成 页面客户端------请求----->代码服务器-------请求------>数据服务器 3、软件产生过程 需求产生------->需求文档------->设计效果图------->产品开发-------->产品测试 …...

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

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

[题] 筛质数 #质数(素数)

题目 AcWing 868. 筛质数 题解 方法一&#xff1a;朴素筛法 及其优化&#xff1a;埃氏筛 从2~n枚举 i,再从小到大枚举所有已知的质数 primes[j],筛掉合数 i*primes[j],遇到新的质数就入队 枚举所有小于n的数i,将i的所有倍数筛掉。 筛完后剩下的数就是质数。 朴素做法 void ge…...

C进阶-语言文件操作

本章重点&#xff1a; 什么是文件 文件名 文件类型 文件缓冲区 文件指针 文件的打开和关闭文件的顺序读写文件的随机读写文件结束的判定 1. 什么是文件 磁盘上的文件是文件。 但是在程序设计中&#xff0c;我们一般谈的文件有两种&#xff1a;程序文件、数据文件 1.1 程序文件…...

17-spring aop调用过程概述

文章目录 1.源码2. debug过程1.源码 public class TestAop {public static void main(String[] args) throws Exception {saveGeneratedCGlibProxyFiles(System.getProperty("user.dir") + "/proxy");ApplicationContext ac = new ClassPathXmlApplicatio…...

微信小程序------框架

目录 视图层 WXML 数据绑定 列表渲染 条件渲染 模板 wsx事件 逻辑层 生命周期 跳转 视图层 WXML WXML&#xff08;WeiXin Markup Language&#xff09;是框架设计的一套标签语言&#xff0c;结合基础组件、事件系统&#xff0c;可以构建出页面的结构。 先在我们的项目中…...

Cross-Modal Joint Embedding with Diverse Semantics

计算两个嵌入之间的相似度得分&#xff0c;然后利用损失函数进行联合嵌入损失最小化优化并更新参数 辅助信息 作者未提供代码...

工具 | macOS 最简方式安装 adb 工具 | Mac

工具 | macOS 最简方式安装 adb 工具 | Mac 介绍 ADB&#xff08;Android Debug Bridge&#xff09;是 Android开发工具包&#xff08;SDK&#xff09;中的一项实用工具&#xff0c;用于与 Android 设备进行通信和调试。 在 macOS 操作系统上安装 ADB 环境可以帮助开发人员与…...

linux进阶(脚本编程/软件安装/进程进阶/系统相关)

一般市第二种,以bash进程执行 shelle脚本编程 env环境变量 set查看所有变量 read设置变量值 echo用于控制台输出 类似java中的sout declear/typeset声明类型 范例 test用于测试表达式 if/else case while for 函数 脚本示例 软件安装及进阶 fork函数(复制一个进程(开启一个进…...

谷歌云:下一代开发者和企业解决方案的强力竞争者

自从2018年Oracle前研发总裁Thomas Kurian加入谷歌云&#xff08;Google Cloud&#xff09;并出任谷歌云CEO以来&#xff0c;业界对于谷歌云的发展就十分好奇。而谷歌云的前任CEO Diane Greene曾是VMware的创始人之一&#xff0c;那么两任企业级技术和解决方案出身的CEO&#x…...

任务分配问题(回溯法)

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

华为OD 字符串消除(100分)【java】A卷+B卷

华为OD统一考试A卷+B卷 新题库说明 你收到的链接上面会标注A卷还是B卷。目前大部分收到的都是B卷。 B卷对应20022部分考题以及新出的题目,A卷对应的是新出的题目。 我将持续更新最新题目 获取更多免费题目可前往夸克网盘下载,请点击以下链接进入: 我用夸克网盘分享了「华为O…...

索引背后的数据结构——B+树

为什么要使用B树&#xff1f; 可以进行数据查询的数据结构有二叉搜索树、哈希表等。对于前者来说&#xff0c;树的高度越高&#xff0c;进行查询比较的时候访问磁盘的次数就越多。而后者只有在数据等于key值的时候才能进行查询&#xff0c;不能进行模糊匹配。所以出现了B树来解…...

面试用-常用注解

Configuration 注意由ConfigurationClassPostProcessor来处理ConfigurationClassPostProcessor执行这个后置处理 ConfigurationClassParser.parse执行这个方法里面会解析很多注解。1、Component 对于Component也是一样递归调用parse方法&#xff0c;一层层解析…...

【c++】跟webrtc学std array 4: H264PacketBuffer 包缓存

H264PacketBuffer m98代码:H264PacketBuffer 类似于PacketBuffer ,但仅用于H264// The H264PacketBuffer does the same job as the PacketBuffer but for H264 // only. To make it fit in with surronding code the PacketBuffer input/output // classes are used. 因此,…...

Nodejs Web数据库应用演示实例

Nodejs Web应用基础演示实例 Web数据库应用 一、服务器端 var express require(express); var app express(); var mysql require(mysql);//设置静态资源目录public app.use(express.static(__dirname /public));//创建mysql数据库访问连接&#xff08;数据库主机地址&a…...

Vue 中setup的特性

特性四&#xff1a;父传子组件传参【defineProps】&#xff1a; 父组件&#xff08;传递数据&#xff09;&#xff1a;利用自定义属性传递数据。 <template><h3>我是父组件</h3><hr /><Child :name"info.name" :age"info.age"…...

Peter算法小课堂—正整数拆分

大家可能会想&#xff1a;正整数拆分谁不会啊&#xff0c;2年级就会了&#xff0c;为啥要学啊 例题 正整数拆分有好几种&#xff0c;这里我们列举两种讲。 关系 我们看着第一幅图&#xff0c;头向左转90&#xff0c;记住你看到的图&#xff0c;再来看第二幅图&#xff0c;你…...

大数据学习栈记——Neo4j的安装与使用

本文介绍图数据库Neofj的安装与使用&#xff0c;操作系统&#xff1a;Ubuntu24.04&#xff0c;Neofj版本&#xff1a;2025.04.0。 Apt安装 Neofj可以进行官网安装&#xff1a;Neo4j Deployment Center - Graph Database & Analytics 我这里安装是添加软件源的方法 最新版…...

label-studio的使用教程(导入本地路径)

文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

Python爬虫实战:研究feedparser库相关技术

1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的信息资源。RSS(Really Simple Syndication)作为一种标准化的信息聚合技术,被广泛用于网站内容的发布和订阅。通过 RSS,用户可以方便地获取网站更新的内容,而无需频繁访问各个网站。 然而,互联网…...

【论文笔记】若干矿井粉尘检测算法概述

总的来说&#xff0c;传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度&#xff0c;通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...

WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成

厌倦手动写WordPress文章&#xff1f;AI自动生成&#xff0c;效率提升10倍&#xff01; 支持多语言、自动配图、定时发布&#xff0c;让内容创作更轻松&#xff01; AI内容生成 → 不想每天写文章&#xff1f;AI一键生成高质量内容&#xff01;多语言支持 → 跨境电商必备&am…...

JUC笔记(上)-复习 涉及死锁 volatile synchronized CAS 原子操作

一、上下文切换 即使单核CPU也可以进行多线程执行代码&#xff0c;CPU会给每个线程分配CPU时间片来实现这个机制。时间片非常短&#xff0c;所以CPU会不断地切换线程执行&#xff0c;从而让我们感觉多个线程是同时执行的。时间片一般是十几毫秒(ms)。通过时间片分配算法执行。…...

云原生玩法三问:构建自定义开发环境

云原生玩法三问&#xff1a;构建自定义开发环境 引言 临时运维一个古董项目&#xff0c;无文档&#xff0c;无环境&#xff0c;无交接人&#xff0c;俗称三无。 运行设备的环境老&#xff0c;本地环境版本高&#xff0c;ssh不过去。正好最近对 腾讯出品的云原生 cnb 感兴趣&…...

Linux中《基础IO》详细介绍

目录 理解"文件"狭义理解广义理解文件操作的归类认知系统角度文件类别 回顾C文件接口打开文件写文件读文件稍作修改&#xff0c;实现简单cat命令 输出信息到显示器&#xff0c;你有哪些方法stdin & stdout & stderr打开文件的方式 系统⽂件I/O⼀种传递标志位…...

高考志愿填报管理系统---开发介绍

高考志愿填报管理系统是一款专为教育机构、学校和教师设计的学生信息管理和志愿填报辅助平台。系统基于Django框架开发&#xff0c;采用现代化的Web技术&#xff0c;为教育工作者提供高效、安全、便捷的学生管理解决方案。 ## &#x1f4cb; 系统概述 ### &#x1f3af; 系统定…...

[拓扑优化] 1.概述

常见的拓扑优化方法有&#xff1a;均匀化法、变密度法、渐进结构优化法、水平集法、移动可变形组件法等。 常见的数值计算方法有&#xff1a;有限元法、有限差分法、边界元法、离散元法、无网格法、扩展有限元法、等几何分析等。 将上述数值计算方法与拓扑优化方法结合&#…...