JQuick-Excel 字段映射实战:用 MAPPING 固化 Excel 表头与业务字段契约

JQuick-Excel 字段映射实战:用 MAPPING 固化 Excel 表头与业务字段契约

tags: #JQuickExcel #Java #Excel导入导出 #字段映射 #XMLDSL

简介

本文围绕 JQuick-Excel 已公开示例中的 MAPPING 展开,完整说明导出"字段到表头"和导入"表头到字段"两种方向,给出 XML、服务接口、JObjectConverter、JQuickRow、JQuickExcelExportXmlParseFactory、JQuickXmlFactory 的组合方式,并说明怎样验收映射是否真正生效。

前言

Excel 文件同时面对业务人员和程序。业务人员关心"学号、姓名、年龄"是否写在正确列,Java 代码则更适合使用 studentNo、name、age 这样的稳定字段名。两套命名并不冲突,真正容易出错的是它们之间的对应关系散落在循环、列号和 if 判断里。模板改一次标题,代码往往也要跟着改;导入文件多一个空格,数据又可能落不到预期字段。

JQuick-Excel 的 XML DSL 将这种对应关系放进 MAPPING。从 README-CN.md 可以确认,导出和导入都支持字段映射;测试资源 jquick-excel.xml 也给出了 EXPORT WITH、IMPORT WITH、HEADER=true 和 JSON 风格映射对象的实际写法。本文只讨论这些已出现的范围,不假定存在字段自动猜测、标题模糊匹配、别名回退或未声明列的自动处理。

理解 MAPPING 最重要的一点是方向。导出规则把当前行字段写到 Excel 标题列,形式是 "字段":"表头";导入规则从 Excel 的表头找到列,再将单元格值放到返回行字段,形式是 "表头":"字段"。左右两边刚好相反。很多"导出正常、导入为空"的问题,不是流、代理或工作簿格式问题,而是把导出映射原样复制到了导入规则。

本篇以学生数据为例。Java 准备 studentNo、name、age 三个字段,导出的第一行显示为"学号、姓名、年龄";同一类文件导入后,读取结果仍使用英文业务字段。这样模板语言可以保持面向用户,业务代码则不必依赖中文标题。

环境与依赖

README-CN.md 标明 JQuick-Excel 需要 Java 8 或更高版本,支持 xls 与 xlsx。本文使用 Maven 坐标 io.github.paohaijiao:jquick-excel,版本采用 README 中的 3.6.0。XML 配置应位于类路径,例如 src/main/resources/jquick-excel.xml,由 JQuickXmlFactory 通过资源名加载。

xml 复制代码
<dependency>
  <groupId>io.github.paohaijiao</groupId>
  <artifactId>jquick-excel</artifactId>
  <version>3.6.0</version>
</dependency>

运行前还要明确四个契约。第一,XML 的 namespace 是服务接口的全限定名。第二,<excel name> 与接口方法名对应。第三,导出数据中的键必须与导出 MAPPING 左侧字段一致。第四,导入工作表的第一行标题必须与导入 MAPPING 左侧文本一致,因为示例使用 HEADER=true。

这些条件不是额外的框架功能,而是公开示例能够工作的必要前提。尤其是标题文本,应把空格、大小写、全半角和业务名称变更都视为模板契约的一部分。不要把"文件能打开"误当作"字段一定能正确映射"。

代码示例

下面的 XML 同时定义一个导出方法和一个导入方法。导出时左侧是 studentNo、name、age;导入时左侧变为真实出现于首行的"学号、姓名、年龄"。SHEET、HEADER 与 MAPPING 都采用 README 和测试 XML 中已验证的写法。

xml 复制代码
<?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.example.StudentExcelService">
    <excel name="exportStudents" returnClass="void">
        <![CDATA[
            EXPORT WITH
            SHEET="学生表",
            HEADER=true,
            MAPPING={
                "studentNo":"学号",
                "name":"姓名",
                "age":"年龄"
            }
        ]]>
    </excel>
    <excel name="importStudents" returnClass="java.util.List">
        <![CDATA[
            IMPORT WITH
            SHEET="学生表",
            HEADER=true,
            MAPPING={
                "学号":"studentNo",
                "姓名":"name",
                "年龄":"age"
            }
        ]]>
    </excel>
</excels>

服务接口只声明 XML 中存在的方法。README 的示例为参数使用 @Param,并让导入方法返回 List<JQuickRow>。本文沿用这一调用边界,不在接口里增加没有被公开示例验证的参数类型或代理约定。

java 复制代码
import com.github.paohaijiao.statement.JQuickRow;
import com.github.paohaijiao.xml.param.Param;

import java.util.List;

public interface StudentExcelService {
    void exportStudents(@Param("field") String field, @Param("value") String value);

    List<JQuickRow> importStudents(@Param("field") String field, @Param("value") String value);
}

导出时,先准备 Map 列表,调用 JObjectConverter.convert(data),再通过 JQuickRow.toRows(...) 得到导出解析器使用的行列表。README 已给出了这一完整转换路径。这里使用 LinkedHashMap 只是让示例的数据构造顺序更容易阅读,真正决定导出列的仍然是 XML 中的 MAPPING。

java 复制代码
import com.github.paohaijiao.convert.JObjectConverter;
import com.github.paohaijiao.statement.JQuickRow;
import com.github.paohaijiao.xml.JQuickFactory;
import com.github.paohaijiao.xml.JQuickXmlFactory;
import com.github.paohaijiao.xml.parse.JQuickParseHandler;
import com.github.paohaijiao.xml.parse.excel.JQuickExcelExportXmlParseFactory;

import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

List<Map<String, Object>> students = new ArrayList<>();
Map<String, Object> student = new LinkedHashMap<>();
student.put("studentNo", "S1001");
student.put("name", "Alice");
student.put("age", 20);
students.add(student);

List<JQuickRow> rows = JQuickRow.toRows(JObjectConverter.convert(students));
try (OutputStream output = new FileOutputStream("students.xlsx")) {
    JQuickParseHandler parser = new JQuickExcelExportXmlParseFactory(rows, output);
    JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");
    StudentExcelService service = factory.createApi(StudentExcelService.class);
    service.exportStudents("field", "value");
}

导入时需要的是输入流和 JContext。即使本例不使用字典转换,也仍按 README 的已验证构造方式传入一个 JContext。XML 中 HEADER=true 表示第一行参与标题识别,因此前一段代码生成的"学生表"可以作为本段的输入文件。

java 复制代码
import com.github.paohaijiao.context.JContext;
import com.github.paohaijiao.statement.JQuickRow;
import com.github.paohaijiao.xml.JQuickFactory;
import com.github.paohaijiao.xml.JQuickXmlFactory;
import com.github.paohaijiao.xml.parse.JQuickParseHandler;
import com.github.paohaijiao.xml.parse.excel.JQuickExcelImportXmlParseFactory;

import java.io.FileInputStream;
import java.io.InputStream;
import java.util.List;

try (InputStream input = new FileInputStream("students.xlsx")) {
    JContext context = new JContext();
    JQuickParseHandler parser = new JQuickExcelImportXmlParseFactory(context, input);
    JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");
    StudentExcelService service = factory.createApi(StudentExcelService.class);
    List<JQuickRow> imported = service.importStudents("field", "value");
    System.out.println(imported.size());
}

如果业务字段已经是 id、name、gender、age,也可以直接采用测试 XML 的导出结构:"id":"主键"、"name":"姓名"、"gender":"性别"、"age":"年龄"。导入时则需要把同一份标题放在左侧,例如 "姓名":"name"。关键不在字段语言,而在两端的方向必须与当前操作一致。

原理说明

XML 服务代理的入口是 JQuickXmlFactory。它读取资源中的 <excels> 定义,并根据 namespace 与 createApi 的接口建立服务代理。调用 exportStudents 时,代理查找同名 <excel> 的 EXPORT WITH 规则;调用 importStudents 时,代理查找对应的 IMPORT WITH 规则。方法名是 XML 规则的路由键,因此改接口方法名时必须同步改 XML 的 name。

导出解析器 JQuickExcelExportXmlParseFactory 在构造时接收两样内容:已经准备好的 List<JQuickRow> 和输出流。每个 JQuickRow 提供当前记录的字段值。对于导出 MAPPING={"studentNo":"学号"},左侧 studentNo 用来从当前行取值,右侧"学号"用于表头。再结合 HEADER=true,生成的首行就是"学号"。后续行在这一列写入 studentNo 的值,例如 S1001。

导入解析器 JQuickExcelImportXmlParseFactory 接收 JContext 与输入流。规则先由 SHEET="学生表" 定位数据页,再由 HEADER=true 赋予第一行标题语义。对于导入 MAPPING={"学号":"studentNo"},左侧"学号"是输入文件中要找的列标题,右侧 studentNo 是返回 JQuickRow 采用的字段名。数据行中的 S1001 因此可通过 studentNo 被后续业务代码读取。

这解释了一个实用事实:映射的职责是列归属,不是数据加工。比如性别代码转中文、日期输出格式化等,应使用 README 已列出的 TRANSFORM 或 FORMAT 范围,而不是试图通过把标题改成表达式来实现。映射也不负责自动推断缺失字段。若 Java 行里没有 studentNo,或者导入首行没有"学号",本文所依据的公开配置没有承诺会替开发者猜测其它字段。

映射顺序在导出场景也很重要。业务人员看到的列顺序应由 XML 显式定义,而不是由 Map 的偶然遍历顺序决定。将 MAPPING 作为模板的一部分后,可以对 XML 做评审:第一列是不是学号,第二列是不是姓名,新增列是否同步提供了业务字段。代码和模板的变更边界会更清晰。

对于双向模板,建议把导出映射的右侧和导入映射的左侧成对维护。例如导出 "age":"年龄",导入就写 "年龄":"age"。这样导出的工作簿能够作为导入样例,人工验收也只需要关注标题是否完全一致。这里说的是配置一致性,并不代表框架会自动生成另一方向的规则;两份规则仍应明确写在 XML 中。

注意事项

第一,不能混淆映射方向。EXPORT WITH 的左侧是行字段、右侧是 Excel 标题;IMPORT WITH 的左侧是 Excel 标题、右侧是返回行字段。把 {"studentNo":"学号"} 用于导入会让规则寻找名为 studentNo 的标题,而不是寻找"学号"。

第二,导入标题要以实际首行文本为准。使用 HEADER=true 时,输入工作表第一行不是普通数据。标题前后空格、错别字、全半角不同、名称改版都应在测试文件中验证,并同步更新 XML。不要依赖未在 README 或测试配置中说明的模糊匹配、自动别名或按字段名兜底。

第三,工作表与标题是两个层次。SHEET="学生表" 选择的是页签,"学号"是该页第一行的单元格内容。工作表名写对但首行标题不一致,映射仍然不能按预期工作;标题写对但选错工作表,同样会读到错误区域。

第四,Java 数据键必须匹配导出映射左侧。上例 XML 使用 studentNo,Java 就应写 student.put("studentNo", "S1001"),而不是写 student.put("学号", "S1001")。表头属于 XML 右侧展示名,业务数据仍以字段名组织。数据值为数字、日期等类型时,也不要为了映射而强制转换成字符串。

第五,XML 必须在运行时类路径中。JQuickXmlFactory(parser, "jquick-excel.xml") 读取的是资源名;仅在本地目录存在文件并不能保证打包后可加载。还要检查 XML 的 namespace、接口全限定名、<excel name>、接口方法名和 returnClass 是否互相对应。

第六,输出流不能在代理调用前关闭,输入流也应在导入完成前保持可读。示例使用 try-with-resources,使资源在代理方法执行完成后自动关闭。对于 Web 上传和下载,应把受控的请求输入流、响应输出流接入相同构造链路,而不要为了字段映射去绕开 XML 代理。

第七,验收应覆盖内容而不仅是文件存在。导出后打开"学生表",确认第一行依次为"学号、姓名、年龄",第二行对应 S1001、Alice、20。再将文件导入,检查返回行数量,并确认业务代码能以 studentNo、name、age 取到相同值。这样可以同时验证 SHEET、HEADER、MAPPING 与数据准备。

总结

MAPPING 是 JQuick-Excel 中连接 Excel 展示字段和 Java 业务字段的明确契约。导出遵循"字段到表头",导入遵循"表头到字段";两者的左右方向相反,但应围绕同一份模板标题成对维护。结合 JObjectConverter.convert、JQuickRow.toRows、导出或导入 XML 解析工厂,以及 JQuickXmlFactory 创建的服务代理,可以把列规则集中在 XML,把业务数据准备保留在 Java。

实践中最值得坚持的是显式验证:字段名对齐映射左侧,工作表名对齐 SHEET,首行标题对齐导入映射左侧,服务接口对齐 XML 的 namespace 和方法名。这样表头变更会变成可见的配置改动,而不是隐藏在列号循环中的运行时问题。

相关推荐
lcj25111 小时前
【C++】set和map——详细使用说明
开发语言·c++·笔记·面试
潼心1412o1 小时前
C++初阶(长期更新)第3讲:类和对象(中)
开发语言·c++
梦幻通灵1 小时前
IDEA 实用快捷键【持续更新】
java·ide·intellij-idea
南归北隐1 小时前
Spring AI Alibaba Graph框架实现Tools工具调用
java·后端·spring·spring ai·spring ai tools
外收内放1 小时前
Python基础语法练习题(40-42)
开发语言·python
开开心心就好1 小时前
视频模糊怎么修复?免费工具支持批量处理
java·前端·人工智能·智能手机·pdf·excel
程序喵大人1 小时前
【C++入门】编译链接模型 - 02 预处理把头文件怎样塞进源文件
开发语言·c++·预处理·编译链接·头文件·源文件
茉莉玫瑰花茶2 小时前
GO [ 并发 · 调度器 ]
开发语言·后端·golang
zhangzeyuaaa2 小时前
深入理解 Ruby 可变对象与不可变对象的原理、坑点与最佳实践
开发语言·后端·ruby