Skip to content

JQuick-Excel 完整 Demo

This content is not available in your language yet.

JQuick-Excel 的用法可以概括为「一份 XML 规则、一个 Java 接口、一次代理调用」。这一章不复述参数清单,而是把一份能跑通的工程从零搭出来:先准备规则文件与服务契约,再分别演示导入、导出、Web 下载与性能配置,最后给出截图清单。

本章的示例数据是一条「学生信息」流水线:同一份 jquick-excel.xml 既能把业务数据导出成带格式、合并、公式、图表与页脚的 Excel,也能把 Excel 读回成带字段映射、字典翻译与数据校验的 JQuickRow 列表。

  1. 规则放在类路径的 jquick-excel.xml 里。根节点 <excels> 的 namespace 必须是服务接口的全限定名,每个 <excel name="..."> 的 name 必须是接口里的方法名,两者对不上会在 createApi 时报错。
  2. 接口方法名 = 规则名,返回类型要匹配。导入方法返回 List<JQuickRow>,导出方法返回 void;参数统一用 @Param 标注(示例里这两个参数不参与规则计算,仅用于保持与框架签名一致)。
  3. 导入和导出使用不同的 ParseFactory。导入传 InputStream,导出传 List<JQuickRow> + OutputStream;需要字典等上下文时,再额外传入一个 JContext。
jquick-excel-demo/
├── src/main/resources/
│ └── jquick-excel.xml 规则文件(namespace = 服务接口全限定名)
└── src/main/java/com/demo/jquick/
├── JQuickExcelDemoService.java 服务接口(方法名 = <excel name>)
├── ImportDemoGenerator.java 生成导入用的测试文件 importDemo.xlsx
├── ImportSheetNameDemo.java 按工作表名称导入
├── ImportSheetIndexDemo.java 按工作表索引导入
├── ImportMappingTransformDemo.java MAPPING + TRANSFORM 导入
├── ImportValidationDemo.java VALIDATION 校验通过/失败
├── ImportBatchDemo.java 大批量分批导入
├── ExportBasicDemo.java 基础导出
├── ExportFormatDemo.java FORMAT 显示格式
├── ExportStyleDemo.java STYLE 单元格样式
├── ExportMergeDemo.java MERGE 行合并 / 列合并
├── ExportFormulasDemo.java FORMULAS 三类目标
├── ExportFormulasRowRangeDemo.java FORMULAS 行区间目标
├── ExportGraphDemo.java GRAPH 图表
├── ExportFooterDemo.java FOOTER 固定页脚 + 动态页脚
├── ExportThemeXmlDemo.java 主题(XML 构造器首参)
├── ExportThemeProgrammaticDemo.java 主题(编程式 setTheme)
├── IoSourceDemo.java 五种输入输出源
├── ConfigDemo.java JQuickExcelConfig 全局配置
└── WebDownloadController.java Spring Boot 浏览器下载
<dependency>
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-excel</artifactId>
<version>3.6.0</version>
</dependency>

为了让文档里的路径可以直接复现,示例把读、写、截图三个目录固定下来,代码中直接写这些绝对路径:

用途 本地路径 说明
导入测试文件目录 D:/idea/jquick-docs/public/jquick-excel/import 只读,测试文件预先放在这里
导出文件输出目录 D:/idea/jquick-docs/public/jquick-excel/export 程序运行后生成,无需手工建文件
运行截图目录 D:/idea/jquick-docs/public/jquick-excel/image 本章截图统一存放于此

导入 Demo 需要一个固定的测试文件。下面这段代码用 POI 直接生成 importDemo.xlsx,工作表名「学生信息」,9 列表头 + 3 行数据。注意所有单元格都按文本写入,这一点直接影响后面 VALIDATION 小节里 integer 规则的表现。

package com.demo.jquick;
import java.io.FileOutputStream;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public class ImportDemoGenerator {
public static void main(String[] args) throws Exception {
try (XSSFWorkbook wb = new XSSFWorkbook()) {
Sheet sheet = wb.createSheet("学生信息");
String[] headers = {"学号", "姓名", "性别", "年龄", "出生日期", "班级", "邮箱", "手机号", "分数"};
Row h = sheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
h.createCell(i).setCellValue(headers[i]);
}
Object[][] data = {
{"2024001", "张三", "男", "20", "2004-09-01", "计算机1班", "[email protected]", "13800000001", "88.5"},
{"2024002", "李四", "女", "21", "2003-09-01", "计算机2班", "[email protected]", "13800000002", "92.0"},
{"2024003", "王五", "男", "22", "2002-09-02", "计算机3班", "[email protected]", "13800000003", "79.5"},
};
for (int r = 0; r < data.length; r++) {
Row row = sheet.createRow(r + 1);
for (int c = 0; c < data[r].length; c++) {
row.createCell(c).setCellValue((String) data[r][c]);
}
}
for (int i = 0; i < headers.length; i++) {
sheet.autoSizeColumn(i);
}
String out = "D:/idea/jquick-docs/public/jquick-excel/import/importDemo.xlsx";
try (FileOutputStream fos = new FileOutputStream(out)) {
wb.write(fos);
}
System.out.println("已生成: " + out);
}
}
}

生成结果可以直接下载对照:importDemo.xlsx。

全部规则集中在一个文件里。根节点 namespace = 服务接口全限定名;每个 <excel> 的 name = 接口方法名;returnClass 与接口返回类型对应;规则体写在 CDATA 中,从 IMPORT WITH 或 EXPORT WITH 开始。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE excels PUBLIC "-//PAOHAIJIAO//DTD API EXCEL 1.0//EN"
"classpath:paohaijiao/dtd/Jquick-excel.dtd">
<excels namespace="com.demo.jquick.JQuickExcelDemoService">
<!-- ============ 导入 ============ -->
<excel name="importBySheetName" returnClass="java.util.List">
<![CDATA[
IMPORT WITH SHEET="学生信息", HEADER=true
]]>
</excel>
<excel name="importBySheetIndex" returnClass="java.util.List">
<![CDATA[
IMPORT WITH SHEET=1, HEADER=true
]]>
</excel>
<excel name="importWithMappingTransform" returnClass="java.util.List">
<![CDATA[
IMPORT WITH SHEET=1, HEADER=true,
MAPPING={"学号":"no","姓名":"name","性别":"sex","年龄":"age","出生日期":"birthday","班级":"className"},
TRANSFORM={"name":toUpper(${name}),"sex":trans(${dict},${sex}),"birthday":dateFormat(${birthday},'yyyy-MM-dd')}
]]>
</excel>
<excel name="importWithValidation" returnClass="java.util.List">
<![CDATA[
IMPORT WITH SHEET=1, HEADER=true, VALIDATION={
COL A:{max_length{required:true,msg:'学号最长10位',map:{maxLength:10}}},
A2:A4:{min_length{required:true,msg:'学号至少4位',map:{minLength:4}}},
COL B:{min_length{required:true,msg:'姓名至少2个字',map:{minLength:2}}},
B2:B4:{max_length{required:true,msg:'姓名最长20个字',map:{maxLength:20}}},
C2:{dict{required:true,msg:'性别必须是男或女',map:{'男':'男','女':'女'}}},
D2:D4:{integer{required:true,msg:'年龄必须是整数'}},
D2:D4:{min_value{required:true,msg:'年龄不能为负',map:{minValue:0}}},
D2:{max_value{required:true,msg:'年龄不能超过150',map:{maxValue:150}}},
E2:E4:{date_format{required:true,msg:'出生日期格式须为yyyy-MM-dd',map:{format:'yyyy-MM-dd'}}},
E2:E4:{min_date{required:true,msg:'出生日期不能早于1990-01-01',map:{format:'yyyy-MM-dd',minDate:1990-01-01}}},
E2:{max_date{required:true,msg:'出生日期不能晚于2010-12-31',map:{format:'yyyy-MM-dd',maxDate:2010-12-31}}},
G2:G4:{email{required:true,msg:'邮箱格式无效'}},
G2:G4:{end_with{required:true,msg:'邮箱必须以.com结尾',map:{endWith:'.com'}}},
G2:{contain{required:true,msg:'邮箱必须包含@',map:{contains:'@'}}},
H2:H4:{mobile{required:true,msg:'手机号格式无效'}},
H2:H4:{regex{required:true,msg:'手机号必须是11位数字',map:{pattern:'^1[0-9]{10}$'}}},
H2:{start_with{required:true,msg:'手机号必须以138开头',map:{startWith:'138'}}},
I2:I4:{decimal{required:true,msg:'分数必须是数值'}},
I2:I4:{max_value{required:true,msg:'分数不能超过100',map:{maxValue:100}}},
I3:{min_value{required:true,msg:'分数不能为负',map:{minValue:0}}},
ROW 2:{max_length{required:true,msg:'第2行存在超长单元格',map:{maxLength:30}}},
ROW 1..4:{max_length{required:true,msg:'存在超长单元格',map:{maxLength:40}}}
}
]]>
</excel>
<excel name="importWithValidationFail" returnClass="java.util.List">
<![CDATA[
IMPORT WITH SHEET=1, HEADER=true, VALIDATION={
COL A:{max_length{required:true,msg:'学号超过3位(演示失败)',map:{maxLength:3}}}
}
]]>
</excel>
<!-- ============ 导出 ============ -->
<excel name="exportBasic" returnClass="void">
<![CDATA[
EXPORT WITH SHEET="基础导出", HEADER=true,
MAPPING={"a":"姓名","b":"性别","c":"年龄"},
TRANSFORM={"b":trans(${dict},${b}),"a":toUpper(${a})}
]]>
</excel>
<excel name="exportFormat" returnClass="void">
<![CDATA[
EXPORT WITH SHEET="格式", HEADER=true,
MAPPING={"a":"工号","b":"姓名","c":"金额","d":"人民币","e":"比率","f":"入职日期"},
FORMAT={"a":"000000","c":"#,##0.00","d":"¥#,##0.00","e":"0.00%","f":"yyyy-MM-dd"}
]]>
</excel>
<excel name="exportStyle" returnClass="void">
<![CDATA[
EXPORT WITH SHEET="样式", HEADER=true,
MAPPING={"a":"姓名","b":"年龄","c":"班级"},
STYLE={
ROW 1:{fontName:Arial,fontHeightInPoints:12,bold:true,italic:true,color:yellow,fillPattern:solid_foreground,fillForegroundColor:darkBlue,alignment:center},
A2:C100:{alignment:center,wrapText:true,borderBottom:thin,fillForegroundColor:lightYellow}
}
]]>
</excel>
<excel name="exportMerge" returnClass="void">
<![CDATA[
EXPORT WITH SHEET="合并-行", HEADER=true,
MAPPING={"a":"部门","b":"姓名","c":"一季度","d":"二季度","e":"三季度","f":"四季度"},
MERGE:{ROWS 1 WITH MERGE_WITH_FIRST, ROWS 2 WITH MERGE_WITH_CONCAT}
]]>
</excel>
<excel name="exportMergeCols" returnClass="void">
<![CDATA[
EXPORT WITH SHEET="合并-列", HEADER=true,
MAPPING={"a":"部门","b":"姓名","c":"一季度","d":"二季度","e":"三季度","f":"四季度"},
MERGE:{COLS A..F WITH MERGE_WITH_FIRST}
]]>
</excel>
<excel name="exportFormulas" returnClass="void">
<![CDATA[
EXPORT WITH SHEET="公式", HEADER=true,
MAPPING={"a":"项目","b":"一月","c":"二月","d":"三月"},
FORMULAS={D5:'SUM(D2:D4)', ROW 6:'SUM(B2:C2)', COL E:'D2*0.1'}
]]>
</excel>
<excel name="exportFormulasRowRange" returnClass="void">
<![CDATA[
EXPORT WITH SHEET="公式行区间", HEADER=true,
MAPPING={"a":"项目","b":"一月","c":"二月"},
FORMULAS={ROW 5..10:'SUM(B2:C2)'}
]]>
</excel>
<excel name="exportGraph" returnClass="void">
<![CDATA[
EXPORT WITH SHEET="图表", HEADER=true,
MAPPING={"a":"产品","b":"销量"},
GRAPH={TYPE=COLUMN, TITLE="季度销售统计", CATEGORY_AXIS=产品, VALUE_AXIS=销量,
CATEGORIES=["产品A","产品B","产品C"],
SERIES=[{NAME=第一季度, DATA=[120,200,150]},{NAME=第二季度, DATA=[180,210,190]}]}
]]>
</excel>
<excel name="exportFooterFixed" returnClass="void">
<![CDATA[
EXPORT WITH SHEET="页脚", HEADER=true, MAPPING={"a":"姓名","b":"年龄"},
FOOTER="Generated by JQuickExcel"
]]>
</excel>
<excel name="exportFooterDynamic" returnClass="void">
<![CDATA[
EXPORT WITH SHEET="页脚", HEADER=true, MAPPING={"a":"姓名","b":"年龄"}
]]>
</excel>
<excel name="exportThemeOcean" returnClass="void">
<![CDATA[
EXPORT WITH SHEET="主题", HEADER=true, MAPPING={"a":"姓名","b":"年龄"}
]]>
</excel>
</excels>

接口方法名与规则名一一对应。返回类型决定了框架怎么消费规则结果:返回 List<JQuickRow> 表示导入,返回 void 表示导出。

package com.demo.jquick;
import java.util.List;
import com.github.paohaijiao.statement.JQuickRow;
import com.github.paohaijiao.xml.param.Param;
public interface JQuickExcelDemoService {
List<JQuickRow> importBySheetName(@Param("field") String field, @Param("value") String value);
List<JQuickRow> importBySheetIndex(@Param("field") String field, @Param("value") String value);
List<JQuickRow> importWithMappingTransform(@Param("field") String field, @Param("value") String value);
List<JQuickRow> importWithValidation(@Param("field") String field, @Param("value") String value);
List<JQuickRow> importWithValidationFail(@Param("field") String field, @Param("value") String value);
void exportBasic(@Param("field") String field, @Param("value") String value);
void exportFormat(@Param("field") String field, @Param("value") String value);
void exportStyle(@Param("field") String field, @Param("value") String value);
void exportMerge(@Param("field") String field, @Param("value") String value);
void exportMergeCols(@Param("field") String field, @Param("value") String value);
void exportFormulas(@Param("field") String field, @Param("value") String value);
void exportFormulasRowRange(@Param("field") String field, @Param("value") String value);
void exportGraph(@Param("field") String field, @Param("value") String value);
void exportFooterFixed(@Param("field") String field, @Param("value") String value);
void exportFooterDynamic(@Param("field") String field, @Param("value") String value);
void exportThemeOcean(@Param("field") String field, @Param("value") String value);
}

接口准备好之后,导入和导出的调用都是同一套三步走。后面各小节只再补充与本节不同的部分。

// 导入:InputStream -> 导入 ParseFactory -> 代理接口
JQuickParseHandler parser = new JQuickExcelImportXmlParseFactory(in);
JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");
JQuickExcelDemoService service = factory.createApi(JQuickExcelDemoService.class);
List<JQuickRow> rows = service.importBySheetName("field", "value");
// 导出:List<JQuickRow> + OutputStream -> 导出 ParseFactory -> 代理接口
JQuickParseHandler parser = new JQuickExcelExportXmlParseFactory(rows, os);
JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");
JQuickExcelDemoService service = factory.createApi(JQuickExcelDemoService.class);
service.exportBasic("field", "value");

SHEET 传字符串时按名称匹配。规则里没有任何映射,导入结果直接使用表头文本作为字段名。

package com.demo.jquick;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.List;
import com.github.paohaijiao.statement.JQuickRow;
import com.github.paohaijiao.xml.factory.JQuickFactory;
import com.github.paohaijiao.xml.factory.JQuickXmlFactory;
import com.github.paohaijiao.xml.handler.JQuickParseHandler;
import com.github.paohaijiao.xml.im.JQuickExcelImportXmlParseFactory;
public class ImportSheetNameDemo {
public static void main(String[] args) throws Exception {
String path = "D:/idea/jquick-docs/public/jquick-excel/import/importDemo.xlsx";
try (InputStream in = new FileInputStream(path)) {
JQuickParseHandler parser = new JQuickExcelImportXmlParseFactory(in);
JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");
JQuickExcelDemoService s = factory.createApi(JQuickExcelDemoService.class);
List<JQuickRow> rows = s.importBySheetName("field", "value");
System.out.println("按工作表名称导入,共 " + rows.size() + " 行");
for (JQuickRow row : rows) {
System.out.println(row);
}
}
}
}
按工作表名称导入,共 3 行
{学号=2024001, 姓名=张三, 性别=男, 年龄=20, 出生日期=2004-09-01, 班级=计算机1班, 邮箱[email protected], 手机号=13800000001, 分数=88.5}
...

SHEET 传数字时按索引匹配,SHEET=1 表示第一张工作表(1 起算)。其余代码与按名称导入完全一致,只换规则名。

List<JQuickRow> rows = s.importBySheetIndex("field", "value");
System.out.println("按工作表索引导入,共 " + rows.size() + " 行");
按工作表索引导入,共 3 行

导入方向的 MAPPING 是「表头 → 字段」,与导出的「字段 → 表头」方向相反。TRANSFORM 负责三种常见改写:

  • toUpper(${name}) —— 调用框架内置转换函数,把姓名转大写;
  • trans(${dict},${sex}) —— 用 JContext 里的字典把中文性别翻译成内部码值;
  • dateFormat(${birthday},'yyyy-MM-dd') —— 统一日期文本格式。

字典通过 JContext 传入,注意导入和导出的字典方向是相反的:导入是「中文 → 码值」,导出是「码值 → 中文」,两份字典不能混用。

package com.demo.jquick;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.github.paohaijiao.param.JContext;
import com.github.paohaijiao.statement.JQuickRow;
import com.github.paohaijiao.xml.factory.JQuickFactory;
import com.github.paohaijiao.xml.factory.JQuickXmlFactory;
import com.github.paohaijiao.xml.handler.JQuickParseHandler;
import com.github.paohaijiao.xml.im.JQuickExcelImportXmlParseFactory;
public class ImportMappingTransformDemo {
public static void main(String[] args) throws Exception {
// 导入字典方向:Excel 中的文字 -> 系统内部码值
Map<String, Object> dict = new HashMap<>();
dict.put("男", "1");
dict.put("女", "2");
JContext ctx = new JContext();
ctx.put("dict", dict);
String path = "D:/idea/jquick-docs/public/jquick-excel/import/importDemo.xlsx";
try (InputStream in = new FileInputStream(path)) {
// 需要上下文时,ParseFactory 的第一个构造参数是 JContext
JQuickParseHandler parser = new JQuickExcelImportXmlParseFactory(ctx, in);
JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");
JQuickExcelDemoService s = factory.createApi(JQuickExcelDemoService.class);
List<JQuickRow> rows = s.importWithMappingTransform("field", "value");
System.out.println("Mapping+Transform 导入,共 " + rows.size() + " 行");
for (JQuickRow row : rows) {
System.out.println("no=" + row.get("no")
+ ", name=" + row.get("name")
+ ", sex=" + row.get("sex")
+ ", birthday=" + row.get("birthday")
+ ", className=" + row.get("className"));
}
}
}
}
Mapping+Transform 导入,共 3 行
no=2024001, name=张三, sex=1, birthday=2004-09-01, className=计算机1班
no=2024002, name=李四, sex=2, birthday=2003-09-01, className=计算机2班
no=2024003, name=王五, sex=1, birthday=2002-09-02, className=计算机3班

VALIDATION 的写法是「目标范围 : 规则块列表」。规则块本身又是一个键值集合:required 控制是否必填,msg 是校验不通过时抛出的异常信息,其余参数放进嵌套的 map:{...}。

写法 含义 示例
ROW n 单行 ROW 2
ROW n..m 行区间 ROW 1..10
COL X 单列 COL A
COL X..Y 列区间 COL A..D
C1 单个单元格 C1
A1:B5 矩形区域 A2:C2
VALIDATION={
C2:{dict{required:true,msg:'性别必须是男或女',map:{'男':'男','女':'女'}}},
D2:D4:{integer{required:true,msg:'年龄必须是整数'}},
H2:{start_with{required:true,msg:'手机号必须以138开头',map:{startWith:'138'}}}
}

同一范围可以连续挂多条规则:D2:D4:{integer{...}}, D2:D4:{min_value{...}},框架会按出现顺序逐条执行。

规则 作用 参数(放在 map:{...} 中)
required 非空校验 无
integer 整数 无
decimal 数值 无
max_value 数值上限 maxValue
min_value 数值下限 minValue
max_length 字符串最大长度 maxLength
min_length 字符串最小长度 minLength
regex 正则匹配 pattern
email 邮箱格式 无
mobile 手机号格式 无
date_format 日期格式 format
max_date 日期上限 format、maxDate
min_date 日期下限 format、minDate
dict 字典成员 字典键值对
boolean 布尔值 字典键值对(如 {'T':'true','F':'false'})
start_with 前缀匹配 startWith
not_start_with 前缀排除 notStartWith
end_with 后缀匹配 endWith
not_end_with 后缀排除 notEndWith
contain / not_contain 子串包含 / 排除 contains / notContain

校验失败会直接抛异常,异常信息就是规则块里的 msg。importWithValidationFail 故意把学号长度上限定为 3 位,用来演示失败路径:

System.out.println("=== 校验失败演示 ===");
try {
List<JQuickRow> rows = s.importWithValidationFail("field", "value");
System.out.println("意外通过,读取 " + rows.size() + " 行");
} catch (Exception e) {
System.out.println("校验失败:" + e.getMessage());
}
=== 校验通过 ===
校验通过,读取 3 行
=== 校验失败演示 ===
校验失败:学号超过3位(演示失败)

下表的结论来自逐条跑通用例,integer、日期与列区间三处与 README 参数表给人的直觉不同,属实测确认:

用法 实测结果 原因与建议
integer 作用于文本单元格(如 "20") 通过 Integer.parseInt("20") 正常
integer 作用于数值单元格(如 20) 不通过,报 msg 数值单元格读出来是 Double,toString() 得到 "20.0",Integer.parseInt 抛 NumberFormatException。整数列要么保证以文本写入,要么改用 decimal + min_value / max_value 组合
date_format / min_date / max_date 作用于文本日期 通过 按 map.format 解析字符串
同三条日期规则作用于真实日期单元格 不通过 日期单元格读出来是 Date,被格式化成 Wed Sep 01 00:00:00 CST 2004 之类的字符串再解析,必然失败。日期列请以文本写入,或去掉日期类规则
COL C 单列范围 通过 —
COL A..C 多列区间 不通过 多列分支会把表头行一并纳入遍历,取到空值后误判为超长。建议改用单列 COL C,或矩形范围 A2:C4
ROW 2、ROW 1..2、C1、A2:C2 通过 —
boolean 不带 map 不通过 实现里要读 map.containsValue(...),不带 map 会抛空指针。必须写成 boolean{required:true,msg:'...',map:{'T':'true','F':'false'}}

一次性把整表读进 List<JQuickRow> 在大文件场景下会撑爆内存。JExcelImportHandler.importDataInBatch 提供分页消费:每读满 pageSize 行回调一次,回调返回 false 可以提前终止,返回值是总读取行数。SHEET、HEADER、MAPPING、TRANSFORM、VALIDATION 在这条路径上同样生效。

package com.demo.jquick;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.List;
import com.github.paohaijiao.handler.JExcelImportHandler;
import com.github.paohaijiao.model.JExcelImportModel;
import com.github.paohaijiao.statement.JQuickRow;
public class ImportBatchDemo {
public static void main(String[] args) throws Exception {
String path = "D:/idea/jquick-docs/public/jquick-excel/import/importDemo.xlsx";
JExcelImportModel model = new JExcelImportModel();
try (InputStream in = new FileInputStream(path)) {
JExcelImportHandler handler = new JExcelImportHandler(in);
int total = handler.importDataInBatch(model, 5000, (List<JQuickRow> batch) -> {
System.out.println("本批 " + batch.size() + " 行");
return true; // 返回 false 可提前结束读取
});
System.out.println("总计读取:" + total);
}
}
}
本批 3 行
总计读取:3

导出方向的 MAPPING 是「字段 → 表头」。所有导出示例共用下面这段骨架,后面的小节只贴规则与差异部分。

TRANSFORM 在写出前对字段值做最后一次替换:b 列是性别码值,经 JContext 字典转成文字;a 列调用 toUpper。

List<Map<String, Object>> data = new ArrayList<>();
Map<String, Object> r1 = new LinkedHashMap<>();
r1.put("a", "张三");
r1.put("b", "1");
r1.put("c", 20);
data.add(r1);
// ... 李四 / 王五
List<JQuickRow> rows = JQuickRow.toRows(JObjectConverter.convert(data));
// 导出字典方向:码值 -> Excel 中的文字
Map<String, Object> dict = new HashMap<>();
dict.put("1", "男");
dict.put("2", "女");
JContext ctx = new JContext();
ctx.put("dict", dict);
String out = "D:/idea/jquick-docs/public/jquick-excel/export/export-basic.xlsx";
try (OutputStream os = new FileOutputStream(out)) {
JQuickParseHandler parser = new JQuickExcelExportXmlParseFactory(ctx, rows, os);
JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");
JQuickExcelDemoService s = factory.createApi(JQuickExcelDemoService.class);
s.exportBasic("field", "value");
}
// 已导出: D:/idea/jquick-docs/public/jquick-excel/export/export-basic.xlsx

回读导出文件,得到三列三行(表头「姓名 / 性别 / 年龄」,性别已变成「男 / 女」):

R1: 姓名 | 性别 | 年龄
R2: 张三 | 男 | 20
R3: 李四 | 女 | 21
R4: 王五 | 男 | 22

FORMAT 的键是字段名,值必须是真实的 Excel 数字格式码。下面是实测生效的几种:

字段 格式码 回读显示
a 000000 240001
c #,##0.00 12,345.60
d ¥#,##0.00 ¥12,345.60
e 0.00% 85.60%
f yyyy-MM-dd 2026-10-07
EXPORT WITH SHEET="格式", HEADER=true,
MAPPING={"a":"工号","b":"姓名","c":"金额","d":"人民币","e":"比率","f":"入职日期"},
FORMAT={"a":"000000","c":"#,##0.00","d":"¥#,##0.00","e":"0.00%","f":"yyyy-MM-dd"}

STYLE 的目标范围与 VALIDATION 一样支持 ROW n、COL X、单元格与矩形区域。本例给表头行与 A2:C100 数据区分别设样式:

EXPORT WITH SHEET="样式", HEADER=true,
MAPPING={"a":"姓名","b":"年龄","c":"班级"},
STYLE={
ROW 1:{fontName:Arial,fontHeightInPoints:12,bold:true,italic:true,color:yellow,
fillPattern:solid_foreground,fillForegroundColor:darkBlue,alignment:center},
A2:C100:{alignment:center,wrapText:true,borderBottom:thin,fillForegroundColor:lightYellow}
}

常用的边框与填充取值:

类别 可选值
边框(borderLeft / borderRight / borderTop / borderBottom) none、thin、medium、dashed、dotted、thick、double
填充(fillPattern) no_fill、solid_foreground、fine_dots、sparse_dots、thin_horz_bands、thin_vert_bands
其他 fontName、fontHeightInPoints、bold、italic、color、alignment、verticalAlignment、wrapText、fillForegroundColor、fillBackgroundColor、dataFormatString

MERGE 采用 {目标 WITH 策略} 的写法,目标分 ROWS 与 COLS 两种。

ROWS n 里的 n 是 POI 的 0 基行号,表头是第 0 行,所以 ROWS 1 合并的是第一条数据行。列合并 COLS A..F 表示 A 到 F 六列各自纵向合并。

EXPORT WITH SHEET="合并-行", HEADER=true,
MAPPING={"a":"部门","b":"姓名","c":"一季度","d":"二季度","e":"三季度","f":"四季度"},
MERGE:{ROWS 1 WITH MERGE_WITH_FIRST, ROWS 2 WITH MERGE_WITH_CONCAT}

回读结果:A2:F2 与 A3:F3 各成一个合并区,第二条数据行使用 MERGE_WITH_CONCAT,把六个单元格的值拼成 110.0,125.0,135.0,145.0 写进左上角单元格。

[Sheet] 合并-行 合并区域=A2:F2 A3:F3
R1: 部门 | 姓名 | 一季度 | 二季度 | 三季度 | 四季度
R2: 研发部 | 张三 | 100 | 120 | 130 | 140
R3: 110.0,125.0,135.0,145.0 | 李四 | 110 | 125 | 135 | 145

九种聚合策略:

策略 含义
MERGE_WITH_MAX 取最大值
MERGE_WITH_MIN 取最小值
MERGE_WITH_VALUE 取固定值
MERGE_WITH_AVG 取平均值
MERGE_WITH_COUNT 计数
MERGE_WITH_SUM 求和
MERGE_WITH_CONCAT 拼接
MERGE_WITH_FIRST 取首值
MERGE_WITH_LAST 取末值

FORMULAS 的写法是 {目标:'表达式'}。目标同样支持行、列、单元格与区域,实测可用四种:

EXPORT WITH SHEET="公式", HEADER=true,
MAPPING={"a":"项目","b":"一月","c":"二月","d":"三月"},
FORMULAS={D5:'SUM(D2:D4)', ROW 6:'SUM(B2:C2)', COL E:'D2*0.1'}
EXPORT WITH SHEET="公式行区间", HEADER=true,
MAPPING={"a":"项目","b":"一月","c":"二月"},
FORMULAS={ROW 5..10:'SUM(B2:C2)'}
目标 含义 实测写入结果
D5 单个单元格 D5 = SUM(D2:D4)
ROW 6 整行 第 6 行每列都写入 SUM(B2:C2)
ROW 5..10 行区间 第 5~10 行逐行写入
COL E 整列 E 列逐行写入 D2*0.1

框架按「数学 / 日期时间 / 文本 / 逻辑查询」四类提供函数:数学类 SUM、MAX、MIN、AVERAGE、COUNT、ABS、POWER、SQRT 等,日期时间类 TODAY、NOW、YEAR 等,文本类 CONCAT、UPPER、LOWER、LEN、LEFT、RIGHT、TEXT 等,逻辑查询类 IF、AND、OR、LOOKUP。但声明支持不等于写入结果正确,见下节实测。

逐条把公式跑一遍真实导出并回读单元格,结果分三类:

公式 实测结果
SUM(B2:D4)、SUM(B2:C2) 原样写入
MAX(B2:D4)、MIN(B2:D4)、AVERAGE(B2:B4)、COUNT(B2:D4)、COUNTA(A2:A4) 原样写入
B2*0.1(裸算术表达式) 原样写入
ROUND(3.14159,2)、POWER(2,3)、SQRT(4)、ABS(B2) 原样写入
TODAY()、NOW() 原样写入
ROUND(AVERAGE(B2:B4),2) 导出失败:IllegalArgumentException: argument type mismatch
RANK(B2,B2:B4) 导出失败:argument type mismatch
YEAR(NOW()) 导出失败:argument type mismatch
IF(B2>0,"OK","NO") 写入 IF(B2,"OK","NO"),比较运算符被丢弃
AND(B2>0,C2>0)、OR(B2>0,C2>0) 写入 AND(B2,C2)、OR(B2,C2),比较运算符被丢弃
UPPER(A2)、LOWER(A2)、LEN(A2) 写入 UPPER("A2")、LOWER("A2")、LEN("A2"),单元格引用被当成文本
LEFT(A2,1)、TEXT(B2,"0.0") 同样被加引号
CONCAT(A2,"-",A3) 生成 CONCAT(A2,-,A3),POI 抛 FormulaParseException 导致导出失败

GRAPH 用一套 KEY=VALUE 描述图表。TYPE 必填,TITLE、CATEGORY_AXIS、VALUE_AXIS 可选,CATEGORIES 是分类标签数组,SERIES 是数据系列数组。

EXPORT WITH SHEET="图表", HEADER=true,
MAPPING={"a":"产品","b":"销量"},
GRAPH={TYPE=COLUMN, TITLE="季度销售统计", CATEGORY_AXIS=产品, VALUE_AXIS=销量,
CATEGORIES=["产品A","产品B","产品C"],
SERIES=[{NAME=第一季度, DATA=[120,200,150]},{NAME=第二季度, DATA=[180,210,190]}]}

引号规则:含空格或中文的字符串用双引号包起来(TITLE="季度销售统计"),枚举值、单字标识不加引号即可。

十种图表类型:

类型 说明
LINE 折线图
COLUMN 柱状图
BAR 条形图
BAR3D 三维条形图
PIE 饼图
AREA 面积图
AREA3D 三维面积图
SCATTER 散点图
RADAR 雷达图
SURFACE 曲面图

导出后工作簿里会多出一张名为图表标题的数据工作表(本例是「季度销售统计」),以及 xl/drawings/drawing1.xml:

[Sheet] 图表 lastRow=3
R1: 产品 | 销量
R2: 产品A | 120
...
[Sheet] 季度销售统计 图表数=Name: /xl/drawings/drawing1.xml
R1: Categories | 第一季度 | 第二季度
R2: 产品A | 120 | 180
...

固定文本页脚直接写进 XML,框架会在数据末尾追加一行并横向合并:

EXPORT WITH SHEET="页脚", HEADER=true, MAPPING={"a":"姓名","b":"年龄"},
FOOTER="Generated by JQuickExcel"
[Sheet] 页脚 合并区域=A3:B3
R1: 姓名 | 年龄
R2: 张三 | 20
R3: Generated by JQuickExcel

主题不是 DSL 关键字,而是在规则解析之后、创建导出处理器之前设置。两种方式都实测可用:

String rule = "EXPORT WITH SHEET=\"编程主题\", HEADER=true, MAPPING={\"a\":\"姓名\",\"b\":\"年龄\"}";
JQuickExcelCommonExportExecutor executor = new JQuickExcelCommonExportExecutor();
JExcelExportModel model = (JExcelExportModel) executor.execute(rule);
model.setTheme("jade");
JExcelExportHandler handler = new JExcelExportHandler(model, rows);
Workbook workbook = handler.getWorkBook();
try (OutputStream os = new FileOutputStream(".../export-theme-jade.xlsx")) {
workbook.write(os);
}

主题编码速查(完整 42 套见 导出模板(内置主题预览)):

编码 名称 预览
default 经典皇家蓝(默认) default 经典皇家蓝
oceanBlue 海洋蓝 oceanBlue 海洋蓝
jade 翡翠绿 jade 翡翠绿
forestGreen 森林绿 forestGreen 森林绿
sunsetOrange 落日橙 sunsetOrange 落日橙
lavenderPurple 薰衣草紫 lavenderPurple 薰衣草紫
midnightDark 午夜深色 midnightDark 午夜深色
minimalistGrey 极简灰 minimalistGrey 极简灰
royalGold 皇家金 royalGold 皇家金
tropicalTeal 热带青 tropicalTeal 热带青

浏览器下载的本质是「把导出的 OutputStream 换成 HTTP 响应流」。下面是一个 Spring Boot 控制器,配合 spring-boot-starter-web 使用。要点是两个响应头:Content-Type 用 xlsx 的 MIME,Content-Disposition 里的中文文件名要先 URLEncoder 编码,再把 + 换回 %20。

package com.demo.jquick;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.github.paohaijiao.statement.JQuickRow;
import com.github.paohaijiao.util.JObjectConverter;
import com.github.paohaijiao.xml.factory.JQuickFactory;
import com.github.paohaijiao.xml.factory.JQuickXmlFactory;
import com.github.paohaijiao.xml.handler.JQuickParseHandler;
import com.github.paohaijiao.xml.ex.JQuickExcelExportXmlParseFactory;
@RestController
public class WebDownloadController {
@GetMapping("/demo/download")
public void download(HttpServletResponse response) throws Exception {
List<Map<String, Object>> data = new ArrayList<>();
Map<String, Object> r1 = new LinkedHashMap<>();
r1.put("a", "张三");
r1.put("b", 20);
data.add(r1);
List<JQuickRow> rows = JQuickRow.toRows(JObjectConverter.convert(data));
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("UTF-8");
String fileName = URLEncoder.encode("学生表.xlsx", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName);
try (OutputStream out = response.getOutputStream()) {
JQuickParseHandler parser = new JQuickExcelExportXmlParseFactory(rows, out);
JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");
JQuickExcelDemoService s = factory.createApi(JQuickExcelDemoService.class);
s.exportThemeOcean("field", "value");
}
}
}

JQuickExcelConfig 是单例,所有开关通过链式调用设置。实测输出如下:

package com.demo.jquick;
import com.github.paohaijiao.config.JQuickExcelConfig;
public class ConfigDemo {
public static void main(String[] args) {
JQuickExcelConfig.getInstance()
.setStreamingExportEnabled(true)
.setStreamingRowAccessWindowSize(100)
.setStreamingExportThreshold(5000)
.setStreamingCompressTempFiles(true)
.setCellStyleCacheEnabled(true);
System.out.println("配置结果:");
System.out.println(JQuickExcelConfig.getInstance().toString());
}
}
配置结果:
JQuickExcelConfig{streamingExportEnabled=true, streamingRowAccessWindowSize=100, streamingExportThreshold=5000, streamingCompressTempFiles=true, bigFileImportEnabled=true, importBatchThreshold=20000, cellStyleCacheEnabled=true}
配置项 作用
setStreamingExportEnabled 是否启用 SXSSF 流式导出
setStreamingRowAccessWindowSize 流式导出时内存中保留的行窗口大小
setStreamingExportThreshold 触发流式导出的行数阈值(默认 5000)
setStreamingCompressTempFiles 是否压缩流式导出的临时文件
setBigFileImportEnabled 大文件导入开关(用 OPCPackage 打开,降低内存占用)
setCellStyleCacheEnabled 单元格样式缓存,规避 64000 个 CellStyle 上限

导入接受任意 InputStream,导出接受任意 OutputStream,因此「本地文件 / 内存字节数组 / HTTP 响应」只是换一个流的实现。下面四种写法在同一段代码里跑通:

public class IoSourceDemo {
/** 导入逻辑只看 InputStream,与来源无关。 */
public List<JQuickRow> doImport(InputStream in) throws Exception {
JQuickParseHandler parser = new JQuickExcelImportXmlParseFactory(in);
JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");
JQuickExcelDemoService s = factory.createApi(JQuickExcelDemoService.class);
return s.importBySheetName("field", "value");
}
public static void main(String[] args) throws Exception {
String importPath = "D:/idea/jquick-docs/public/jquick-excel/import/importDemo.xlsx";
String exportDir = "D:/idea/jquick-docs/public/jquick-excel/export/";
// ① 从本地文件导入
try (InputStream in = new FileInputStream(importPath)) {
List<JQuickRow> rows = new IoSourceDemo().doImport(in);
}
// ② 从内存字节流导入(数据来自网络、OSS 或数据库 BLOB 时同理)
byte[] bytes;
try (InputStream in = new FileInputStream(importPath)) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[8192];
int len;
while ((len = in.read(buf)) != -1) {
bos.write(buf, 0, len);
}
bytes = bos.toByteArray();
}
try (InputStream in = new ByteArrayInputStream(bytes)) {
List<JQuickRow> rows = new IoSourceDemo().doImport(in);
}
// ③ 导出到本地文件
List<JQuickRow> exportRows = /* 业务数据 */ null;
try (OutputStream os = new FileOutputStream(exportDir + "io-file.xlsx")) {
JQuickParseHandler parser = new JQuickExcelExportXmlParseFactory(exportRows, os);
JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");
JQuickExcelDemoService s = factory.createApi(JQuickExcelDemoService.class);
s.exportThemeOcean("field", "value");
}
// ④ 导出到内存字节数组,可直接作为邮件附件或上传 OSS
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
JQuickParseHandler parser = new JQuickExcelExportXmlParseFactory(exportRows, bos);
JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");
JQuickExcelDemoService s = factory.createApi(JQuickExcelDemoService.class);
s.exportThemeOcean("field", "value");
byte[] outBytes = bos.toByteArray();
}
}
}
=== ① 文件路径导入 ===
从文件导入 3 行
=== ② 任意流导入(ByteArrayInputStream) ===
从字节流导入 3 行
=== ③ 写入本地文件 ===
已导出: D:/idea/jquick-docs/public/jquick-excel/export/io-file.xlsx
=== ④ 写入字节数组(可作邮件附件/OSS 上传) ===
字节数组长度: 3592 bytes,可直接作为邮件附件或上传 OSS

以下截图统一放在 D:/idea/jquick-docs/public/jquick-excel/image,文档按文件名引用。放入同名 PNG 后即可正常显示,无需改动本文。

截图内容 文件名 预览
导入测试文件 importDemo.xlsx 在 Excel 中的打开效果 demo-import-file.png 导入测试文件
导入 Demo 的控制台输出(映射 / 转换 / 校验三例) demo-import-console.png 导入控制台
校验失败的异常信息输出 demo-validation-fail.png 校验失败
基础导出结果 export-basic.xlsx(含字典转换后的性别列) demo-export-basic.png 基础导出
FORMAT 导出结果(工号补零、金额千分位、人民币、百分比、日期) demo-export-format.png 格式导出
STYLE 导出结果(表头配色 + 数据区边框换行) demo-export-style.png 样式导出
行合并结果 export-merge-rows.xlsx(含 MERGE_WITH_CONCAT 拼接值) demo-export-merge-rows.png 行合并
列合并结果 export-merge-cols.xlsx demo-export-merge-cols.png 列合并
FORMULAS 导出结果(单元格 / 整行 / 整列三种目标) demo-export-formulas.png 公式导出
GRAPH 导出结果(柱状图 + 数据工作表) demo-export-graph.png 图表导出
FOOTER 固定页脚与动态页脚对比 demo-export-footer.png 页脚导出
主题导出对比(oceanBlue 与 jade) demo-export-theme.png 主题导出
浏览器下载效果(Spring Boot 控制器) demo-web-download.png Web 下载
JQuickExcelConfig 配置输出 demo-config-console.png 配置输出