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

FlatFileFooterCallback异常无法进入SkipListener致Job终止求解决方案

问题描述

在Spring Batch任务中,FlatFileFooterCallback的writeFooter方法抛出异常时,无法被配置的SkipListener捕获,导致任务(Job)直接终止,无法继续执行。

核心原因

Spring Batch的skip机制和SkipListener仅覆盖Chunk内的Item读写处理环节(即reader.read()、processor.process()、writer.write(List)中的异常)。而FlatFileFooterCallback.writeFooter()是在Writer完成所有Chunk处理后、资源关闭前执行的收尾操作,不属于Chunk容错的覆盖范围,因此异常会直接向上传播导致Job终止。

解决方案

1. 修正设计:将数据库操作移至StepExecutionListener

由于FlatFileFooterCallback的核心职责是生成文件的footer内容,而非执行批量数据库插入,将该逻辑迁移到StepExecutionListener的afterStep方法是更合理的设计,同时能自主控制异常处理:

步骤1:实现StepExecutionListener

@Component
public class MMSSummaryAfterStepHandler implements StepExecutionListener {

    @Autowired
    @Qualifier("jdbcTemplate")
    private JdbcTemplate jdbcTemplate;

    @Override
    public void beforeStep(StepExecution stepExecution) {
        // 无需实现,留空即可
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        ExecutionContext executionContext = stepExecution.getExecutionContext();
        HashMap<String, MMSDto> summaryMap = (HashMap<String, MMSDto>) executionContext.get("Summary");
        List<MMSDto> summaryValues = new ArrayList<>(summaryMap.values());

        try {
            jdbcTemplate.batchUpdate(InterConnectUtils.SUMMARY_EVENT_INSERT, new BatchPreparedStatementSetter() {
                @Override
                public void setValues(PreparedStatement ps, int i) throws SQLException {
                    MMSDto mmsDto = summaryValues.get(i);
                    ps.setString(1, mmsDto.getBatchNumber());
                    ps.setString(2, mmsDto.getEntityCode());
                    ps.setString(3, mmsDto.getInputFileType());
                    ps.setInt(4, mmsDto.getTotalType());
                    ps.setString(5, mmsDto.getServiceType());
                    ps.setString(6, mmsDto.getCallInd());
                    ps.setString(7, mmsDto.getCallDateTime());
                    ps.setString(8, mmsDto.getEventDirection());
                    ps.setString(9, mmsDto.getProductID());
                    ps.setInt(10, mmsDto.getTargetInd());
                    ps.setInt(11, mmsDto.getNoRecords());
                    ps.setLong(26, mmsDto.getTotalDuration());
                }

                @Override
                public int getBatchSize() {
                    return summaryMap.size();
                }
            });
            return stepExecution.getExitStatus();
        } catch (Exception e) {
            // 复用SkipListener的错误记录逻辑
            recordFooterError(summaryValues, e);
            // 标记Step状态为失败,但不终止Job
            return ExitStatus.FAILED.addExitDescription("汇总数据插入失败:" + e.getMessage());
        }
    }

    private void recordFooterError(List<MMSDto> summaryValues, Throwable t) {
        Exception ex = new Exception(t);
        for (MMSDto mmsDto : summaryValues) {
            jdbcTemplate.update(InterConnectUtils.BATCH_ERROR_INSERT, ps -> {
                StringWriter sw = new StringWriter();
                ex.printStackTrace(new PrintWriter(sw));
                String exceptionAsString = sw.toString();
                ps.setDate(1, new java.sql.Date(new Date().getTime()));
                ps.setString(2, mmsDto.getBatchNumber());
                ps.setString(3, "FOOTER_SUMMARY");
                Reader reader = new StringReader(exceptionAsString + "--" + ex.getMessage());
                ps.setClob(4, reader);
            });
        }
    }
}

步骤2:修改Step配置
移除FlatFileItemWriter中的footerCallback配置,并注册StepExecutionListener:

@Bean
@StepScope
public FlatFileItemWriter<BaseDTO> mmsItemWriter(@Value("#{stepExecutionContext['fileName']}") String filePath) throws Exception {
    BeanWrapperFieldExtractor<BaseDTO> fieldExtractor = new BeanWrapperFieldExtractor<>();
    fieldExtractor.setNames(mmsWriterFields);
    fieldExtractor.afterPropertiesSet();
    FormatterLineAggregator<BaseDTO> lineAggregator = new FormatterLineAggregator<>();
    lineAggregator.setFormat(mmsWriterFieldsFormat);
    lineAggregator.setFieldExtractor(fieldExtractor);
    String fileName = FilenameUtils.getName(filePath);
    Resource outputResource = new FileSystemResource(new File(sucessPath + fileName + inputDataSource));
    
    return new FlatFileItemWriterBuilder<BaseDTO>()
            .name("mmsWriter")
            .resource(outputResource)
            .lineAggregator(lineAggregator)
            .shouldDeleteIfEmpty(true)
            .build(); // 移除footerCallback配置
}

@Bean
public Step mmsSlaveStep() throws Exception {
    return stepBuilderFactory.get("mmsSlaveStep")
            .<BaseDTO, BaseDTO>chunk(chunkSize)
            .reader(mmsReader(null))
            .processor(mmsProcessor())
            .writer(mmsItemWriter(null))
            .faultTolerant()
            .skipLimit(skipErrorCount)
            .skip(Exception.class)
            .listener(itemSkipListener())
            .listener(new MMSSummaryAfterStepHandler()) // 注册新的Listener
            .build();
}

2. 临时规避:在writeFooter中手动捕获异常

如果不想调整现有结构,可以在MMSSummaryWriter的writeFooter方法中直接捕获异常,避免抛出导致Job终止:

@Override
public void writeFooter(Writer writer) throws IOException {
    ExecutionContext executionContext = stepExecution.getExecutionContext();
    HashMap<String, MMSDto> summaryMap = (HashMap<String, MMSDto>) executionContext.get("Summary");
    List<MMSDto> summaryValues = new ArrayList<>(summaryMap.values());

    try {
        jdbcTemplate.batchUpdate(InterConnectUtils.SUMMARY_EVENT_INSERT, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                MMSDto mmsDto = summaryValues.get(i);
                ps.setString(1, mmsDto.getBatchNumber());
                ps.setString(2, mmsDto.getEntityCode());
                ps.setString(3, mmsDto.getInputFileType());
                ps.setInt(4, mmsDto.getTotalType());
                ps.setString(5, mmsDto.getServiceType());
                ps.setString(6, mmsDto.getCallInd());
                ps.setString(7, mmsDto.getCallDateTime());
                ps.setString(8, mmsDto.getEventDirection());
                ps.setString(9, mmsDto.getProductID());
                ps.setInt(10, mmsDto.getTargetInd());
                ps.setInt(11, mmsDto.getNoRecords());
                ps.setLong(26, mmsDto.getTotalDuration());
            }

            @Override
            public int getBatchSize() {
                return summaryMap.size();
            }
        });
    } catch (Exception e) {
        // 记录错误到数据库
        recordFooterError(summaryValues, e);
        // 可选:标记Step执行状态
        stepExecution.setExitStatus(ExitStatus.FAILED.addExitDescription("Footer汇总插入失败"));
        // 不抛出异常,Job会继续执行
    }
}

// 复用错误记录逻辑
private void recordFooterError(List<MMSDto> summaryValues, Throwable t) {
    Exception ex = new Exception(t);
    for (MMSDto mmsDto : summaryValues) {
        jdbcTemplate.update(InterConnectUtils.BATCH_ERROR_INSERT, ps -> {
            StringWriter sw = new StringWriter();
            ex.printStackTrace(new PrintWriter(sw));
            String exceptionAsString = sw.toString();
            ps.setDate(1, new java.sql.Date(new Date().getTime()));
            ps.setString(2, mmsDto.getBatchNumber());
            ps.setString(3, "FOOTER_WRITER");
            Reader reader = new StringReader(exceptionAsString + "--" + ex.getMessage());
            ps.setClob(4, reader);
        });
    }
}

内容的提问来源于stack exchange,提问作者Rakesh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 16:35:14