Spring Batch分块读取多层级XML 跨层级获取元素信息方案
多层级大体积XML分块读取实现方案
问题场景
需要读取如下结构的XML文件:
<?xml version="1.0"?> <doc xmlns="http://www.hello.com.br/parent.xsd"> <fileheader> <name>name</name> <protocol>134567</protocol> <datefile>2022-06-17T07:51:47</datefile> <dateref>2022-06-17</dateref> </fileheader> <file> <filetype> <one>Hello</one> <two>Spring</two> <three>Batch</three> <four>World</four> <parent> <five>010</five> <children> <id>885447534111124</id> <value>0</value> <repeatChildren> <date>2022-06-07</date> <value>0.00</value> <repeatChildrenTwo> <id>123456789</id> <value1>309.02</value1> <value2>0</value2> <value3>0</value3> <value4>309.02</value4> </repeatChildrenTwo> </repeatChildren> <!-- 更多repeatChildren节点 --> </children> <!-- 更多children节点 --> </parent> </filetype> </file> </doc>
业务要求
- 最终结果对象需提取
children、repeatChildren节点核心信息,同时关联获取上层filetype、parent节点的信息 - 数据规模:
parent节点下可包含数千个children节点,单个children下可包含数千个repeatChildren节点,全文件标签量可达百万级
现有实现问题
当前基于Spring Batch StaxEventItemReader的实现代码如下:
@Bean @StepScope public StaxEventItemReader<Doc> docReader( @Value("#{jobParameters['input.file.name']}") String inputFile) { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setClassesToBeBound(Doc.class); marshaller.setMappedClass(Doc.class); return new StaxEventItemReaderBuilder<Doc>().name("docReader") .resource(new FileSystemResource(inputFile)).addFragmentRootElements("doc").unmarshaller(marshaller) .build(); }
该配置将根节点
doc作为片段根元素,会一次性加载整个XML文档传递给ItemProcessor,无法遵循chunk分块处理逻辑,大文件场景下极易触发内存溢出。
实现方案
核心思路:放弃全量文档绑定,将最小处理粒度的重复节点作为分块片段根,流式解析过程中仅暂存必要的上层节点上下文,不构建全量节点树
具体实现分为3个核心调整:
- 重新指定片段根元素
不要将最外层doc作为片段根,根据你的业务处理粒度选择最小重复单元作为片段根:如果最终处理维度是repeatChildren就配置repeatChildren为根,如果处理维度是children就配置children为根。注意XML存在默认命名空间http://www.hello.com.br/parent.xsd,配置时需要带上命名空间前缀,否则无法识别节点。 - 自定义流读取器缓存上层上下文
通过自定义XMLStreamReader包装类,在流式遍历节点的过程中,解析到fileheader、filetype、parent这类非重复上层节点时,将需要的字段值直接存在读取器的成员变量中作为上下文,不需要构建完整的节点对象,内存开销极低。当解析到配置的片段根节点时,将暂存的上层信息和当前片段的反序列化结果组装成完整的业务对象,传给后续的chunk处理链路。 - 调整JAXB绑定范围
取消全量Doc类的JAXB绑定,仅针对片段根对应的业务类做绑定,每次反序列化只处理当前片段的小体量XML内容,避免加载无关节点数据。
参考实现代码
@Bean @StepScope public StaxEventItemReader<RepeatChildrenRecord> repeatChildrenReader( @Value("#{jobParameters['input.file.name']}") String inputFile) { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); // 仅绑定最小处理单元的类,不绑定全量Doc结构 marshaller.setClassesToBeBound(RepeatChildrenRecord.class); marshaller.setMappedClass(RepeatChildrenRecord.class); return new StaxEventItemReaderBuilder<RepeatChildrenRecord>() .name("repeatChildrenReader") .resource(new FileSystemResource(inputFile)) // 核心:配置最小重复单元为片段根,带上命名空间 .addFragmentRootElements("{http://www.hello.com.br/parent.xsd}repeatChildren") .unmarshaller(marshaller) // 自定义XML流工厂,返回带上下文缓存能力的读取器 .xmlStreamReaderFactory(resource -> { XMLInputFactory factory = XMLInputFactory.newInstance(); // 关闭外部实体解析避免安全问题 factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); XMLStreamReader nativeReader = factory.createXMLStreamReader(resource.getInputStream()); // UpperContextCachingReader为自定义包装类,负责缓存上层节点字段、组装最终结果对象 return new UpperContextCachingReader(nativeReader); }) .build(); }
优化说明
- 该方案下内存中仅保留当前解析的片段内容和上层固定节点的字段值,哪怕文件标签量达百万级,内存占用也会稳定在低水平,完全符合chunk分块处理的设计要求
- 如果处理粒度为
children维度,只需将片段根改为children节点,解析过程中遇到内部的repeatChildren节点时暂存到当前children的上下文列表中,等children节点解析结束时输出组装好的完整children对象即可 - 自定义上下文缓存读取器只需处理节点的开始、文本事件即可,逻辑简单,性能损耗极低
内容的提问来源于stack exchange,提问作者Guilherme Bernardi
相关产品推荐
相关产品推荐

