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

Spring Batch处理大MultipartFile:替换静态变量传参方案咨询

Spring Batch 避免静态变量传递MultipartFile数组给ItemReader的解决方案

问题背景

开发Spring图片批处理系统时,当前流程通过静态变量将MultipartFile[]从Service传递给自定义ItemReader,但处理GB级大文件时静态变量会带来内存占用过高、线程安全等问题,需要找到无静态变量的传参方式。

现有代码:

Service类

@Service
public class ImageService {
    @Autowired
    private JobLauncher jobLauncher;
    @Autowired
    private Job job;
    public static MultipartFile[] files = null;

    public void imageFiles(MultipartFile[] files) {
        JobParameters jobParameters = new JobParametersBuilder()
                .addLong("startAt", System.currentTimeMillis()).toJobParameters();
        try {
            ImageService.files = files;
            jobLauncher.run(job, jobParameters);
        } catch (JobExecutionAlreadyRunningException | JobRestartException | JobInstanceAlreadyCompleteException |
                 JobParametersInvalidException e) {
            e.printStackTrace();
        }
    }
}

自定义Reader类

@Service
public class MyCustomReader implements ItemReader<MultipartFile> {
    private int index = 0;

    @Override
    public MultipartFile read() throws Exception {
        MultipartFile[] files = ImageService.files;
        if (index >= files.length)
            return null;
        return files[index++];
    }
}

配置类

@Configuration
@EnableBatchProcessing
@AllArgsConstructor
public class BatchConfig {
    @Autowired
    private JobBuilderFactory jobBuilderFactory;
    @Autowired
    private StepBuilderFactory stepBuilderFactory;
    @Autowired
    private MyCustomReader myCustomReader;
    @Autowired
    private MyCustomProcessor myCustomProcessor;
    @Autowired
    private MyCustomWriter myCustomWriter;

    @Bean
    public Step step() {
        return stepBuilderFactory.get("MyStep").<MultipartFile, MultipartFile>chunk(3)
                .reader(myCustomReader)
                .processor(myCustomProcessor)
                .writer(myCustomWriter)
                .build();
    }

    @Bean
    public Job runBatchJob() {
        return jobBuilderFactory.get("batch")
                .flow(step()).end().build();
    }
}

可行解决方案

方案一:转存临时文件,通过JobParameters传递路径(推荐用于大文件)

将MultipartFile转存到系统临时目录,把文件路径作为Job参数传递,Reader从Job参数中读取路径并加载文件,避免内存中持有大文件对象。

1. 修改ImageService

@Service
public class ImageService {
    @Autowired
    private JobLauncher jobLauncher;
    @Autowired
    private Job job;
    // 定义临时文件存储目录
    private final Path tempDir = Paths.get(System.getProperty("java.io.tmpdir"), "batch-image-temp");

    public void imageFiles(MultipartFile[] files) throws IOException {
        // 创建临时目录
        Files.createDirectories(tempDir);
        List<String> filePaths = new ArrayList<>();

        // 将MultipartFile转存为临时文件
        for (MultipartFile file : files) {
            // 生成唯一文件名避免冲突
            String tempFileName = UUID.randomUUID() + "-" + file.getOriginalFilename();
            Path tempFile = tempDir.resolve(tempFileName);
            Files.copy(file.getInputStream(), tempFile, StandardCopyOption.REPLACE_EXISTING);
            filePaths.add(tempFile.toString());
        }

        // 将路径列表转为逗号分隔字符串,作为Job参数传递
        JobParameters jobParameters = new JobParametersBuilder()
                .addLong("startAt", System.currentTimeMillis())
                .addString("imageFilePaths", String.join(",", filePaths))
                .toJobParameters();

        try {
            jobLauncher.run(job, jobParameters);
        } catch (Exception e) {
            e.printStackTrace();
            // 异常时清理临时文件
            filePaths.forEach(path -> {
                try {
                    Files.deleteIfExists(Paths.get(path));
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            });
        }
    }
}

2. 修改自定义Reader(实现StepExecutionListener)

@Service
@StepScope // 必须添加StepScope才能获取StepExecution上下文
public class MyCustomReader implements ItemReader<File>, StepExecutionListener {
    private int currentIndex = 0;
    private List<File> imageFiles;

    @Override
    public void beforeStep(StepExecution stepExecution) {
        // 从Job参数中读取文件路径列表
        String pathStr = stepExecution.getJobParameters().getString("imageFilePaths");
        String[] paths = pathStr.split(",");
        imageFiles = Arrays.stream(paths)
                .map(File::new)
                .collect(Collectors.toList());
    }

    @Override
    public File read() throws Exception {
        if (currentIndex >= imageFiles.size()) {
            return null;
        }
        return imageFiles.get(currentIndex++);
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        // 批处理完成后清理临时文件
        imageFiles.forEach(File::delete);
        return ExitStatus.COMPLETED;
    }
}

3. 更新配置类

需要将Reader注册为Step监听器,并调整Processor/Writer适配File类型:

@Configuration
@EnableBatchProcessing
@AllArgsConstructor
public class BatchConfig {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final MyCustomReader myCustomReader;
    private final MyCustomProcessor myCustomProcessor;
    private final MyCustomWriter myCustomWriter;

    @Bean
    public Step imageProcessStep() {
        return stepBuilderFactory.get("ImageProcessStep")
                .<File, File>chunk(3) // 调整泛型为File
                .reader(myCustomReader)
                .processor(myCustomProcessor) // 需要修改Processor处理File类型
                .writer(myCustomWriter) // 需要修改Writer处理File类型
                .listener(myCustomReader) // 注册Step监听器
                .build();
    }

    @Bean
    public Job imageBatchJob() {
        return jobBuilderFactory.get("ImageBatchJob")
                .flow(imageProcessStep())
                .end()
                .build();
    }
}

方案二:将Reader设为原型Bean,动态注入文件(适合小文件)

把Reader改为原型作用域,每次批处理时获取新的Reader实例,直接设置MultipartFile[],避免静态变量共享。

1. 修改自定义Reader

@Service
@Scope("prototype") // 原型作用域,每次获取都是新实例
public class MyCustomReader implements ItemReader<MultipartFile> {
    private int currentIndex = 0;
    private MultipartFile[] files;

    // 提供setter方法注入文件数组
    public void setFiles(MultipartFile[] files) {
        this.files = files;
        this.currentIndex = 0; // 重置索引
    }

    @Override
    public MultipartFile read() throws Exception {
        if (files == null || currentIndex >= files.length) {
            return null;
        }
        return files[currentIndex++];
    }
}

2. 修改ImageService动态创建Job/Step

@Service
public class ImageService {
    @Autowired
    private JobLauncher jobLauncher;
    @Autowired
    private JobBuilderFactory jobBuilderFactory;
    @Autowired
    private StepBuilderFactory stepBuilderFactory;
    @Autowired
    private ApplicationContext applicationContext;
    @Autowired
    private MyCustomProcessor myCustomProcessor;
    @Autowired
    private MyCustomWriter myCustomWriter;

    public void imageFiles(MultipartFile[] files) {
        // 获取原型Reader实例
        MyCustomReader reader = applicationContext.getBean(MyCustomReader.class);
        reader.setFiles(files);

        // 动态创建Step(每次用唯一名称避免重复)
        Step step = stepBuilderFactory.get("ImageProcessStep-" + System.currentTimeMillis())
                .<MultipartFile, MultipartFile>chunk(3)
                .reader(reader)
                .processor(myCustomProcessor)
                .writer(myCustomWriter)
                .build();

        // 动态创建Job
        Job job = jobBuilderFactory.get("ImageBatchJob-" + System.currentTimeMillis())
                .flow(step)
                .end()
                .build();

        JobParameters jobParameters = new JobParametersBuilder()
                .addLong("startAt", System.currentTimeMillis())
                .toJobParameters();

        try {
            jobLauncher.run(job, jobParameters);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3. 简化配置类

不需要提前创建Job和Step Bean,只需提供工厂类:

@Configuration
@EnableBatchProcessing
public class BatchConfig {
    @Bean
    public JobBuilderFactory jobBuilderFactory(JobRepository jobRepository) {
        return new JobBuilderFactory(jobRepository);
    }

    @Bean
    public StepBuilderFactory stepBuilderFactory(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        return new StepBuilderFactory(jobRepository, transactionManager);
    }
}

方案对比

  • 方案一:适合GB级大文件,通过临时文件减少内存占用,需处理文件清理,线程安全。
  • 方案二:实现简单,适合小文件场景,直接传递MultipartFile,但大文件会导致内存溢出,需注意线程安全(原型Bean保证每次实例独立)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 00:08:14