JQuick-Excel 完整 Demo
This content is not available in your language yet.
JQuick-Excel 完整 Demo
Section titled “JQuick-Excel 完整 Demo”JQuick-Excel 的用法可以概括为「一份 XML 规则、一个 Java 接口、一次代理调用」。这一章不复述参数清单,而是把一份能跑通的工程从零搭出来:先准备规则文件与服务契约,再分别演示导入、导出、Web 下载与性能配置,最后给出截图清单。
本章的示例数据是一条「学生信息」流水线:同一份 jquick-excel.xml 既能把业务数据导出成带格式、合并、公式、图表与页脚的 Excel,也能把 Excel 读回成带字段映射、字典翻译与数据校验的 JQuickRow 列表。
调用方需要记住的三件事
Section titled “调用方需要记住的三件事”- 规则放在类路径的
jquick-excel.xml里。根节点<excels>的namespace必须是服务接口的全限定名,每个<excel name="...">的name必须是接口里的方法名,两者对不上会在createApi时报错。 - 接口方法名 = 规则名,返回类型要匹配。导入方法返回
List<JQuickRow>,导出方法返回void;参数统一用@Param标注(示例里这两个参数不参与规则计算,仅用于保持与框架签名一致)。 - 导入和导出使用不同的 ParseFactory。导入传
InputStream,导出传List<JQuickRow>+OutputStream;需要字典等上下文时,再额外传入一个JContext。
示例工程结构
Section titled “示例工程结构”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 浏览器下载Maven 依赖
Section titled “Maven 依赖”<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 |
本章截图统一存放于此 |
生成导入测试文件
Section titled “生成导入测试文件”导入 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 = { }; 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。
规则文件 jquick-excel.xml
Section titled “规则文件 jquick-excel.xml”全部规则集中在一个文件里。根节点 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);}三步调用骨架
Section titled “三步调用骨架”接口准备好之后,导入和导出的调用都是同一套三步走。后面各小节只再补充与本节不同的部分。
// 导入: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");导入 Demo
Section titled “导入 Demo”按工作表名称导入
Section titled “按工作表名称导入”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}...按工作表索引导入
Section titled “按工作表索引导入”SHEET 传数字时按索引匹配,SHEET=1 表示第一张工作表(1 起算)。其余代码与按名称导入完全一致,只换规则名。
List<JQuickRow> rows = s.importBySheetIndex("field", "value");System.out.println("按工作表索引导入,共 " + rows.size() + " 行");按工作表索引导入,共 3 行MAPPING 与 TRANSFORM
Section titled “MAPPING 与 TRANSFORM”导入方向的 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 数据校验
Section titled “VALIDATION 数据校验”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{...}},框架会按出现顺序逐条执行。
20 条内置规则
Section titled “20 条内置规则”| 规则 | 作用 | 参数(放在 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 |
校验不通过时的表现
Section titled “校验不通过时的表现”校验失败会直接抛异常,异常信息就是规则块里的 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位(演示失败)实测行为说明
Section titled “实测行为说明”下表的结论来自逐条跑通用例,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'}} |
批量大文件导入
Section titled “批量大文件导入”一次性把整表读进 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导出 Demo
Section titled “导出 Demo”导出方向的 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: 张三 | 男 | 20R3: 李四 | 女 | 21R4: 王五 | 男 | 22FORMAT 显示格式
Section titled “FORMAT 显示格式”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 单元格样式
Section titled “STYLE 单元格样式”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 单元格合并
Section titled “MERGE 单元格合并”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:F3R1: 部门 | 姓名 | 一季度 | 二季度 | 三季度 | 四季度R2: 研发部 | 张三 | 100 | 120 | 130 | 140R3: 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 公式
Section titled “FORMULAS 公式”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。但声明支持不等于写入结果正确,见下节实测。
实测行为说明
Section titled “实测行为说明”逐条把公式跑一遍真实导出并回读单元格,结果分三类:
| 公式 | 实测结果 |
|---|---|
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 图表
Section titled “GRAPH 图表”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=3R1: 产品 | 销量R2: 产品A | 120...[Sheet] 季度销售统计 图表数=Name: /xl/drawings/drawing1.xmlR1: Categories | 第一季度 | 第二季度R2: 产品A | 120 | 180...FOOTER 页脚
Section titled “FOOTER 页脚”固定文本页脚直接写进 XML,框架会在数据末尾追加一行并横向合并:
EXPORT WITH SHEET="页脚", HEADER=true, MAPPING={"a":"姓名","b":"年龄"}, FOOTER="Generated by JQuickExcel"[Sheet] 页脚 合并区域=A3:B3R1: 姓名 | 年龄R2: 张三 | 20R3: 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);}// 第一个参数是主题编码,后续仍是标准的三步调用JQuickParseHandler parser = new JQuickExcelExportXmlParseFactory("oceanBlue", rows, os);JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");JQuickExcelDemoService s = factory.createApi(JQuickExcelDemoService.class);s.exportThemeOcean("field", "value");主题编码速查(完整 42 套见 导出模板(内置主题预览)):
| 编码 | 名称 | 预览 |
|---|---|---|
default |
经典皇家蓝(默认) | ![]() |
oceanBlue |
海洋蓝 | ![]() |
jade |
翡翠绿 | ![]() |
forestGreen |
森林绿 | ![]() |
sunsetOrange |
落日橙 | ![]() |
lavenderPurple |
薰衣草紫 | ![]() |
midnightDark |
午夜深色 | ![]() |
minimalistGrey |
极简灰 | ![]() |
royalGold |
皇家金 | ![]() |
tropicalTeal |
热带青 | ![]() |
Web 下载 Demo
Section titled “Web 下载 Demo”浏览器下载的本质是「把导出的 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;
@RestControllerpublic 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"); } }}性能与配置 Demo
Section titled “性能与配置 Demo”JQuickExcelConfig 全局配置
Section titled “JQuickExcelConfig 全局配置”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 |
![]() |
JQuickExcelConfig 配置输出 |
demo-config-console.png |
![]() |























