面向 Java 开发者的 JQuick-Excel 字段映射与单元格格式配置手册。阅读本文前建议先了解 README.md 中的基础 DSL 语法。
MAPPING | |||
FORMAT | |||
TRANSFORM |
三者层级关系(导出时执行顺序):
原始数据对象 → [MAPPING 决定哪些字段参与] → [TRANSFORM 改值] → [FORMAT 改显示] → 写入 Excel关键区别:
FORMAT是 Excel 层面的"数字格式掩码"(相当于在 Excel 里右键设置单元格格式),不会改动底层存储的数值;TRANSFORM是 Java 层面的值转换(如toUpperCase、dateFormat),会真正改变写入单元格的值。
语法:
MAPPING = { ”源字段名” : ”Excel表头名” [, ...] }语义:
Map 中的字段名(源字段)示例:
EXPORT WITHSHEET=”学生表”,HEADER=true,MAPPING={”id”:”主键”,”name”:”姓名”,”gender”:”性别”,”age”:”年龄”,”enrollmentDate”:”入学时间”,”className”:”班级”,”ignoreField”:”是否忽略”}
对应 Java 数据:
public class JStudentModel {private String id;private String name;private Integer gender;private Integer age;private Date enrollmentDate;private String className;private String ignoreField;// getters/setters...}
生成的 Excel:
public Void visitMappingOption(JQuickExcelParser.MappingOptionContext ctx) {Map mapping = new HashMap<>();for (JQuickExcelParser.FieldMappingContext fieldMappingContext : ctx.fieldMapping()) {JKeyValueModel fieldMappings = visitFieldMapping(fieldMappingContext);mapping.put(fieldMappings.getKey(), fieldMappings.getValue().toString());}config.setMapping(mapping);return null;}
语法:
MAPPING = { ”Excel列名” : ”目标字段名” [, ...] }语义(与导出相反):
Map 的字段名(目标)示例:
IMPORT WITHSHEET=”Sheet1”,HEADER=true,MAPPING={”学号”: ”no”,”姓名”: ”name”,”性别”: ”sex”,”年龄”: ”age”,”出生日期”: ”birthday”}
读取 Excel:
得到 Java 数据:
List rows = handler.importData(model);// 每行 Map 的 key 是:no / name / sex / age / birthday
源码追踪(JQuickExcelCommonImportVisitor.java:70-86):
public Object visitMappingOption(JQuickExcelParser.MappingOptionContext ctx) {Map mappings = new HashMap<>();for (JQuickExcelParser.FieldMappingContext mapping : ctx.fieldMapping()) {if (null != mapping.STRING()) {String source = JStringUtils.trim(mapping.STRING(0).getText()); // Excel列名String target = mapping.STRING(1) != null? JStringUtils.trim(mapping.STRING(1).getText()) // 目标字段名: mapping.functionCall().getText(); // 或函数调用mappings.put(source, target);}}config.setMappings(mappings);return null;}
⚠️这是最容易踩坑的地方——MAPPING 在导出和导入方向上 key/value 语义是相反的:
EXPORT | |||
IMPORT |
对称写法对照:
# 导出:Java 字段 enrollmentDate → Excel 表头”入学时间”EXPORT WITH MAPPING={ ”enrollmentDate”:”入学时间” }# 导入:Excel 列”入学时间” → Java 字段 enrollmentDateIMPORT WITH MAPPING={ ”入学时间”:”enrollmentDate” }
设计原因:MAPPING 始终遵循"源 → 目标"的语义。导出时源是 Java、目标是 Excel;导入时源是 Excel、目标是 Java。
MAPPING 还兼任"字段过滤"职责——未在 MAPPING 中声明的字段:
getOrDefault(header, header))。要彻底排除某字段,需在数据转换阶段移除,或使用 TRANSFORM 处理。Map 中。即 MAPPING 同时是"白名单"。💡 导入时若想跳过某列,直接不写进 MAPPING 即可;导出时若想跳过某字段,需在 Java 侧过滤掉。
导出:
String rule = ”””EXPORT WITHSHEET=”学生表”,HEADER=true,MAPPING={”id”:”主键”,”name”:”姓名”,”gender”:”性别”,”age”:”年龄”,”enrollmentDate”:”入学时间”,”className”:”班级”}”””;List> data = JObjectConverter.convert(getStudentList());FileOutputStream fos = new FileOutputStream(”students.xlsx”);JQuickExcelCommonExportExecutor executor = new JQuickExcelCommonExportExecutor();JExcelExportModel config = (JExcelExportModel) executor.execute(rule);JExcelExportHandler handler = new JExcelExportHandler(config, data);handler.getWorkBook().write(fos);fos.close();
导入:
String rule = ”””IMPORT WITHSHEET=”Sheet1”,HEADER=true,MAPPING={”学号”: ”no”,”姓名”: ”name”,”性别”: ”sex”,”年龄”: ”age”,”出生日期”: ”birthday”}”””;JQuickExcelCommonImportExecutor executor = new JQuickExcelCommonImportExecutor();JExcelImportModel model = (JExcelImportModel) executor.execute(rule);InputStream is = new FileInputStream(”students.xlsx”);XSSFWorkbook workbook = new XSSFWorkbook(is);JExcelImportHandler handler = new JExcelImportHandler(workbook);List rows = handler.importData(model);
FORMAT = { ”源字段名” : ”Excel格式字符串” [, ...] }仅用于 EXPORT,IMPORT 不支持 FORMAT(导入时如需格式化,请用 TRANSFORM 的 dateFormat 等函数)。
⚠️FORMAT 的 key 必须是源字段名(Java 字段名),不是 Excel 表头名!
这一点与 MAPPING 的 key 保持一致,但很多开发者会误以为要用 Excel 表头名。
正确:
MAPPING={ ”enrollmentDate”:”入学时间” },FORMAT={ ”enrollmentDate”:”yyyy-MM-dd” }# ✅ 用源字段名
错误:
MAPPING={ ”enrollmentDate”:”入学时间” },FORMAT={ ”入学时间”:”yyyy-MM-dd” }# ❌ 用表头名,不会生效
源码追踪(JExcelExportHandler.java:179-190):
for (Map.Entry entry : rowData.entrySet()) { // entry.getKey() = 源字段名Cell cell = row.createCell(colNum++);// ... 写入值 ...String fmt = formats == null ? null : formats.get(entry.getKey()); // 用源字段名查格式CellStyle cellStyle = JCellStyleCache.getThemedDataWithFormat(workbook, theme, isOdd, fmt);cell.setCellStyle(cellStyle);}
FORMAT 的 value 会原样传给Workbook.createDataFormat().getFormat(format),因此任何 Excel 原生支持的格式字符串都可用,DSL 解析器不做任何校验。
yyyy-MM-dd | 2025-01-23 | |
yyyy年m月d日 | 2025年1月23日 | |
hh:mm:ss | 14:30:00 | |
[h]:mm | 62:30 | |
0.00 | 3.14 | |
#,##0.00 | 1,234.56 | |
0.00% | 25.00% | |
0.00E+00 | 1.23E+03 | |
¥#,##0.00 | ¥1,234.56 | |
¥#,##0.00;[Red]-¥#,##0.00 | ||
0.0,"K" | 12341.2K | |
@ | ||
[>1000]"高";[>0]"中";"低" | ||
| 1.251 1/4 |
⚠️ 注意:格式串语法错误不会在导出时报错,POI 写入时不校验。问题要等 Excel 打开时才暴露(显示为默认格式或弹窗)。建议复杂格式先用 Excel 验证一遍。
FORMAT不会覆盖主题样式(边框、背景色、字体),而是在主题样式基础上叠加DataFormat。
public static CellStyle getThemedDataWithFormat(Workbook wb, JExcelTheme theme, boolean isOdd, String format) {CellStyle base = resolveBaseDataStyle(wb, theme, isOdd); // 先取主题基础样式if (format == null || format.isEmpty()) {return base; // 无格式直接返回基础样式}String key = (theme == null ? ”_DEF_” : ”_THEME_” + theme.getCode() + ”_”)+ (isOdd ? ”ODD” : ”EVEN”) + ”_FMT_” + format;return getStyleMap(wb).computeIfAbsent(key, k -> cloneStyleWithFormat(wb, base, format));}
缓存机制:同一个 workbook + theme + 奇偶 + format 组合只创建一个 CellStyle,避免触达 POI 的 64000 样式上限。即使百万行数据,只要格式串种类有限,样式对象数量就是常数级。
String rule = ”””EXPORT WITHSHEET=”学生表”,HEADER=true,MAPPING={”id”:”主键”,”name”:”姓名”,”age”:”年龄”,”enrollmentDate”:”入学时间”,”salary”:”薪资”},FORMAT={”enrollmentDate”:”yyyy-MM-dd”,”age”:”0”,”salary”:”¥#,##0.00;[Red]-¥#,##0.00”}”””;List> data = JObjectConverter.convert(getStudentList());FileOutputStream fos = new FileOutputStream(”formatted.xlsx”);JQuickExcelCommonExportExecutor executor = new JQuickExcelCommonExportExecutor();JExcelExportModel config = (JExcelExportModel) executor.execute(rule);// 可选:配合主题使用,FORMAT 会叠加在主题样式之上config.setTheme(”royalGold”);JExcelExportHandler handler = new JExcelExportHandler(config, data);handler.getWorkBook().write(fos);fos.close();
生成效果:
三者各司其职,组合使用时执行顺序为MAPPING(选字段)→ TRANSFORM(改值)→ FORMAT(改显示)。
EXPORT WITHSHEET=”学生表”,HEADER=true,MAPPING={”id”:”主键”,”name”:”姓名”,”gender”:”性别”,”enrollmentDate”:”入学时间”,”salary”:”薪资”},FORMAT={”enrollmentDate”:”yyyy-MM-dd”,# 显示层:日期格式”salary”:”¥#,##0.00”# 显示层:货币格式},TRANSFORM={”name”: toUpper(${name}),# 值层:姓名转大写”enrollmentDate”: dateFormat(${enrollmentDate},'yyyy-MM-dd'),# 值层:Date→String”gender”: trans(${dict},${gender})# 值层:1/0 → 男/女}
关键区别:
TRANSFORMdateFormat 把 Date 对象转成 String 写入单元格 → 单元格存的是字符串FORMATyyyy-MM-dd 让单元格按日期格式显示 → 单元格存的是 Date 数值,只是显示成日期推荐做法:
Java 侧上下文传递(TRANSFORM 用到 ${dict} 时需要注入):
HashMap dict = new HashMap<>();dict.put(”1”, ”男”);dict.put(”0”, ”女”);JContext context = new JContext();context.put(”dict”, dict);JExcelExportHandler handler = new JExcelExportHandler(config, context, JQuickRow.toRows(data));
排查清单:
记住口诀:"源 → 目标"。
"javaField":"excelHeader""excelHeader":"javaField"MAPPING 不能直接排除字段(未声明的字段会用字段名本身作为表头输出)。两种方案:
JObjectConverter.convert 前移除字段,或用 @JsonIgnore 等注解List<POJO> 转成 List<Map> 时只保留需要的 key不会。JCellStyleCache 对 workbook + theme + 奇偶 + format 组合做了缓存(JCellStyleCache.java:117-122),同种组合只创建一个 CellStyle。即便百万行,只要格式串种类有限(通常 < 20 种),样式对象就是常数级。
FORMATDataFormat(数字/日期格式掩码),保留主题的边框/背景/字体STYLE需要完整自定义样式时用 STYLE,只调数字/日期格式时用 FORMAT 更轻量。
IMPORT 不支持 FORMAT。用 TRANSFORM:
IMPORT WITHMAPPING={ ”出生日期”:”birthday” },TRANSFORM={”birthday”: dateFormat(${birthday},'yyyy-MM-dd')}
EXPORT WITH MAPPING={ "id":"主键" } | |||
IMPORT WITH MAPPING={ "学号":"no" } |
EXPORT WITH FORMAT={ "enrollmentDate":"yyyy-MM-dd" } | |||
yyyy-MM-dd | |
yyyy年m月d日 | |
hh:mm:ss | |
0.00 | |
#,##0.00 | |
0.00% | |
¥#,##0.00 | |
¥#,##0.00;[Red]-¥#,##0.00 | |
@ |
MAPPING | ||||
FORMAT | ||||
TRANSFORM |
相关文档
README.md - 项目总览与完整 DSL 语法 README_EN.md - English documentation benchmark.md - 性能基准测试报告 template.md - 主题模板预览