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

Posted MyBatis

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 文件快照。

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

 
   
   
 
  1. public ChangedFiles getChangedFiles(FolderSnapshot snapshot,

  2.            FileFilter triggerFilter)

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

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

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

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

1.6 FileSystemWatcher 文件系统监控

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

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

 
   
   
 
  1. /**

  2. * Start monitoring the source folder for changes.

  3. */

  4. public void start() {

  5.    synchronized (this.monitor) {

  6.        saveInitialSnapshots();

  7.        if (this.watchThread == null) {

  8.            Map<File, FolderSnapshot> localFolders = new HashMap<>();

  9.            localFolders.putAll(this.folders);

  10.            this.watchThread = new Thread(new Watcher(this.remainingScans,

  11.                    new ArrayList<>(this.listeners), this.triggerFilter,

  12.                    this.pollInterval, this.quietPeriod, localFolders));

  13.            this.watchThread.setName("File Watcher");

  14.            this.watchThread.setDaemon(this.daemon);

  15.            this.watchThread.start();

  16.        }

  17.    }

  18. }

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

1.7 Watcher 监控类

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

 
   
   
 
  1. @Override

  2. public void run() {

  3.    int remainingScans = this.remainingScans.get();

  4.    while (remainingScans > 0 || remainingScans == -1) {

  5.        try {

  6.            if (remainingScans > 0) {

  7.                this.remainingScans.decrementAndGet();

  8.            }

  9.            scan();

  10.        }

  11.        catch (InterruptedException ex) {

  12.            Thread.currentThread().interrupt();

  13.        }

  14.        remainingScans = this.remainingScans.get();

  15.    }

  16. }

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

 
   
   
 
  1. private void scan() throws InterruptedException {

  2.    Thread.sleep(this.pollInterval - this.quietPeriod);

  3.    Map<File, FolderSnapshot> previous;

  4.    Map<File, FolderSnapshot> current = this.folders;

  5.    do {

  6.        previous = current;

  7.        current = getCurrentSnapshots();

  8.        Thread.sleep(this.quietPeriod);

  9.    }

  10.    while (isDifferent(previous, current));

  11.    if (isDifferent(this.folders, current)) {

  12.        updateSnapshots(current.values());

  13.    }

  14. }

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

再看 updateSnapshots 方法:

 
   
   
 
  1. private void updateSnapshots(Collection<FolderSnapshot> snapshots) {

  2.    Map<File, FolderSnapshot> updated = new LinkedHashMap<>();

  3.    Set<ChangedFiles> changeSet = new LinkedHashSet<>();

  4.    for (FolderSnapshot snapshot : snapshots) {

  5.        FolderSnapshot previous = this.folders.get(snapshot.getFolder());

  6.        updated.put(snapshot.getFolder(), snapshot);

  7.        ChangedFiles changedFiles = previous.getChangedFiles(snapshot,

  8.                this.triggerFilter);

  9.        if (!changedFiles.getFiles().isEmpty()) {

  10.            changeSet.add(changedFiles);

  11.        }

  12.    }

  13.    if (!changeSet.isEmpty()) {

  14.        fireListeners(Collections.unmodifiableSet(changeSet));

  15.    }

  16.    this.folders = updated;

  17. }

  18. private void fireListeners(Set<ChangedFiles> changeSet) {

  19.    for (FileChangeListener listener : this.listeners) {

  20.        listener.onChange(changeSet);

  21.    }

  22. }

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

未完待续...

以上是关于Spring Developer Tools 源码分析:一文件目录监控设计的主要内容,如果未能解决你的问题,请参考以下文章

Google Developer Tools

Using The Chrome Developer Tools

Chrome Developer Tools:Network Panel说明

xcodebuild 无法通过 Jenkins 启动“/Applications/Xcode.app/Contents/Developer/Tools/otest”

Enabling Chrome Developer Tools inside Postman

谷歌 Chrome Dev Tools 浅析 – 成为更高效的 Developer