在日常的文件处理和系统管理中,实时监听文件和目录的变化是一项非常实用的功能。今天,我们将深入剖析一个基于 Java 的文件和目录监听工具,它能够精准地监听指定目录下特定扩展名文件的各种变化,并在文件内容有追加时输出新增内容。
文章目录
一、包结构与核心类概览
该 Java 工具的代码结构清晰,主要类都位于cn.study
包下,共同实现了强大的文件和目录监听功能。
(一)FileOrDirectoryListener
类:程序的入口引导者
FileOrDirectoryListener
类包含了main
方法,是整个程序的入口点。它首先检查命令行参数的正确性,确保用户输入了有效的路径。然后,它创建ExtensionValidator
实例,并根据默认或用户自定义的规则添加有效的扩展名。接下来,根据输入路径的类型(文件或目录),它会智能地选择启动相应的监听操作。如果是目录,它将创建DirectoryListener
实例并开启全面的目录监听之旅;如果是文件且扩展名合法,它则会创建FileListener
实例,开始对单个文件进行细致的监听,并在发现初始新增内容时及时输出,为用户呈现文件的动态变化。
(二)FileChangeListener
接口:文件变化的回调契约
FileChangeListener
接口定义了文件内容发生变化时的统一回调方法,它为不同类型的文件监听提供了一个标准化的接口。就像一份契约,所有实现该接口的类都必须遵守其中的规则,当文件内容有变动时,能够按照约定的方式进行响应。这种基于接口的设计模式使得文件变化的处理更加灵活和可扩展,为整个监听工具的功能拓展奠定了坚实的基础。
(三)FileListener
类:单个文件的贴心守护者
FileListener
类专注于单个文件的监听细节。在文件被打开时,它通过FileChannel
精准定位到文件末尾,准备随时捕捉新追加的内容。一旦文件有修改动作,它利用ByteBuffer
和FileChannel
以高效、细粒度的方式迅速读取新增内容,并将其按行(在假设文件为纯文本且以换行符分割行的情况下,可根据实际需求调整)整理成列表。同时,它还精心管理着文件监听的位置信息,确保不会遗漏任何新增内容。当不再需要监听该文件时,FileListener
会及时关闭FileChannel
,释放系统资源,就像一个贴心的管家,在完成任务后将一切收拾得井井有条。
(四)DirectoryListener
类:目录变化的守望者
这个类如同一个敏锐的守护者,负责对指定目录进行全方位的监听。它通过递归遍历目录下的所有文件,不放过任何一个角落。在遍历过程中,它会根据设定的扩展名规则,筛选出需要监听的文件,并为它们注册相应的变化事件监听器。同时,DirectoryListener
还巧妙地管理着每个文件的监听任务,确保在文件发生新增、修改或删除等事件时,能够及时做出准确的响应。无论是一个庞大复杂的项目目录,还是一个简单的文档文件夹,它都能稳稳地掌控其中文件的动态。
(五)ExtensionValidator
类:扩展名的把关者
ExtensionValidator
类就像是一个严格的守门员,它的主要职责是验证文件的扩展名是否符合要求。开发人员可以灵活地使用addExtension
方法,向其告知有效的扩展名,无论是单个扩展名还是一组扩展名,它都能妥善处理。而isValidExtension
方法则像一把精准的标尺,能够快速准确地判断给定路径的文件扩展名是否在合法范围内。在整个监听过程中,它为文件筛选提供了重要的依据,确保只有符合扩展名规则的文件才会进入后续的监听流程。
二、运行结果与代码源码
(一)代码运行结果
- 启动代码
- 追加文件内容
$ echo "abc\n123\n*&(\n" >> abc.md
- 监控显示
(二)源代码
FileOrDirectoryListener
类
package cn.study;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Created on 2024/12/8 19:21
*
* @author QingLian
* @version 0.0.1
*/
public class FileOrDirectoryListener {
private final static Set<String> EXTENSIONS = Set.of(".txt", ".md");
public static void main(String[] args) throws IOException, InterruptedException {
if (args.length != 1) {
System.out.println("请提供一个文件或目录路径作为参数。");
return;
}
ExtensionValidator extensionValidator = new ExtensionValidator();
extensionValidator.addExtension(EXTENSIONS);
Path path = Paths.get(args[0]);
if (Files.isDirectory(path)) {
DirectoryListener directoryListener = new DirectoryListener((filePath, newLines) -> {
for (String line : newLines) {
System.out.println("文件变化 " + filePath + ",新增内容: " + line);
}
}, extensionValidator);
directoryListener.listen(path);
} else if (Files.isRegularFile(path) && extensionValidator.isValidExtension(path.toString())) {
FileListener fileListener = new FileListener(path);
List<String> newLines = new ArrayList<>();
fileListener.onChange(path, newLines);
if (!newLines.isEmpty()) {
for (String line : newLines) {
System.out.println("文件变化 " + path + ",新增内容: " + line);
}
}
} else {
System.out.println("不支持的文件或目录类型。");
}
}
}
- 参数检查与扩展名配置
main
方法首先对命令行参数进行严格检查,确保用户输入了正确数量和格式的参数。这一步骤就像在程序启动前进行的一次全面检查,防止因错误的输入导致程序异常。接着,它创建ExtensionValidator
实例,并根据默认或用户自定义的规则添加有效的扩展名。默认情况下,它包含了常见的.txt
和.md
扩展名,同时也为用户提供了自定义扩展名的灵活性,满足了不同场景下对文件类型的监听需求。
- 文件 / 目录类型判断与监听启动
- 根据用户输入的路径,
FileOrDirectoryListener
类能够智能判断其是文件还是目录。如果是目录,它会创建DirectoryListener
实例,并传入FileChangeListener
和ExtensionValidator
实例,启动全面的目录监听操作。如果是文件,它会进一步检查文件的扩展名是否有效。如果有效,就创建FileListener
实例,开始监听文件内容变化,并在发现初始新增内容时输出给用户。这种根据路径类型自动选择合适监听方式的设计,使得程序具有高度的通用性和智能性,无论是处理单个文件还是整个目录,都能轻松应对。
- 根据用户输入的路径,
FileChangeListener
类
package cn.study;
import java.nio.file.Path;
import java.util.List;
/**
* Created on 2024/12/8 18:54
*
* @author QingLian
* @version 0.0.1
*/
public interface FileChangeListener {
void onChange(Path path, List<String> newLines);
}
FileListener
类
package cn.study;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.WatchKey;
import java.util.Arrays;
import java.util.List;
/**
* Created on 2024/12/8 18:54
*
* @author QingLian
* @version 0.0.1
*/
public class FileListener implements FileChangeListener {
private long lastPosition = 0;
private WatchKey watchKey;
private FileChannel fileChannel;
public FileListener(Path filePath) {
try {
fileChannel = FileChannel.open(filePath, StandardOpenOption.READ);
// 初始化 lastPosition 为文件大小,以便从文件末尾开始监听
lastPosition = fileChannel.size();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onChange(Path path, List<String> newLines) {
try {
// 可以根据实际情况调整缓冲区大小
ByteBuffer buffer = ByteBuffer.allocate(1024);
long currentPosition = fileChannel.size();
if (currentPosition > lastPosition) {
fileChannel.position(lastPosition);
StringBuilder newContent = new StringBuilder();
while (fileChannel.read(buffer) > 0) {
buffer.flip();
newContent.append(new String(buffer.array(), 0, buffer.limit()));
buffer.clear();
}
// 根据实际的文件内容格式,这里假设文件内容为纯文本,以换行符分割行
String[] lines = newContent.toString().split("\n");
newLines.addAll(Arrays.asList(lines));
lastPosition = currentPosition;
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void stopListening() {
if (watchKey != null) {
watchKey.cancel();
}
try {
if (fileChannel != null) {
fileChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public long getLastPosition() {
return lastPosition;
}
public FileListener setLastPosition(long lastPosition) {
this.lastPosition = lastPosition;
return this;
}
public WatchKey getWatchKey() {
return watchKey;
}
public FileListener setWatchKey(WatchKey watchKey) {
this.watchKey = watchKey;
return this;
}
}
- 文件监听初始化
- 在
FileListener
类的构造函数中,开启了对单个文件监听的准备工作。通过FileChannel
打开文件,获取文件的初始大小,并将其记录为监听的起始位置(lastPosition
)。这一步骤就像是在文件末尾设置了一个书签,标记了从哪里开始捕捉新内容。这个初始位置的准确记录,为后续精确监听文件新增内容奠定了基础,确保不会错过任何新添加的信息。
- 在
- 文件内容变化处理
- 当文件内容发生变化时,
onChange
方法迅速启动。它使用ByteBuffer
和FileChannel
以高效的方式读取新增内容。通过循环读取文件通道中的数据,将其转换为字符串,并按行(基于假设的文本文件格式)进行处理。每一行新增内容都被精心添加到newLines
列表中,同时lastPosition
也会实时更新为当前文件的大小,为下一次监听做好准备。这种精细的内容处理方式,能够及时、准确地捕捉到文件的细微变化,为用户提供最新的文件动态。
- 当文件内容发生变化时,
- 资源释放与监听控制
stopListening
方法在文件监听任务结束时发挥重要作用。它负责关闭FileChannel
,释放与之相关的系统资源,避免资源泄漏。同时,FileListener
类还提供了获取和设置lastPosition
以及WatchKey
的方法,这些方法为外部对文件监听状态的控制和查询提供了便利。开发人员可以根据需要灵活调整监听位置或获取监听的关键信息,实现对文件监听过程的精准控制。
DirectoryListener
类
package cn.study;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
/**
* Created on 2024/12/8 18:57
*
* @author QingLian
* @version 0.0.1
*/
public class DirectoryListener {
private final List<Path> filePaths = new ArrayList<>();
private final FileChangeListener fileChangeListener;
private final ExtensionValidator extensionValidator;
private final Map<Path, FileListener> fileListenerMap = new HashMap<>();
public DirectoryListener(FileChangeListener fileChangeListener, ExtensionValidator extensionValidator) {
this.fileChangeListener = fileChangeListener;
this.extensionValidator = extensionValidator;
}
public void listen(Path dirPath) throws IOException {
Files.walkFileTree(dirPath, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (isValidExtension(file)) {
filePaths.add(file);
System.out.println("初始化监听文件 "+ file);
startListeningToFile(file);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
// 为目录注册监听,用于检测新增和删除文件
WatchService watchService = FileSystems.getDefault().newWatchService();
dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
new Thread(() -> {
while (true) {
try {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
Path newFilePath = dir.resolve((Path) event.context());
if (isValidExtension(newFilePath)) {
filePaths.add(newFilePath);
System.out.println("新增文件 "+ newFilePath);
startListeningToFile(newFilePath);
}
} else if (event.kind() == StandardWatchEventKinds.ENTRY_DELETE) {
Path deletedFilePath = dir.resolve((Path) event.context());
if (fileListenerMap.containsKey(deletedFilePath)) {
FileListener fileListener = fileListenerMap.get(deletedFilePath);
fileListener.stopListening();
fileListenerMap.remove(deletedFilePath);
filePaths.remove(deletedFilePath);
System.out.println("删除文件 "+ deletedFilePath);
}
}
}
key.reset();
} catch (InterruptedException | IOException e) {
e.printStackTrace();
}
}
}).start();
return FileVisitResult.CONTINUE;
}
});
}
private boolean isValidExtension(Path path) {
return extensionValidator.isValidExtension(path.toString());
}
private void startListeningToFile(Path filePath) throws IOException {
FileListener fileListener = new FileListener(filePath);
WatchService watchService = FileSystems.getDefault().newWatchService();
Path dir = filePath.getParent();
WatchKey watchKey = dir.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
fileListener.setWatchKey(watchKey);
fileListenerMap.put(filePath, fileListener);
new Thread(() -> {
while (true) {
try {
WatchKey key = watchService.take();
List<String> newLines = new ArrayList<>();
for (WatchEvent<?> event : key.pollEvents()) {
if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY && event.context().toString().equals(filePath.getFileName().toString())) {
fileListener.onChange(filePath, newLines);
if (!newLines.isEmpty()) {
fileChangeListener.onChange(filePath, newLines);
}
}
}
key.reset();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
- 目录遍历与文件筛选
listen
方法是DirectoryListener
类的核心功能之一。它利用Files.walkFileTree
方法对指定目录进行深度优先的递归遍历。在遍历过程中,对于每个文件,它首先获取文件的扩展名,然后借助ExtensionValidator
类的isValidExtension
方法判断扩展名是否有效。如果扩展名符合要求,就会调用startListeningToFile
方法为该文件启动监听任务。这种全面而细致的目录遍历和文件筛选机制,确保了目录下所有符合条件的文件都能被纳入监听范围,无论是隐藏在深层子目录中的文件,还是直接位于根目录下的文件,都逃不过它的 “法眼”。
- 文件监听任务启动与管理
startListeningToFile
方法负责为单个文件创建FileListener
实例,并将其与文件修改事件紧密绑定。通过WatchService
注册文件修改事件监听器,当文件发生修改时,FileListener
能够及时响应。该方法还将文件监听任务提交到一个独立的新线程中执行,这种异步处理方式避免了因文件监听操作而阻塞主线程,确保了整个应用程序的流畅运行。在新线程中,FileListener
不断监听文件修改事件,一旦发现文件内容有新增且不为空,就会通过FileChangeListener
接口的回调方法,将新增内容及时通知给相关的处理模块,实现了文件变化信息的高效传递。
- 目录变化事件处理
- 在目录监听线程中,
DirectoryListener
时刻关注着目录内的动态。当检测到文件新增事件时,它会迅速获取新增文件的路径,判断其扩展名是否有效。如果有效,就立即为该新增文件启动监听任务,使其能够及时被纳入监控范围。相反,当检测到文件删除事件时,DirectoryListener
能够准确找到对应的FileListener
实例,停止其监听操作,并从文件监听器映射和文件路径列表中移除相关记录。这种对目录内文件新增和删除事件的精准处理,保证了文件监听列表的实时更新,使整个监听过程始终保持准确和高效。
- 在目录监听线程中,
ExtensionValidator
类
package cn.study;
import java.util.HashSet;
import java.util.Set;
/**
* Created on 2024/12/8 19:31
*
* @author QingLian
* @version 0.0.1
*/
public class ExtensionValidator {
private final Set<String> validExtensions;
public ExtensionValidator() {
this.validExtensions = new HashSet<>();
}
public void addExtension(String extension) {
validExtensions.add(extension);
}
public void addExtension(Set<String> extensions){
if (extensions == null){
return;
}
validExtensions.addAll(extensions);
}
public boolean isValidExtension(String path) {
for (String extension : validExtensions) {
if (path.endsWith(extension)) {
return true;
}
}
return false;
}
}
四、程序执行流程全解析
- 程序从
FileOrDirectoryListener
类的main
方法开始执行。首先,对命令行参数进行严格检查,确保输入的路径信息准确无误。这一步骤是整个程序正常运行的基础,避免了因错误参数导致的后续错误。 - 创建
ExtensionValidator
实例,并根据默认或用户自定义的规则添加有效的扩展名。这个过程为文件筛选提供了重要的依据,决定了哪些文件将进入后续的监听流程。 - 根据输入路径判断其类型。如果是目录,
DirectoryListener
开始发挥作用。它通过递归遍历目录下的所有文件,对每个文件的扩展名进行验证。符合扩展名规则的文件将被标记为监听对象,为其创建FileListener
实例,并注册文件修改事件监听器。同时,为目录注册监听线程,用于实时监测文件的新增和删除事件。在这个过程中,DirectoryListener
就像一个高效的调度中心,有条不紊地管理着目录内所有文件的监听任务。 - 如果输入路径是文件且扩展名有效,
FileListener
开始工作。它在构造函数中初始化文件监听的相关参数,定位到文件末尾,准备捕捉新内容。然后,进入监听循环,等待文件内容发生变化。一旦文件有修改,FileListener
迅速读取新增内容,更新监听位置,并通过FileChangeListener
回调通知文件内容的变化。在main
方法中,接收到回调通知后,输出新增内容,让用户及时了解文件的动态。 - 在整个监听过程中,无论是文件内容追加、新增还是删除事件,各个类之间密切协作。
ExtensionValidator
确保文件符合扩展名规则,FileListener
专注于单个文件内容变化的捕捉和处理,DirectoryListener
则全面管理目录内文件的监听任务,FileOrDirectoryListener
作为程序入口,协调各方资源,实现了整个文件和目录监听流程的顺畅运行。
五、代码特点
- 面向对象设计理念贯穿始终
- 整个代码严格遵循面向对象编程原则,将不同的功能模块完美封装在各自独立的类中,每个类都高度聚焦于单一职责,形成了一个清晰、灵活且易于维护的架构。这种设计模式使得各个类之间的职责划分明确,耦合度低,为代码的扩展和优化提供了极大的便利。例如,
DirectoryListener
类专注于目录的监听管理,FileListener
类专注于单个文件的监听操作,ExtensionValidator
类负责扩展名验证,它们之间通过合理的接口和方法调用相互协作,就像一个精密的机器中各个相互配合的零件,共同实现了强大的文件和目录监听功能。
- 整个代码严格遵循面向对象编程原则,将不同的功能模块完美封装在各自独立的类中,每个类都高度聚焦于单一职责,形成了一个清晰、灵活且易于维护的架构。这种设计模式使得各个类之间的职责划分明确,耦合度低,为代码的扩展和优化提供了极大的便利。例如,
- 功能全面且实用
- 该工具实现了对文件和目录的多种事件监听,包括文件内容追加、文件新增和删除等常见操作。无论是在文件管理系统、实时数据处理还是其他需要关注文件动态变化的场景中,都具有很高的实用价值。例如,在一个文档编辑系统中,可以实时监听文档文件的变化,为用户提供即时的保存提示或自动备份功能;在一个日志管理系统中,可以实时捕捉日志文件的新增内容,进行实时分析和处理。
通过对这个 Java 文件和目录监听工具的深入剖析,可以实现一个简单的文件内容监控,基于这个能力可以完成很多的拓展,等你发现。
加公众号,我们一起学~~~