概述
这篇文章出现的原因是为客户应用程序中需要添加一项新功能,使其可以随时处理复制到目录中的文件。监控目录是否变更是许多计算机系统和应用程序中的一项常见任务。在 Java 生态系统中,有不同的选项可用于执行此类任务。下面列出了几种可能的解决方案:
- Java WatchService API:它是在 Java 7 中引入的一个比较低级的特性。
- Apache commons io:软件包监控器提供了一个用于监控文件系统事件的组件。
- Spring Integration's file support:这是 Spring 集成项目的一部分,该项目支持多种企业集成模式。
- Spring Boot 开发者工具中的 Filewatch 包:它允许监视本地文件系统的更改。
我们将选择第四种实现方式,因为它和spring集成,很容易实现。
用例
我们希望通过将 csv 文件复制到特定位置,在应用程序中创建新客户。文件完全传输后将被读取。然后,csv 文件将被验证、处理并移动到目标目录。以下是 csv 文件的示例:
name, email, dob, info, vip
James Hart,james.hart@gmail.com,12/05/2002,Information for James,No
Will Avery,will.avery@gmail.com,23/10/1991,Information for Will,Yes
Anne Williams,anne.williams@gmail.com,12/05/1975,Information for Anne,No
Julia Norton,julia.norton@gmail.com,23/10/1984,Information for Julia,Yes
csv 文件将始终包含标题行和按特定顺序排列的 5 列。
准备工作
该项目采用了以下技术:
- Java 21
- Spring Boot 3.1.5
- Maven 3.9.1
您至少需要 java 17+、Spring Boot 3+ 和 Maven 3.8+ 来运行源代码。
依赖关系
必须将一种新的依赖项添加到 pom.xml 文件中才能导入必要的类。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
这就是我们需要先准备好或者掌握的东西
监控目录
用于监视特定目录的文件更改的类是 FileSystemWatcher。它带有三个构造函数,但您很可能会使用接受三个参数的构造函数
public FileSystemWatcher(boolean daemon,
Duration pollInterval,
Duration quietPeriod)
让我们看一下每个参数
- deamon:是否有一个守护线程监视变化。如果您希望在 jvm 停止时终止线程(监控),请将其设置为 true。
- pollInterval:再次检查更改之间等待的时间。
- QuietPeriod:确保检测到更改后等待的时间。如果您将大文件传输到该目录,则必须考虑这一点以避免文件损坏。
由于我们希望在不影响源代码的情况下更改上述所有参数值,因此将自定义属性添加到 application.properties 文件中
application.file.watch.daemon=true
application.file.watch.pollInterval=5
application.file.watch.quietPeriod=1
application.file.watch.directory=C:\workspace\files\customer
应用程序将每 5 分钟扫描一次目录是否有修改。如果更改将在 1 分钟后触发。这已经足够了,因为 csv 文件很小(小于 1 MB)。 相应的 Java 代码可以加载其中的值。
@ConfigurationProperties(prefix = "application.file.watch")
public record FileWatcherProperties(
@NotBlank String directory,
boolean daemon,
@Positive Long pollInterval,
@Positive Long quietPeriod
) {}
下一步是定义 bean 类型为 FileSystemWatch 的配置类
@Configuration
@EnableConfigurationProperties(FileWatcherProperties.class)
public class CustomerFileWatcherConfig {
// ommiting class members, constructor and logger
@Bean
FileSystemWatcher fileSystemWatcher() {
var fileSystemWatcher = new FileSystemWatcher(
properties.daemon(),
Duration.ofMinutes(properties.pollInterval()),
Duration.ofMinutes(properties.quietPeriod()));
fileSystemWatcher.addSourceDirectory(
Path.of(properties.directory()).toFile());
fileSystemWatcher.addListener(
new CustomerAddFileChangeListener(fileProcessor));
fileSystemWatcher.setTriggerFilter(
f -> f.toPath().endsWith(".csv"));
fileSystemWatcher.start();
logger.info(String.format("FileSystemWatcher initialized.
Monitoring directory %s",properties.directory()));
return fileSystemWatcher;
}
@PreDestroy
public void onDestroy() throws Exception {
logger.info("Shutting Down File System Watcher.");
fileSystemWatcher().stop();
}
}
我们来回顾一下 fileSystemWatcher() 方法:
- 首先创建一个 fileSystemWatcher 实例,将 bean 属性中的值作为参数传递。bean 属性对象由 spring 容器管理并通过构造函数注入。
- 调用 addSourceDirectory 方法。它需要一个代表要监视的目录的文件。
- addListener 方法采用文件更改事件的侦听器。FileChangeListener 是一个函数式接口,当文件发生更改时,将调用其方法 onChange。
- (可选)可以在 setTriggerFilter 方法中设置 FileFilter 以限制触发更改的文件。计算结果为布尔值的 lambda 表达式用于将文件限制为 csv。
- 最后一个方法是 start 来启动监视源目录的更改。
请注意,有一个 predestroy 连接方法可以在 jvm 停止时正常关闭观察程序。
添加监听器
第二步是实现一个可以处理文件的 FileChangeListener。该接口也是函数式的,因此可以使用 lambda 表达式或方法引用。在我们的例子中,最好将它放在自己的类中,因为这样可以提高可读性。
public class CustomerAddFileChangeListener implements
FileChangeListener {
private static Logger logger = LoggerFactory.getLogger(
CustomerAddFileChangeListener.class);
private CustomerCSVFileProcessor fileProcessor;
public CustomerAddFileChangeListener(
CustomerCSVFileProcessor fileProcessor) {
this.fileProcessor = fileProcessor;
}
@Override
public void onChange(Set<ChangedFiles> changeSet) {
for(ChangedFiles files : changeSet)
for(ChangedFile file: files.getFiles())
if (file.getType().equals(ChangedFile.Type.ADD))
fileProcessor.process(file.getFile().toPath());
}
}
正如前面提到的,当文件发生更改时,将调用 onChange 方法。参数 Set 是更改的文件的集合(自轮询间隔开始以来)。迭代完changeSet后,就可以通过getFiles方法从每个ChangedFiles对象中获取文件。因此,嵌套循环将用于访问各个文件。单个文件的类型为 ChangedFile,它提供对更改/事件的文件和类型的访问(这是一个具有三个值 ADD、DELETE 和 MODIFY 的枚举)。
回到代码,if 语句检查类型以确保仅处理 ADD 事件。其他将被忽略。CustomerCSVFileProcessor 类执行所有工作。
处理文件
处理文件的业务逻辑可以在 process 方法中找到(在 FileProcessor 接口中声明)。它需要 CustomerService 将 Customer 保存在数据库中,因此它是构造函数注入的。CustomerCSVFileProcessor 类被标记为组件,因为它也被注入到 Watcher 中。
@Component
public class CustomerCSVFileProcessor implements FileProcessor {
public static final int NUMBER_OF_COLUMNS = 5;
private static Logger logger = LoggerFactory.getLogger(
CustomerCSVFileProcessor.class);
private CustomerService customerService;
public CustomerCSVFileProcessor(
CustomerService customerService) {
this.customerService = customerService;
}
public void process(Path file) {
logger.info(String.format(
"Init processing file %s",file.getFileName()));
var parser = CSVParser.parse(file);
parser.getRecords().forEach(this::processRecord);
moveFile(file);
}
private void processRecord(CSVRecord csvRecord) {
if (csvRecord.size() < NUMBER_OF_COLUMNS) {
logger.info(String.format(
"Line %d skipped. Not enough values.",
csvRecord.lineNumber()));
return;
}
Customer customer = customerMapper.mapToCustomer(csvRecord);
customer.setStatus(Customer.Status.ACTIVATED);
customerService.save(customer);
logger.info(String.format(
"Saved customer %s in line %d",
customer.getName(),
csvRecord.lineNumber()));
}
private static void moveFile(Path file) {
try {
var destinationFolder = Path.of(
file.getParent().toString() + OUTPUT_FOLDER );
Files.move(
file,
destinationFolder.resolve(file.getFileName()),
REPLACE_EXISTING);
logger.info(String.format(
"File %s has been moved to %s",file.getFileName(),
destinationFolder));
} catch (IOException e) {
logger.error("Unable to move file "+ file, e);
}
}
}
该方法在 CSVParser 的帮助下解析 csv 文件。然后,每个 cvs 行(描述为 CSVRecord)都映射到客户实体并将其保存到数据库中。最后,foreach 循环完成,文件被移动到一个不同的位置。
测试应用程序
重新运行应用程序将从 CustomerFileWatcherConfig 配置类启动文件系统观察程序 bean。应用程序启动后,包含一些客户的 csv 文件就会复制到受监控的目录中。5 分钟后生成以下log
CustomerFileWatcherConfig : FileSystemWatcher initialized. Monitoring directory C:\workspace\files\customer
CustomerCSVFileProcessor : Init processing file customerFile.csv
CustomerCSVFileProcessor : Saved customer James Hart in line 1
CustomerCSVFileProcessor : Saved customer Will Avery in line 2
CustomerCSVFileProcessor : Saved customer Anne Williams in line 3
CustomerCSVFileProcessor : Saved customer Julia Norton in line 4
CustomerCSVFileProcessor : File customerFile.csv has been moved to C:\workspace\files\customer\output
事实上,文件中的数据行已作为客户实体添加到数据库中,并且文件已成功移动。
总结
在这篇短文中,我们演示了如何配置 Spring 应用程序以监控本地目录。该功能在执行自动化任务时非常有用。例如,与其他服务结合使用,如 FTP 服务器将文件传输到目录,然后文件监视器将其拾取进行处理。最后 如果感觉有帮助的话 点个赞吧!