Java常用类库-29-CSV方言与流式记录契约
一行文本不一定是一条商品记录
商品导入文件有 sku、label、note 三列。label 可以包含逗号,note 可以包含换行,某个备注也可能为空。用 readLine 加 split 处理,既会把引号内的逗号拆成多列,也会把一个多行备注拆成多条记录。错误通常不表现为立即异常,而是字段错位后继续进入后续业务。
本篇固定 Commons CSV 1.14.1,提交 e14ef86d34211f9d1b9b6040c5f6dbafc93ba907。BOM 处理使用 Commons IO 2.22.0。实验契约是按明确方言读取逻辑记录,拒绝缺失或重复表头,逐条校验列数,再打印为同一方言;往返验收比较字段值,不要求字节逐一相同。完整测试见 Chapter29Test.java,命令见 RUN.md。
CSV 是结构语法,商品字段约束是业务规则,电子表格公式则属于消费程序的解释规则。三个层面必须分开。一个 CSVParser 成功返回的记录,仍然可能列数不符、价格格式错误,或者在电子表格中被解释为公式。
方言必须成为接口的一部分
测试以 CSVFormat.RFC4180 为起点,启用从第一条记录读取表头,并跳过表头作为数据的交付。同时显式禁止缺失列名和重复列名。配置写在一个 format 方法中,避免读取端各处分散选择默认值。
CSVFormat 固定源码将分隔符、引号、记录分隔、空白、空记录和表头政策组合为格式。RFC4180 只是一个具体起点,不代表所有叫作 CSV 的文件都遵循完全相同的规则。制表符、分号、反斜杠转义或不同空值约定,需要选择相应方言并写入导入协议。
空字符串也不必然等于业务 null。本组文件的第二条记录以逗号后直接结束,读取后的 note 为长度零字符串。测试没有配置 nullString,因此不能把这个空值解释为数据库 NULL。导入层若要做这种转换,必须结合字段定义;否则空备注、未填写价格与字面文本 NULL 可能被合并成同一种值。
方言变更应该像接口变更一样对待。如果发送方改了分隔符而接收方仍按逗号解析,整条数据可能被当成一列,语法本身仍可被解析。列数和必需表头校验因此是必要的后续步骤,不能只依赖 parser 是否抛异常来判断文件符合业务。
引号、逗号和多行字段必须一起验收
往返样本包含两个商品。第一条 label 为 sale,new,note 为 line1 换行 line2;第二条 label 含一个双引号,note 为空。源文件通过引用字段与成对双引号表达这些值。
第一次解析采用 for-each 遍历 CSVRecord,逐条检查 isConsistent 后交给 CSVPrinter,没有调用 getRecords 将全部记录加载到 List。打印端显式写出 sku、label、note 表头。第二次解析打印结果,再核对逗号、多行、内部引号与空字段均保留,并确认没有第三条记录。
这个测试比较的是字段语义。打印器可以选择合法的引用方式和记录分隔符,产生与输入不同的字节形式;只要协议允许,这种差异不是数据丢失。反过来,单纯比较文件行数也无法证明记录完整,因为一条记录内可以包含多个物理换行。
第一项可迁移模式是先定义往返的不变量。数据交换通常要求字段值、字段顺序与记录数一致;若业务还要求原始字节、注释或原始引用风格保持不变,则解析再打印可能不适合,应保存原文件。把“无损”拆成具体不变量,测试才有明确对象。
BOM 在字符解码之前处理
UTF-8 文件的开头可能带 BOM。如果直接把它当普通字符读入,首列表头可能不是 sku,而是包含不可见前缀的另一个字符串,随后按 sku 查找就会失败。实验通过 BOMInputStream 去掉已识别的 UTF-8 BOM,再使用明确的 UTF-8 InputStreamReader。
测试读取后的 headerNames 必须正好是 sku、price,首条商品的 price 必须为 100。这既检查了字节前缀处理,也检查了表头到字段访问的实际结果。只打印文件内容通常看不出不可见前缀,不能替代这种按名称访问的断言。
BOMInputStream 不是自动字符集识别器。默认配置识别 UTF-8 BOM,并不意味着任何 UTF-16 文件都能按 UTF-8 读取。实际导入应限定编码,或实现明确的编码选择政策,再分别验收;本组实验没有处理任意编码猜测。
Reader、parser 与底层流的关闭也需要一致的所有权。示例把 Reader 和 CSVParser 都放入 try-with-resources,parser 关闭其解析资源后,外层 Reader 的关闭仍安全。打印器关闭后才能把输出视为已完成,真实文件输出还应区分生成完成与业务发布完成。
重复表头必须在字段映射之前拒绝
一个文件若有两列都叫 sku,按名称访问无法表达究竟要哪一列。不同允许重复政策可能产生不同的索引映射,不能让某个默认覆盖行为决定业务意义。本例设置 DuplicateHeaderMode.DISALLOW,sku,sku 在建表头阶段抛 IllegalArgumentException。
sku, 则属于缺失列名,本例同样拒绝。表头存在但未包含必需的 price,是另一种模式错误,还需要业务必需列检查。本地测试只直接覆盖空列名与重复列名,不把它们写成完整必需列集合校验。
CSVParser 固定源码在 createHeaders 流程中建立列名映射,根据允许缺失列名和重复列名政策决定是否报错。表头配置因此影响的不只是显示名称,也影响后续按名称寻址的确定性。
动态导入如果允许附加列,需要区分“必需列缺失”“未知列出现”和“同名列重复”。未知列可以按协议忽略或拒绝,但不能因希望宽松兼容,就顺带放宽重复列与列数不符。不同异常来源应形成不同诊断信息,便于上传方修复文件。
列数不符与引号未闭合是不同失败
样本 sku,price 后只有一列 A,parser 仍会产生 CSVRecord。isConsistent 返回 false,recordNumber 为 1,再读取 price 抛 IllegalArgumentException。这个实验说明语法解析成功不代表记录符合表头结构。导入循环应先验证列数,避免在后续业务访问时才出现难以定位的异常。
另一个样本在字段开始双引号后结束文件。遍历器的 hasNext 触发读取并抛出运行时异常,消息包含 line。异常发生在惰性遍历阶段,而不是一定发生在创建 parser 的那一行,因此 try-catch 和资源管理范围应覆盖整个遍历过程。
逻辑记录号与物理行号不能混用。多行字段使一个记录跨越多行;记录号适合定位第几个商品,行号适合辅助查看原始文本。生产诊断可以同时保留文件标识、最后成功记录号、解析器提供的位置和原始异常,但不应把两种计数拼成一个没有定义的“第 N 行”。
本地测试只要求异常消息包含行信息,没有把完整英文消息固定为长期接口。库版本升级可能改变措辞而不改变失败行为。对外错误响应应由应用定义稳定的错误码与说明,底层异常作为诊断细节保留。
流式迭代仍需要单条记录预算
for-each 避免一次保存全部 CSVRecord,却不能保证任意输入都只占固定内存。一条记录可以含一个很大的引用字段,parser 在交付该记录前仍要处理其内容。记录数上限也不能限制第一条记录的大小。
因此,导入入口还应有实际输入字节上限,必要时对字段长度、记录数量和处理时长设置政策。字段长度在记录构造后检查,只能防止继续交付,未必防止此前的分配。需要严格分配边界时,要评估解析器能力与外围输入预算共同能保证什么。
本篇实验使用内存中的小样本,没有做大文件吞吐或峰值堆占用测量。“流式往返”在这里特指逐条迭代并输出,而不是声称已经验证所有异常输入下的恒定内存。第二项可迁移模式是把迭代方式和资源上限分别验收,不能用 API 返回 Iterator 代替预算设计。
CSV 引号不能消除电子表格公式
打印单字段 =1+1,输出仍以 =1+1 开头。CSV 引用只解决分隔符、引号和换行的结构表达;消费程序若把单元格解释为公式,合法 CSV 不会自动改变这种语义。
实验另放了一个极小的 spreadsheetText 策略:首字符属于等号、加号、减号、@ 或指定控制字符时,加一个前导单引号。断言同时覆盖 =1+1 与 -1,后者说明这项策略会把负数字段也改成文本。因此它不能无条件用于所有列,更不能作为适用于全部电子表格产品的安全保证。
面向程序的原始数据 CSV 与面向人工查看的电子表格导出,应分别定义字段类型与转换政策。若必须保留精确数字和文本类型,使用能够显式指定单元格类型的格式可能更容易表达要求。本组实验没有启动 Excel 或其他电子表格程序,前缀策略只作为独立业务转换示例。
JDK 替代与改动练习
解析器读取成功后,业务写入也需要有边界。逐条保存商品可以节省内存,却可能在后面出现坏记录时已经提交前面一部分。导入协议应选择整批事务、先暂存后确认,或允许部分成功并返回逐项结果。不能因为 CSV 解析使用 try-with-resources,就认为数据库写入也会随解析异常自动回滚。
重试还需要稳定标识。文件中第几个逻辑记录可以用于错误定位,但文件重新排序后该编号会变化,不适合单独作为商品幂等键。商品标识、文件批次标识和记录位置承担不同用途,应分别保留。这样既能重放失败批次,也能判断一次重复上传是否会再次产生业务修改。
JDK 的 Reader 和字符串处理能提供字符输入,但没有直接等价的 CSV 方言解析器。只有在协议确实禁止分隔符、引号和换行进入字段时,简单分割才可能满足一个更窄的自定义格式;这样的格式约束应明确声明,不能仍假定它支持通用 CSV。
四个测试在双 JDK 上通过:语义往返、BOM 与表头拒绝、缺列与未闭合引号、公式数据与独立前缀政策。手算题:一条 note 含两个换行,文件物理行数是否还能用于商品计数?不能,应统计解析后的逻辑记录。改动练习是增加一条列数过多的记录,再断言 isConsistent 为 false,并保留该记录的逻辑编号。
| 判断关键词 | 可迁移模式 | 具体选择 |
|---|---|---|
| 往返 | 明确保持的语义不变量 | 比较字段与记录,不默认比较字节 |
| 表头 | 映射必须无歧义 | 拒绝缺失列名和重复名称 |
| Iterator | 迭代与资源预算分开 | 输入总量与单记录约束分别设计 |
| 公式 | 结构语法与消费语义分开 | 电子表格政策按字段和目标程序定义 |
可独立复跑的 Maven 项目见 下载 25–30 实验包,包含固定依赖配置、完整源码与 Maven Wrapper。
系列起点:可复现基线。


