Spring Developer Tools 源码分析:一、文件目录监控设计

Spring Developer Tools 源码分析

Spring DevTools 介绍
https://blog.csdn.net/isea533/article/details/70495714

Spring Developer Tools,后续简称为 devtools,这个工具不仅好用,而且在这个工具的源码中,也有很多非常有学习价值的设计,本系列会逐个分析 devtools,最后使得阅读本文的读者不仅能了解 devtools 的实现,还能学会 devtools 的一些设计思想。

devtools 功能实现的基础就是检测代码变化,所以先从这个比较独立的部分开始。

一、文件目录监控设计

从 JDK 7 开始,Java 提供了 java.nio.file.WatchService 用于文件监控, Commons IO 也提供了 org.apache.commons.io.monitor.FileAlterationObserver 用于监控目录。

devtools 自己实现了简单文件监控,通过独立线程,在一定的时间间隔内,对监控的目录做一次快照,然后对比前后两次快照,对比文件的信息来判断文件是新增、修改还是删除。

下图是文件监控相关的类的类间关系图:
这里写图片描述

下面按类来介绍。

1.1 Type 枚举,变化状态

在这个枚举类中定义了文件变化的三种状态:ADD,MODIFY,DELETE

1.2 ChangedFile,变化的文件

该类记录了文件所在的资源目录,文件本身,文件的变化状态。

1.3 ChangedFiles,变化文件的集合

该类是 ChangedFile 的一个集合。包含的都是同一个资源目录下变化的文件。

1.4 FileSnapshot 文件快照

记录单个文件的信息,包含是否存在,文件长度,修改时间。

1.5 FolderSnapshot 目录快照

一个资源目录会对应一个目录快照,通过目录创建 FolderSnapshot 时,会递归遍历所有子目录,获取内部的所有文件,所有具体的文件都对应一个 FileSnapshot 文件快照。

该类还提供了下面的方法:

public ChangedFiles getChangedFiles(FolderSnapshot snapshot,
            FileFilter triggerFilter)

通过该方法可以对比两个目录快照,获得差异文件,也就是 1.3 中的 ChangedFiles。方法逻辑比较简单,首先比较的两个快照必须是相同的目录 (Fileequals 时,只要是相同的路径就相等),当前的实例(this)是早的快照,传入的 snapshot 是最新的快照,通过对比,如果前一个没有,而新的有,就是新增的文件。反之就是删除,如果两个快照的各项属性中有不相同的,就是修改

该方法中的 triggerFilter(触发文件) 用于实现当指定的文件变化时,才会重启的功能。

前面这 5 个类都是很简单的类,只有 FolderSnapshot 包含了稍微复杂的逻辑。

现在只要我们能拿到两个不同时间的目录快照,就能对比出变化的文件。

1.6 FileSystemWatcher 文件系统监控

这个类提供了一些可配置的参数,用于控制监控周期,监控次数等等。默认情况下的监控次数(remainingScans)为 -1,也就是不限制次数,会不停的通过轮询监控目录。

这个类中最主要的代码就是 start 方法:

/**
 * Start monitoring the source folder for changes.
 */
public void start() {
    synchronized (this.monitor) {
        saveInitialSnapshots();
        if (this.watchThread == null) {
            Map<File, FolderSnapshot> localFolders = new HashMap<>();
            localFolders.putAll(this.folders);
            this.watchThread = new Thread(new Watcher(this.remainingScans,
                    new ArrayList<>(this.listeners), this.triggerFilter,
                    this.pollInterval, this.quietPeriod, localFolders));
            this.watchThread.setName("File Watcher");
            this.watchThread.setDaemon(this.daemon);
            this.watchThread.start();
        }
    }
}

这里创建了一个 watchThread 去执行 Watcher,任务,具体的内容在下面的 Watcher 类中。

1.7 Watcher 监控类

Watcher 类是整个监控设计的核心,先看主要的 run 方法:

@Override
public void run() {
    int remainingScans = this.remainingScans.get();
    while (remainingScans > 0 || remainingScans == -1) {
        try {
            if (remainingScans > 0) {
                this.remainingScans.decrementAndGet();
            }
            scan();
        }
        catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
        }
        remainingScans = this.remainingScans.get();
    }
}

注意前面创建工作线程时的参数,remainingScans 默认值是 -1,在这里的 while 判断中,-1 的值一直不会变,循环一直会执行。再看具体的 scan 方法:

private void scan() throws InterruptedException {
    Thread.sleep(this.pollInterval - this.quietPeriod);
    Map<File, FolderSnapshot> previous;
    Map<File, FolderSnapshot> current = this.folders;
    do {
        previous = current;
        current = getCurrentSnapshots();
        Thread.sleep(this.quietPeriod);
    }
    while (isDifferent(previous, current));
    if (isDifferent(this.folders, current)) {
        updateSnapshots(current.values());
    }
}

这个方法默认会等待 1000-400 毫秒,然后调用 getCurrentSnapshots(结果是有序的),再等待 400 毫秒,如果目录没有变化(isDifferent == false),就会一直按照 400 毫秒轮询。如果文件发生了变化,就调用 updateSnapshots 方法,这里没有复杂的逻辑,就不细说各个方法了。

再看 updateSnapshots 方法:

private void updateSnapshots(Collection<FolderSnapshot> snapshots) {
    Map<File, FolderSnapshot> updated = new LinkedHashMap<>();
    Set<ChangedFiles> changeSet = new LinkedHashSet<>();
    for (FolderSnapshot snapshot : snapshots) {
        FolderSnapshot previous = this.folders.get(snapshot.getFolder());
        updated.put(snapshot.getFolder(), snapshot);
        ChangedFiles changedFiles = previous.getChangedFiles(snapshot,
                this.triggerFilter);
        if (!changedFiles.getFiles().isEmpty()) {
            changeSet.add(changedFiles);
        }
    }
    if (!changeSet.isEmpty()) {
        fireListeners(Collections.unmodifiableSet(changeSet));
    }
    this.folders = updated;
}

private void fireListeners(Set<ChangedFiles> changeSet) {
    for (FileChangeListener listener : this.listeners) {
        listener.onChange(changeSet);
    }
}

这里通过对比项目目录的两个快照,找出其中的差异文件,得到 Set<ChangedFiles>,然后将差异文件作为参数,调用 onChange 监听方法。此时所有注册的 listener 都会收到文件变化的通知。

后续我们继续看 devtools 如何使用 FileSystemWatcher 对类路径进行监控。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

isea533

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值