You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Batch双Job联合执行时FlatFileItemWriter仅输出头尾问题

问题根因

异常本质是全局Spring容器共享导致动态注册的Writer Bean跨Job污染,加上Step流绑定逻辑未做隔离,打乱了FlatFileItemWriter的生命周期:

  1. 两个Job在同一个JVM进程内顺序执行,共享同一个ApplicationContext。单独执行单个Job时,容器内只有当前Job动态注册的FlatFileItemWriter,流绑定、生命周期管理都正常;联合执行时,第一个Job执行完成后,它注册的所有FlatFileItemWriter已经执行完close()操作,状态为已关闭,且Bean定义残留在全局容器中。
  2. 原有逻辑在Job对应的Step2中,全量获取容器内所有FlatFileItemWriter绑定为Step管理的流,没有过滤属于其他Job的Writer实例。第二个Job执行时,会把前一个Job残留的、已经关闭的Writer也纳入自己的流生命周期管理,导致当前Job的Writer状态被干扰:open阶段正常写入页眉,close阶段正常写入页脚,但中间write阶段输出流状态异常,业务数据无法写入。
  3. 两个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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 12:21:28