监听文件修改的四种方法

遇到了监听配置文件是否被修改的需求,因功能规模小,没有加入 Apollo、config 等组件,所以得自己实现


1. 自行实现

第一想法是用定时任务去实现,下面是笔者的实现思路:FileModifyManager 来监听管理全部文件,要实现监听接口 FileListener 并传入给 FileModifyManager ,每当文件发生变化就调用监听接口的方法 doListener


1.1 FileListener

@FunctionalInterface
public interface FileListener {
    void doListener();
}

看了 Hutool 文档才知道这种设计叫钩子函数,那笔者和 Hutool 作者思路也有相似之处



1.2 FileModifyManager

/**
 * @author Howl
 * @date 2022/01/15
 */
public class FileModifyManager {

    // 存放监听的文件及 FileNodeRunnable 节点
    private static ConcurrentHashMap<File, FileNodeRunnable> data = new ConcurrentHashMap<>(16);

    // 线程池执行定时监听任务
    private static ScheduledExecutorService service = Executors.newScheduledThreadPool(20);

    // 单例模式--双重校验锁
    private volatile static FileModifyManager instance = null;

    private FileModifyManager() {
    }

    public static FileModifyManager getInstance() {
        if (instance == null) {
            synchronized (FileModifyManager.class) {
                if (instance == null) {
                    instance = new FileModifyManager();
                }
            }
        }
        return instance;
    }

    // 开始监听,默认 10 秒监听一次
    public FileModifyManager startWatch(File file, FileListener fileListener) throws Exception {
        return startWatch(file, fileListener, 0, 1000 * 10, TimeUnit.MILLISECONDS);
    }

    public FileModifyManager startWatch(File file, FileListener fileListener, long delay, long period, TimeUnit timeUnit) throws Exception {
        FileNodeRunnable fileNodeRunnable = addFile(file, fileListener);
        ScheduledFuture<?> scheduledFuture = service.scheduleAtFixedRate(fileNodeRunnable, delay, period, timeUnit);
        fileNodeRunnable.setFuture(scheduledFuture);
        return instance;
    }

    // 停止监听
    public FileModifyManager stopWatch(File file) {
        return stopWatch(file, true);
    }

    public FileModifyManager stopWatch(File file, boolean mayInterruptIfRunning) {
        FileNodeRunnable fileNodeRunnable = data.get(file);
        fileNodeRunnable.getFuture().cancel(mayInterruptIfRunning);
        removeFile(file);
        return instance;
    }

    // 是否监听
    public boolean isWatching(File file) {
        return containsFile(file);
    }

    // 监听列表
    public Set listWatching() {
        return getFileList();
    }


    // 管理文件
    private FileNodeRunnable addFile(File file, FileListener fileListener) throws Exception {
        isFileExists(file);
        FileNodeRunnable fileNodeRunnable = new FileNodeRunnable(file, fileListener, file.lastModified());
        data.put(file, fileNodeRunnable);
        return fileNodeRunnable;
    }

    private void removeFile(File file) {
        data.remove(file);
    }

    private boolean containsFile(File file) {
        return data.containsKey(file);
    }

    private Set getFileList() {
        return data.keySet();
    }

    // 判断文件存在与否
    private void isFileExists(File file) throws Exception {
        if (!file.exists()) {
            throw new Exception("文件或路径不存在");
        }
    }

    // 文件节点及其定时任务
    private class FileNodeRunnable implements Runnable {

        private File file;
        private long lastModifyTime;
        private FileListener listener;
        private ScheduledFuture future;

        FileNodeRunnable(File file, FileListener listener, long lastModifyTime) {
            this.file = file;
            this.listener = listener;
            this.lastModifyTime = lastModifyTime;
        }

        @Override
        public void run() {
            if (this.lastModifyTime != file.lastModified()) {
                System.out.println(file.toString() + " lastModifyTime is " + this.lastModifyTime);
                this.lastModifyTime = file.lastModified();
                listener.doListener();
            }
        }

        public ScheduledFuture getFuture() {
            return future;
        }

        public void setFuture(ScheduledFuture future) {
            this.future = future;
        }
    }
}

对外只暴露 startWatch、stopWatch、listWatching 三个方法,入参为 File 和 FileListener

Hutool 也是用了 HashMap 来存放对应的关系表,那笔者思路还是挺清晰的



1.3 使用案例

public class FileTest {
    public static void main(String[] args) throws Exception {

        File file1 = new File("C:\\Users\\Howl\\Desktop\\123.txt");
        File file2 = new File("C:\\Users\\Howl\\Desktop\\1234.txt");

        FileModifyManager manager = FileModifyManager.getInstance();

        manager.startWatch(file1,() -> System.out.println("123.txt 文件改变了"))
                .startWatch(file2,() -> System.out.println("1234.txr 文件改变了"));
        
    }
}






2. WatchService

WatchService 是利用本机操作系统的文件系统来实现监控文件目录(监控目录),于 JDK1.7 引入的位于 NIO 包下的新机制,所以使用方式和 NIO 也很相似


JDK 自带的 watchService 的缺点是修改文件会触发两次事件,因操作系统有不同情况:

  • 修改了文件的 meta 信息和日期
  • 写时复制效果,即旧文件改名,并将内容复制到新建的文件里

watchService 只能监控本目录的内容,不能检测子目录里的内容,如需监控则遍历添加子目录


public class WatchServiceTest {
    public static void main(String[] args) throws IOException, InterruptedException {

        // 目录路径,不能输入文件否则报错
        Path path = Paths.get("C:\\Users\\Howl\\Desktop");

        // 获取监听服务
        WatchService watchService = FileSystems.getDefault().newWatchService();

        // 只注册修改事件(还有创建和删除)
        path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);

        // 监听
        while (true) {

            // 获取监听到的事件 key
            WatchKey watchKey = watchService.poll(3 * 1000, TimeUnit.MILLISECONDS);

            // poll 的返回有可能为 null
            if (watchKey == null) {
                continue;
            }

            // 遍历这些事件
            for (WatchEvent<?> watchEvent : watchKey.pollEvents()) {
                if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
                    Path watchPath = (Path) watchEvent.context();
                    File watchFile = watchPath.toFile();
                    System.out.println(watchFile.toString() + "文件修改了");
                }
            }

            // watchKey 复原,用于下次监听
            watchKey.reset();
        }
    }
}






3. Hutool(推荐)

Hutool 是国人维护的工具集,使用别人的轮子,总比自己重复造轮子高效(但也要了解轮子的设计思路),hutool 底层还是使用 WatchService ,其解决了修改文件会触发两次事件,思路是在某个毫秒数范围内的修改视为同一个修改。还可以监控子目录,思路是递归遍历


3.1 添加依赖

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.19</version>
</dependency>

参考文档 Hutool



3.2 示例

public class HutoolTest {
    public static void main(String[] args) throws Exception {

        File file = new File("C:\\Users\\Howl\\Desktop\\123.txt");

        // 监听文件修改
        WatchMonitor watchMonitor = WatchMonitor.create(file, WatchMonitor.ENTRY_MODIFY);

        // 设置钩子函数
        watchMonitor.setWatcher(new SimpleWatcher() {
            @Override
            public void onModify(WatchEvent<?> event, Path currentPath) {
                System.out.println(((Path) event.context()).toFile().toString() + "修改了");
            }
        });

        // 设置监听目录的最大深入,目录层级大于制定层级的变更将不被监听,默认只监听当前层级目录
        watchMonitor.setMaxDepth(1);

        // 启动监听
        watchMonitor.start();
    }
}

思路是继承 Thread 类,然后 run 方法一直循环监听 watchService 事件







4. commons-io

commons-io 是 Apache 提供的实现 I/O 操作的工具集


4.1 添加依赖

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>


4.2 示例

稍微看了一下使用的是观察者模式

public class CommonsTest {
    public static void main(String[] args) throws Exception {

        // 也是只能写目录
        String filePath = "C:\\Users\\Howl\\Desktop";

        // 文件观察者
        FileAlterationObserver observer = new FileAlterationObserver(filePath);

        // 添加监听
        observer.addListener(new FileAlterationListenerAdaptor() {
            @Override
            public void onFileChange(File file) {
                System.out.println(file.toString() + "文件修改了");
            }
        });

        // 监视器
        FileAlterationMonitor monitor = new FileAlterationMonitor(10);

        // 添加观察者
        monitor.addObserver(observer);

        // 启动线程
        monitor.start();
    }
}


原文地址:https://www.cnblogs.com/Howlet/p/15808845.html