Spring Developer Tools 源码分析
Spring DevTools 介绍
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。方法逻辑比较简单,首先比较的两个快照必须是相同的目录 (File
类 equals
时,只要是相同的路径就相等),当前的实例(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
对类路径进行监控。