每日一个小工具:Java 文件与目录监听神器,解锁高效文件管理新姿势

在日常的文件处理和系统管理中,实时监听文件和目录的变化是一项非常实用的功能。今天,我们将深入剖析一个基于 Java 的文件和目录监听工具,它能够精准地监听指定目录下特定扩展名文件的各种变化,并在文件内容有追加时输出新增内容。

一、包结构与核心类概览

该 Java 工具的代码结构清晰,主要类都位于cn.study包下,共同实现了强大的文件和目录监听功能。

image-20241208202428520
在这里插入图片描述

(一)FileOrDirectoryListener类:程序的入口引导者

FileOrDirectoryListener类包含了main方法,是整个程序的入口点。它首先检查命令行参数的正确性,确保用户输入了有效的路径。然后,它创建ExtensionValidator实例,并根据默认或用户自定义的规则添加有效的扩展名。接下来,根据输入路径的类型(文件或目录),它会智能地选择启动相应的监听操作。如果是目录,它将创建DirectoryListener实例并开启全面的目录监听之旅;如果是文件且扩展名合法,它则会创建FileListener实例,开始对单个文件进行细致的监听,并在发现初始新增内容时及时输出,为用户呈现文件的动态变化。

(二)FileChangeListener接口:文件变化的回调契约

FileChangeListener接口定义了文件内容发生变化时的统一回调方法,它为不同类型的文件监听提供了一个标准化的接口。就像一份契约,所有实现该接口的类都必须遵守其中的规则,当文件内容有变动时,能够按照约定的方式进行响应。这种基于接口的设计模式使得文件变化的处理更加灵活和可扩展,为整个监听工具的功能拓展奠定了坚实的基础。

(三)FileListener类:单个文件的贴心守护者

FileListener类专注于单个文件的监听细节。在文件被打开时,它通过FileChannel精准定位到文件末尾,准备随时捕捉新追加的内容。一旦文件有修改动作,它利用ByteBufferFileChannel以高效、细粒度的方式迅速读取新增内容,并将其按行(在假设文件为纯文本且以换行符分割行的情况下,可根据实际需求调整)整理成列表。同时,它还精心管理着文件监听的位置信息,确保不会遗漏任何新增内容。当不再需要监听该文件时,FileListener会及时关闭FileChannel,释放系统资源,就像一个贴心的管家,在完成任务后将一切收拾得井井有条。

(四)DirectoryListener类:目录变化的守望者

这个类如同一个敏锐的守护者,负责对指定目录进行全方位的监听。它通过递归遍历目录下的所有文件,不放过任何一个角落。在遍历过程中,它会根据设定的扩展名规则,筛选出需要监听的文件,并为它们注册相应的变化事件监听器。同时,DirectoryListener还巧妙地管理着每个文件的监听任务,确保在文件发生新增、修改或删除等事件时,能够及时做出准确的响应。无论是一个庞大复杂的项目目录,还是一个简单的文档文件夹,它都能稳稳地掌控其中文件的动态。

(五)ExtensionValidator类:扩展名的把关者

ExtensionValidator类就像是一个严格的守门员,它的主要职责是验证文件的扩展名是否符合要求。开发人员可以灵活地使用addExtension方法,向其告知有效的扩展名,无论是单个扩展名还是一组扩展名,它都能妥善处理。而isValidExtension方法则像一把精准的标尺,能够快速准确地判断给定路径的文件扩展名是否在合法范围内。在整个监听过程中,它为文件筛选提供了重要的依据,确保只有符合扩展名规则的文件才会进入后续的监听流程。

二、运行结果与代码源码

(一)代码运行结果

  1. 启动代码

image-20241208204354728

  1. 追加文件内容
$ echo "abc\n123\n*&(\n" >> abc.md
  1. 监控显示

image-20241208204505253

(二)源代码

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("不支持的文件或目录类型。");
        }
    }
}
  1. 参数检查与扩展名配置
    • main方法首先对命令行参数进行严格检查,确保用户输入了正确数量和格式的参数。这一步骤就像在程序启动前进行的一次全面检查,防止因错误的输入导致程序异常。接着,它创建ExtensionValidator实例,并根据默认或用户自定义的规则添加有效的扩展名。默认情况下,它包含了常见的.txt.md扩展名,同时也为用户提供了自定义扩展名的灵活性,满足了不同场景下对文件类型的监听需求。
  2. 文件 / 目录类型判断与监听启动
    • 根据用户输入的路径,FileOrDirectoryListener类能够智能判断其是文件还是目录。如果是目录,它会创建DirectoryListener实例,并传入FileChangeListenerExtensionValidator实例,启动全面的目录监听操作。如果是文件,它会进一步检查文件的扩展名是否有效。如果有效,就创建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;
    }
}
  1. 文件监听初始化
    • FileListener类的构造函数中,开启了对单个文件监听的准备工作。通过FileChannel打开文件,获取文件的初始大小,并将其记录为监听的起始位置(lastPosition)。这一步骤就像是在文件末尾设置了一个书签,标记了从哪里开始捕捉新内容。这个初始位置的准确记录,为后续精确监听文件新增内容奠定了基础,确保不会错过任何新添加的信息。
  2. 文件内容变化处理
    • 当文件内容发生变化时,onChange方法迅速启动。它使用ByteBufferFileChannel以高效的方式读取新增内容。通过循环读取文件通道中的数据,将其转换为字符串,并按行(基于假设的文本文件格式)进行处理。每一行新增内容都被精心添加到newLines列表中,同时lastPosition也会实时更新为当前文件的大小,为下一次监听做好准备。这种精细的内容处理方式,能够及时、准确地捕捉到文件的细微变化,为用户提供最新的文件动态。
  3. 资源释放与监听控制
    • 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();
    }
}

  1. 目录遍历与文件筛选
    • listen方法是DirectoryListener类的核心功能之一。它利用Files.walkFileTree方法对指定目录进行深度优先的递归遍历。在遍历过程中,对于每个文件,它首先获取文件的扩展名,然后借助ExtensionValidator类的isValidExtension方法判断扩展名是否有效。如果扩展名符合要求,就会调用startListeningToFile方法为该文件启动监听任务。这种全面而细致的目录遍历和文件筛选机制,确保了目录下所有符合条件的文件都能被纳入监听范围,无论是隐藏在深层子目录中的文件,还是直接位于根目录下的文件,都逃不过它的 “法眼”。
  2. 文件监听任务启动与管理
    • startListeningToFile方法负责为单个文件创建FileListener实例,并将其与文件修改事件紧密绑定。通过WatchService注册文件修改事件监听器,当文件发生修改时,FileListener能够及时响应。该方法还将文件监听任务提交到一个独立的新线程中执行,这种异步处理方式避免了因文件监听操作而阻塞主线程,确保了整个应用程序的流畅运行。在新线程中,FileListener不断监听文件修改事件,一旦发现文件内容有新增且不为空,就会通过FileChangeListener接口的回调方法,将新增内容及时通知给相关的处理模块,实现了文件变化信息的高效传递。
  3. 目录变化事件处理
    • 在目录监听线程中,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;
    }
}

四、程序执行流程全解析

  1. 程序从FileOrDirectoryListener类的main方法开始执行。首先,对命令行参数进行严格检查,确保输入的路径信息准确无误。这一步骤是整个程序正常运行的基础,避免了因错误参数导致的后续错误。
  2. 创建ExtensionValidator实例,并根据默认或用户自定义的规则添加有效的扩展名。这个过程为文件筛选提供了重要的依据,决定了哪些文件将进入后续的监听流程。
  3. 根据输入路径判断其类型。如果是目录,DirectoryListener开始发挥作用。它通过递归遍历目录下的所有文件,对每个文件的扩展名进行验证。符合扩展名规则的文件将被标记为监听对象,为其创建FileListener实例,并注册文件修改事件监听器。同时,为目录注册监听线程,用于实时监测文件的新增和删除事件。在这个过程中,DirectoryListener就像一个高效的调度中心,有条不紊地管理着目录内所有文件的监听任务。
  4. 如果输入路径是文件且扩展名有效,FileListener开始工作。它在构造函数中初始化文件监听的相关参数,定位到文件末尾,准备捕捉新内容。然后,进入监听循环,等待文件内容发生变化。一旦文件有修改,FileListener迅速读取新增内容,更新监听位置,并通过FileChangeListener回调通知文件内容的变化。在main方法中,接收到回调通知后,输出新增内容,让用户及时了解文件的动态。
  5. 在整个监听过程中,无论是文件内容追加、新增还是删除事件,各个类之间密切协作。ExtensionValidator确保文件符合扩展名规则,FileListener专注于单个文件内容变化的捕捉和处理,DirectoryListener则全面管理目录内文件的监听任务,FileOrDirectoryListener作为程序入口,协调各方资源,实现了整个文件和目录监听流程的顺畅运行。

五、代码特点

  1. 面向对象设计理念贯穿始终
    • 整个代码严格遵循面向对象编程原则,将不同的功能模块完美封装在各自独立的类中,每个类都高度聚焦于单一职责,形成了一个清晰、灵活且易于维护的架构。这种设计模式使得各个类之间的职责划分明确,耦合度低,为代码的扩展和优化提供了极大的便利。例如,DirectoryListener类专注于目录的监听管理,FileListener类专注于单个文件的监听操作,ExtensionValidator类负责扩展名验证,它们之间通过合理的接口和方法调用相互协作,就像一个精密的机器中各个相互配合的零件,共同实现了强大的文件和目录监听功能。
  2. 功能全面且实用
    • 该工具实现了对文件和目录的多种事件监听,包括文件内容追加、文件新增和删除等常见操作。无论是在文件管理系统、实时数据处理还是其他需要关注文件动态变化的场景中,都具有很高的实用价值。例如,在一个文档编辑系统中,可以实时监听文档文件的变化,为用户提供即时的保存提示或自动备份功能;在一个日志管理系统中,可以实时捕捉日志文件的新增内容,进行实时分析和处理。

通过对这个 Java 文件和目录监听工具的深入剖析,可以实现一个简单的文件内容监控,基于这个能力可以完成很多的拓展,等你发现。

加公众号,我们一起学~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值