引言:为什么我们需要封装 excel 导出?
在企业级后台管理系统的开发中,“导出 excel” 绝对是一个高频且让人又爱又恨的需求。
初入职场时,我们可能都会手写原生的 apache poi 代码:创建 workbook,创建 sheet,写死大标题,然后用两层 for 循环把数据一行一行塞进单元格。
这种做法在前期看似简单,但随着业务线的发展,你会遇到以下几个致命痛点:
- 重复劳动: 每次新增一个报表,都要复制粘贴几百行高度相似的 poi 代码,仅仅是改了改表头和字段。
- 字典翻译极其痛苦: 数据库存的是
type = 1,导出时要变成“收入”;存的是时间戳,导出时要格式化。如果在 sql 里连表翻译,会拖慢查询效率;如果在 java 循环里写if-else,代码会变得又长又臭。 - 内存溢出(oom)风险: 如果直接使用
xssfworkbook导出几十万条数据,内存瞬间打满。即使用了基于磁盘流的sxssfworkbook,手动管理的逻辑依然繁杂。
为了解放生产力,我们必须对 excel 导出进行通用化封装。本文将为你提供业界最主流的两套解决方案:无依赖侵入的 java 8 函数式封装,以及阿里开源神器 easyexcel 的接入指南。
方案一:基于 java 8 function 的极致轻量级封装
如果你不想引入庞大的第三方框架,或者项目要求严格控制依赖,那么原生的 poi-ooxml 配合 java 8 的函数式接口(function)是绝佳的选择。
痛点突围:如何避免使用反射?
很多传统封装喜欢用 java 反射来读取对象属性,但这无法优雅地解决“字典翻译”和“复杂格式化”的问题。利用 function<t, object>,我们可以把“如何提取数据”和“如何转换数据”的动作,作为参数传给工具类。
核心工具类代码excelexportutil
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.cellrangeaddress;
import org.apache.poi.xssf.streaming.sxssfworkbook;
import javax.servlet.http.httpservletresponse;
import java.net.urlencoder;
import java.util.list;
import java.util.function.function;
/**
* 通用 excel 导出工具类
*/
public class excelexportutil {
/**
* 核心导出方法
* @param response httpservletresponse
* @param filename 文件名
* @param title 表格大标题(传空则不生成)
* @param headers 表头集合
* @param datalist 数据集合 list<t>
* @param extractors 数据提取规则集合,支持动态翻译和格式化
*/
public static <t> void export(httpservletresponse response,
string filename,
string title,
list<string> headers,
list<t> datalist,
list<function<t, object>> extractors) throws exception {
if (headers.size() != extractors.size()) {
throw new illegalargumentexception("表头列数与数据提取规则数量不一致!");
}
// 使用 sxssfworkbook 防 oom,内存仅保留 100 行
sxssfworkbook workbook = new sxssfworkbook(100);
sheet sheet = workbook.createsheet("sheet1");
int rowindex = 0;
// 1. 生成大标题 (可选)
if (title != null && !title.isempty()) {
row titlerow = sheet.createrow(rowindex++);
cell titlecell = titlerow.createcell(0);
titlecell.setcellvalue(title);
sheet.addmergedregion(new cellrangeaddress(0, 0, 0, headers.size() - 1));
}
// 2. 生成表头
row headerrow = sheet.createrow(rowindex++);
for (int i = 0; i < headers.size(); i++) {
cell cell = headerrow.createcell(i);
cell.setcellvalue(headers.get(i));
sheet.setcolumnwidth(i, 15 * 256); // 简单统一列宽
}
// 3. 填充数据(核心亮点:使用 function 动态取值并转换)
for (t data : datalist) {
row row = sheet.createrow(rowindex++);
for (int i = 0; i < extractors.size(); i++) {
cell cell = row.createcell(i);
// 执行外部传入的 lambda 表达式
object value = extractors.get(i).apply(data);
if (value != null) {
if (value instanceof number) {
cell.setcellvalue(((number) value).doublevalue());
} else {
cell.setcellvalue(value.tostring());
}
} else {
cell.setcellvalue("");
}
}
}
// 4. 写出响应流
response.setcontenttype("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setcharacterencoding("utf-8");
string encodedfilename = urlencoder.encode(filename, "utf-8").replaceall("\\+", "%20");
response.setheader("content-disposition", "attachment;filename*=utf-8''" + encodedfilename + ".xlsx");
try {
workbook.write(response.getoutputstream());
} finally {
workbook.dispose(); // 清理磁盘临时文件
workbook.close();
}
}
}实战调用:爽到飞起的 controller
有了这个工具类,controller 层的代码变得极其清爽,甚至可以在一行代码内完成复杂的字典翻译和日期格式化。
@postmapping("/exportfinancerecord")
public void exportfinancerecord(httpservletresponse response, @requestbody financequery param) throws exception {
list<financerecord> recordlist = financerecordservice.list(param);
simpledateformat sdf = new simpledateformat("yyyy-mm-dd hh:mm:ss");
// 定义表头
list<string> headers = arrays.aslist("记录id", "收支类型", "金额", "创建时间");
// 定义数据提取与转换规则(利用 lambda 表达式)
list<function<financerecord, object>> extractors = arrays.aslist(
financerecord::getid,
record -> record.gettype() == 1 ? "收入" : "支出", // 完美解决字典翻译
financerecord::getamount,
record -> record.getcreatetime() != null ? sdf.format(record.getcreatetime()) : "" // 日期安全格式化
);
// 一行代码调用导出
excelexportutil.export(response, "财务收支记录表", "财务明细大表", headers, recordlist, extractors);
}
点评: 这种方案没有引入任何第三方黑科技,可读性极强,且没有任何反射带来的性能损耗,非常适合中小型项目。
方案二:拥抱阿里开源生态,easyexcel 的降维打击
如果你的系统是一个复杂的后台管理平台,未来面临着几十上百个报表的导入导出,且动辄几十万级的数据量。别犹豫了,直接拥抱 alibaba easyexcel。
相比于原生的 poi,easyexcel 就是一辆造好的跑车。它基于注解驱动,彻底重写了 poi 对 07 版 excel 的解析引擎,极大地降低了内存占用。
1. 定义专属导出 vo
不要直接把数据库的 entity 拿去导出,专门建一个 view object (vo),利用注解定义一切。
import com.alibaba.excel.annotation.excelproperty;
import com.alibaba.excel.annotation.format.datetimeformat;
import com.alibaba.excel.annotation.write.style.columnwidth;
import lombok.data;
import java.util.date;
@data
public class financerecordexportvo {
@excelproperty("记录id")
@columnwidth(12)
private integer id;
// 翻译逻辑可以在 service 层查询后处理,或者实现 easyexcel 的 converter
@excelproperty("收支类型")
@columnwidth(12)
private string typestr;
@excelproperty("金额")
@columnwidth(15)
private double amount;
@excelproperty("创建时间")
@columnwidth(22)
@datetimeformat("yyyy-mm-dd hh:mm:ss") // 自带强大的格式化注解
private date createtime;
}
2. 极致优雅的 controller
@postmapping("/exportfinancerecord")
public void exportfinancerecord(httpservletresponse response, @requestbody financequery param) throws exception {
// 1. 查询数据并转换为 vo 集合 (推荐使用 mapstruct 等工具)
list<financerecordexportvo> volist = getandconvertdata(param);
// 2. 设置响应头
response.setcontenttype("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setcharacterencoding("utf-8");
string filename = urlencoder.encode("财务收支记录", "utf-8");
response.setheader("content-disposition", "attachment;filename*=utf-8''" + filename + ".xlsx");
// 3. 一行代码完成写入!
easyexcel.write(response.getoutputstream(), financerecordexportvo.class)
.sheet("数据明细")
.dowrite(volist);
}
点评: 从列宽、时间格式化到样式控制,一切皆可通过注解完成。对于复杂的分页分批写入、复杂表头,easyexcel 都有着不可替代的优势。
避坑指南:引入 easyexcel 的依赖冲突问题
如果你在项目中原本已经引入了 poi 或 poi-ooxml,当你满心欢喜地加入 easyexcel 依赖后,运行项目时你大概率会遇到 nosuchmethoderror 或 classnotfoundexception。
原因分析:
easyexcel 的底层强依赖了特定版本的 poi。如果你原来的 pom.xml 中指定了较老的 poi 版本,就会覆盖掉 easyexcel 真正需要的版本,导致运行时崩溃。
最佳解决姿势(直接“卸磨杀驴”):
既然 easyexcel 已经包含了 poi,最清爽的做法就是直接把原有独立的 poi 依赖全部删除或注释掉,全权交给 easyexcel 管理。
<dependency>
<groupid>com.alibaba</groupid>
<artifactid>easyexcel</artifactid>
<version>3.3.4</version> </dependency>注:即使你移除了单独的 poi 依赖,你依然可以在项目中使用原生的 poi api(如 sxssfworkbook),因为依赖已经被 easyexcel 传递带入,且版本绝对安全。
总结
- 场景选择: 如果只是零星几个简单的表格导出,追求代码零外部侵入,方案一(java 8 function) 绝对能让你眼前一亮。
- 架构演进: 如果是长期维护、报表繁多、数据量大的企业级后台,强烈建议使用方案二(easyexcel)。它不仅仅是一个导出工具,更是处理海量数据的标准组件。
希望这篇文章能帮你告别痛苦的 excel 导出代码编写,把时间留给更有价值的业务逻辑!
以上就是springboot导出excel的最佳实践的详细内容,更多关于springboot导出excel的资料请关注代码网其它相关文章!
发表评论