Windows环境下Spring Batch批量监听无法删除文件:文件被占用问题
你遇到的这个问题很典型,是Windows和Unix-like系统文件锁机制差异导致的。先帮你拆解下问题根源,再给出可行的解决办法:
问题背景
你开发的Spring Batch作业在Linux/macOS上运行正常,但在Windows下执行RemovingListener的afterStep方法删除文件时,抛出了文件被占用的异常:
2019-09-09 12:08:13.752 WARN 4028 --- [ main] c.b.m.b.RemovingListener : Failed to remove chunk 0b9a2623-b4c3-42b2-9acf-373a2d81007c.csv
java.nio.file.FileSystemException: D:\chunk\1.csv: The process cannot access the file because it is being used by another process.
...(堆栈信息略)
你的作业核心逻辑是通过MultiResourcePartitioner分区处理目录下的文件,每个Slave Step使用FlatFileItemReader读取文件,完成后通过RemovingListener删除对应文件。
问题根源
Windows和Unix-like系统的文件访问机制有本质区别:
- Linux/macOS:即使文件被进程打开,只要没有独占锁,其他进程可以直接删除文件(目录项被移除,磁盘空间会在所有打开句柄关闭后释放)。
- Windows:只要有进程持有文件的打开句柄,任何尝试删除/修改文件的操作都会直接失败,抛出"文件被占用"的错误。
回到你的代码,问题出在文件句柄的释放时机:
你在afterStep阶段尝试删除文件,但此时FlatFileItemReader持有的文件输入流可能还没被完全关闭。Spring Batch在Windows下的资源回收时机和Unix系统不同,加上分区Step的多实例特性,Reader的句柄可能没有及时释放,导致删除操作失败。
解决办法
我整理了几个从易到难的解决方案,你可以根据自己的场景选择:
1. 确保Reader是Step作用域并正确配置
首先,检查你的FlatFileItemReader是否是@StepScope的。每个Slave Step需要独立的Reader实例,这样每个文件读完后,对应的Reader会被及时销毁,释放文件句柄。
修改Reader的配置:
@Bean @StepScope public FlatFileItemReader<String> reader(@Value("#{stepExecutionContext['fileName']}") Resource resource) { return new FlatFileItemReaderBuilder<String>() .resource(resource) .lineMapper(new PassThroughLineMapper()) .build(); }
使用FlatFileItemReaderBuilder创建Reader,它会自动实现ItemStream接口,确保Step结束时调用close()方法释放资源。
2. 延迟/异步删除文件
如果即时删除还是有问题,可以先把文件重命名到临时目录,再异步删除临时文件——Windows允许对打开的文件进行重命名操作:
修改RemovingListener的afterStep方法:
@Override public ExitStatus afterStep(@NonNull StepExecution stepExecution) { try { Path sourcePath = resource.getFile().toPath(); // 创建临时目录(如果不存在) Path tempDir = Path.of("D:\\chunk\\temp"); Files.createDirectories(tempDir); // 重命名文件到临时目录 Path tempPath = tempDir.resolve(sourcePath.getFileName() + ".tmp"); Files.move(sourcePath, tempPath, StandardCopyOption.REPLACE_EXISTING); // 异步删除临时文件,不阻塞主流程 CompletableFuture.runAsync(() -> { try { Files.deleteIfExists(tempPath); } catch (IOException e) { log.warn("Failed to remove temp file {}", tempPath.getFileName(), e); } }); } catch (IOException e) { log.warn("Failed to process file {}", resource.getFilename(), e); } return stepExecution.getExitStatus(); }
3. 自定义强制关闭的Reader
如果上面的方法都不行,可以自定义Reader,强制在close()方法中关闭文件流:
@Slf4j public class CloseableFlatFileItemReader extends FlatFileItemReader<String> implements ResourceAwareItemReaderItemStream { @Override public void setResource(Resource resource) { super.setResource(resource); } @Override public void close() throws ItemStreamException { super.close(); // 额外确保底层的Reader被关闭 try { if (getReader() != null) { getReader().close(); } } catch (IOException e) { log.error("Failed to close file reader", e); } } }
然后在Slave Step的配置中使用这个自定义Reader:
@Bean @StepScope public CloseableFlatFileItemReader reader(@Value("#{stepExecutionContext['fileName']}") Resource resource) { CloseableFlatFileItemReader reader = new CloseableFlatFileItemReader(); reader.setResource(resource); reader.setLineMapper(new PassThroughLineMapper()); return reader; }
内容的提问来源于stack exchange,提问作者gstackoverflow

