Spring Batch双Job联合执行时FlatFileItemWriter仅输出头尾问题
异常本质是全局Spring容器共享导致动态注册的Writer Bean跨Job污染,加上Step流绑定逻辑未做隔离,打乱了FlatFileItemWriter的生命周期:
- 两个Job在同一个JVM进程内顺序执行,共享同一个ApplicationContext。单独执行单个Job时,容器内只有当前Job动态注册的FlatFileItemWriter,流绑定、生命周期管理都正常;联合执行时,第一个Job执行完成后,它注册的所有FlatFileItemWriter已经执行完close()操作,状态为已关闭,且Bean定义残留在全局容器中。
- 原有逻辑在Job对应的Step2中,全量获取容器内所有FlatFileItemWriter绑定为Step管理的流,没有过滤属于其他Job的Writer实例。第二个Job执行时,会把前一个Job残留的、已经关闭的Writer也纳入自己的流生命周期管理,导致当前Job的Writer状态被干扰:open阶段正常写入页眉,close阶段正常写入页脚,但中间write阶段输出流状态异常,业务数据无法写入。
- 两个Job对应的ClassifierCompositeItemWriter也全量持有容器内所有Writer实例,存在路由到错误Writer的风险;同时动态注册的FlatFileItemWriter默认是单例作用域,无法在不同Step/Job中独立初始化实例,进一步放大了生命周期混乱的问题。
按以下顺序修改代码即可解决问题:
第一步:动态注册Writer前先清理其他Job残留的Bean定义
在两个动态配置Writer的Tasklet的execute方法最开头,先移除容器内不属于当前Job的FlatFileItemWriter定义,避免旧Bean残留。以DynamicSchoolWritersConfigurationTasklet为例:@Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory(); BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; // 先清理之前残留的student类writer for (String beanName : beanFactory.getBeanNamesForType(FlatFileItemWriter.class)) { if (beanName.startsWith("student-group")) { registry.removeBeanDefinition(beanName); } } String sql = "select distinct(groupId) from school"; List<Integer> groups = jdbcTemplate.queryForList(sql, Integer.class); for (Integer group : groups) { String name = "school-group" + group + "Writer"; MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.addPropertyValue("name", name); propertyValues.addPropertyValue("lineAggregator", new PassThroughLineAggregator<>()); propertyValues.addPropertyValue("resource", new FileSystemResource("school-" + group + ".txt")); propertyValues.addPropertyValue("headerCallback", (FlatFileHeaderCallback) writer -> writer.write("header-school")); propertyValues.addPropertyValue("footerCallback", (FlatFileFooterCallback) writer -> writer.write("footer-school")); GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClassName(FlatFileItemWriter.class.getName()); beanDefinition.setPropertyValues(propertyValues); // 给动态注册的writer设置step作用域,保证每个step独立生成实例 beanDefinition.setScope("step"); registry.registerBeanDefinition(name, beanDefinition); } return RepeatStatus.FINISHED; }同理在DynamicStudentWritersConfigurationTasklet开头,移除所有
school-group开头的Writer定义,注册Student Writer时也加上beanDefinition.setScope("step")配置。第二步:Step绑定流时只绑定当前Job对应的Writer
修改两个Step2的流绑定逻辑,过滤掉不属于当前Job的Writer,不要把其他Job的Writer纳入当前Step的生命周期管理。以schoolStep2为例:@Bean @JobScope public Step schoolStep2(SchoolReader reader, @Qualifier("schoolClassfierItemWriter") ClassifierCompositeItemWriter<School> writer) { SimpleStepBuilder<School, School> schoolStep2 = stepBuilderFactory.get("readWriteSchools") .<School, School>chunk(2) .reader(reader) .writer(writer); Map<String, FlatFileItemWriter> beansOfType = applicationContext.getBeansOfType(FlatFileItemWriter.class); for (Map.Entry<String, FlatFileItemWriter> entry : beansOfType.entrySet()) { // 只绑定当前schoolJob对应的writer if (entry.getKey().startsWith("school-group")) { schoolStep2.stream(entry.getValue()); } } return schoolStep2.build(); }studentStep2做相同修改,判断Bean名称以
student-group开头才绑定到流。第三步:Classifier路由逻辑做隔离
修改两个ClassifierCompositeItemWriter的定义,只持有当前Job对应的Writer实例,避免路由到错误的Writer。以SchoolWriter为例:@Bean(name = "schoolClassfierItemWriter") @StepScope public ClassifierCompositeItemWriter<School> itemWriter() { Map<String, FlatFileItemWriter> allWriters = applicationContext.getBeansOfType(FlatFileItemWriter.class); Map<String, FlatFileItemWriter<School>> schoolWriters = new HashMap<>(); // 过滤出仅属于schoolJob的writer allWriters.forEach((beanName, writerInstance) -> { if (beanName.startsWith("school-group")) { schoolWriters.put(beanName, writerInstance); } }); Classifier<School, FlatFileItemWriter<School>> classifier = school -> schoolWriters.get("school-group" + school.getGroupId() + "Writer"); return new ClassifierCompositeItemWriterBuilder<School>().classifier(classifier).build(); }StudentWriter做相同修改,仅过滤
student-group开头的Writer供路由使用。
修改完成后,两个Job无论是独立执行还是联合执行,Writer的生命周期都会被正确隔离,不会再出现只有页眉页脚无业务数据的问题。
内容的提问来源于stack exchange,提问作者igarciadev

