AbstractWatchService watchService = new PollingWatchService();
PollingWatchService() {
// TBD: Make the number of threads configurable
scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
}
初始化时会初始化一个单线程池。
接下来,向该watchService
注册目录监听。
//dirSet为目录列表
Map<WatchKey, File> watchKeyFileMap = registerWatchService(dirSet);
private Map<WatchKey, File> registerWatchService(Set<File> dirSet) {
Map<WatchKey, File> watchKeyFileMap = Maps.newHashMap();
try {
for (File dirFile : dirSet) {
Path path = FileSystems.getDefault().getPath(dirFile.getAbsolutePath());
//只监听该目录下的文件修改事件
WatchKey watchKey = watchService.register(path, new WatchEvent.Kind[]{StandardWatchEventKinds.ENTRY_MODIFY}, QConfigSensitivityWatchEventModifier.HIGH);
watchKeyFileMap.put(watchKey, dirFile);
}
} catch (Throwable e) {
LOGGER.error("初始化watchService失败", e);
}
return watchKeyFileMap;
}
接下来就是注册前的前置校验,只处理文件创建、修改、删除事件,并获取监听文件变更的频率,检查watchService
是否已经关闭。
/**
* Register the given file with this watch service
*/
@Override
public WatchKey register(final Path path, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers)
throws IOException {
// check events - CCE will be thrown if there are invalid elements
final Set<WatchEvent.Kind<?>> eventSet = new HashSet<>(events.length);
for (WatchEvent.Kind<?> event : events) {
if (event == null) {
continue;
}
// standard events
if (event == StandardWatchEventKinds.ENTRY_CREATE ||
event == StandardWatchEventKinds.ENTRY_MODIFY ||
event == StandardWatchEventKinds.ENTRY_DELETE) {
eventSet.add(event);
continue;
}
// OVERFLOW is ignored
if (event == StandardWatchEventKinds.OVERFLOW) {
continue;
}
//unsupported
throw new UnsupportedOperationException(event.name());
}
if (eventSet.isEmpty())
throw new IllegalArgumentException("No events to register");
// Extended modifiers may be used to specify the sensitivity level
int sensitivity = QConfigSensitivityWatchEventModifier.LOW.getSensitivityInMs();
if (modifiers.length > 0) {
for (WatchEvent.Modifier modifier : modifiers) {
if (modifier == null)
continue;
if (QConfigSensitivityWatchEventModifier.HIGH == modifier) {
sensitivity = QConfigSensitivityWatchEventModifier.HIGH.getSensitivityInMs();
} else if (QConfigSensitivityWatchEventModifier.MEDIUM == modifier) {
sensitivity = QConfigSensitivityWatchEventModifier.MEDIUM.getSensitivityInMs();
} else if (QConfigSensitivityWatchEventModifier.LOW == modifier) {
sensitivity = QConfigSensitivityWatchEventModifier.LOW.getSensitivityInMs();
} else {
throw new UnsupportedOperationException("Modifier not supported");
}
}
}
// check if watch service is closed
if (!isOpen())
throw new ClosedWatchServiceException();
// registration is done in privileged block as it requires the
// attributes of the entries in the directory.
final int value = sensitivity;
return doRegister(path, eventSet, value);
}
执行注册。
//存储已注册的目录,以及其对应的WatchKey映射关系
private final Map<Object, PollingWatchKey> map = new HashMap<>();
private PollingWatchKey doRegister(Path path,
Set<? extends WatchEvent.Kind<?>> events,
int sensitivityInMs)
throws IOException {
//注册的监听对象必须是目录
if (!path.toFile().isDirectory()) {
throw new NotDirectoryException(path.toString());
}
String fileKey = path.toFile().getAbsolutePath();
if (fileKey == null)
throw new AssertionError("File keys must be supported");
// grab close lock to ensure that watch service cannot be closed
synchronized (closeLock()) {
if (!isOpen())
throw new ClosedWatchServiceException();
PollingWatchKey watchKey;
synchronized (map) {
//新的注册动作被触发,那么已经注册的目录要先取消其监听动作(其实就是取消其watchKey中调度线程池的线程轮询),因为此次的注册动作会更新配置(监听的事件类型、监听频率),而对于之前未注册的目录,新创建watchKey并将目录的绝对路径与watchKey的映射关系保存在`PollingWatchService#map`中。最后调用`PollingWatchService.PollingWatchKey#enable`开启该目录的轮询监听动作(此处才会将配置更新生效)
watchKey = map.get(fileKey);
if (watchKey == null) {
// new registration
watchKey = new PollingWatchKey(path, this, fileKey);
map.put(fileKey, watchKey);
} else {
// update to existing registration
watchKey.disable();
}
}
watchKey.enable(events, sensitivityInMs);
return watchKey;
}
}
注册的监听对象必须是目录。新的注册动作被触发,那么已经注册的目录要先取消其监听动作(其实就是取消其watchKey中调度线程池的线程轮询),因为此次的注册动作会更新配置(监听的事件类型、监听频率),而对于之前未注册的目录,新创建watchKey并将目录的绝对路径与watchKey的映射关系保存在PollingWatchService#map
中。最后调用PollingWatchService.PollingWatchKey#enable
开启该目录的轮询监听动作(此处才会将配置更新生效)
下面,在讨论watchKey
轮询前,先看一下watchKey
是如何构造(初始化)的。
PollingWatchKey(Path dir, PollingWatchService watcher, Object fileKey)
throws IOException {
super(dir, watcher);
this.fileKey = fileKey;
this.valid = true;
this.tickCount = 0;
this.entries = new HashMap<Path, CacheEntry>();
// get the initial entries in the directory
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
for (Path entry : stream) {
// don't follow links
long lastModified = Files.getLastModifiedTime(entry, LinkOption.NOFOLLOW_LINKS).toMillis();
entries.put(entry.getFileName(), new CacheEntry(lastModified, tickCount));
}
} catch (DirectoryIteratorException e) {
throw e.getCause();
}
}
// reference to watcher
private final AbstractWatchService watcher;
// reference to the original directory
private final Path dir;
// key state
private State state;
// pending events
private List<WatchEvent<?>> events;
// maps a context to the last event for the context (iff the last queued
// event for the context is an ENTRY_MODIFY event).
private Map<Object, WatchEvent<?>> lastModifyEvents;
AbstractWatchKey(Path dir, AbstractWatchService watcher) {
this.watcher = watcher;
this.dir = dir;
this.state = State.READY;
this.events = new ArrayList<>();
this.lastModifyEvents = new HashMap<>();
}
首先初始化状态valid
为有效。初始化entries
,保存当前目录下所有文件的Path及其最后修改的时间。而其父类,则会保存当前监听目录的Path、监听服务watchService,标记状态state
为State.READY
。
下面就是轮询监听的逻辑。
/**
* Polls the directory to detect for new files, modified files, or
* deleted files.
*/
synchronized void poll() {
//watchKey不可用则直接返回
if (!valid) {
return;
}
// update tick
tickCount++;
// open directory
DirectoryStream<Path> stream = null;
try {
stream = Files.newDirectoryStream(watchable());
} catch (IOException x) {
// directory is no longer accessible so cancel key
cancel();
signal();
return;
}
// iterate over all entries in directory
try {
//循环当前目录下的所有文件
for (Path entry : stream) {
long lastModified = 0L;
try {
//获取当前文件的最后修改时间
lastModified = Files.getLastModifiedTime(entry, LinkOption.NOFOLLOW_LINKS).toMillis();
} catch (IOException x) {
// unable to get attributes of entry. If file has just
// been deleted then we'll report it as deleted on the
// next poll
continue;
}
//获取当前文件之前缓存的最后修改时间。如果该文件之前不存在,说明这是一个新的文件,则将该文件Path及其最后修改的时间缓存到`entries`,如果注册文件创建事件,则触发文件创建事件通知,否则,如果注册文件修改通知,则触发文件修改事件通知。否则对于已有文件,如果文件发生了修改,并且注册了文件修改事件,则触发文件修改事件通知。对于该目录下的所有文件都会更新`entries`中该文件的最后修改时间、并将计数+1。
// lookup cache
CacheEntry e = entries.get(entry.getFileName());
if (e == null) {
// new file found
entries.put(entry.getFileName(), new CacheEntry(lastModified, tickCount));
// queue ENTRY_CREATE if event enabled
if (events.contains(StandardWatchEventKinds.ENTRY_CREATE)) {
signalEvent(StandardWatchEventKinds.ENTRY_CREATE, entry.getFileName());
continue;
} else {
// if ENTRY_CREATE is not enabled and ENTRY_MODIFY is
// enabled then queue event to avoid missing out on
// modifications to the file immediately after it is
// created.
if (events.contains(StandardWatchEventKinds.ENTRY_MODIFY)) {
signalEvent(StandardWatchEventKinds.ENTRY_MODIFY, entry.getFileName());
}
}
continue;
}
// check if file has changed
if (e.lastModified != lastModified) {
if (events.contains(StandardWatchEventKinds.ENTRY_MODIFY)) {
signalEvent(StandardWatchEventKinds.ENTRY_MODIFY, entry.getFileName());
}
}
// entry in cache so update poll time
e.update(lastModified, tickCount);
}
} catch (DirectoryIteratorException e) {
// ignore for now; if the directory is no longer accessible
// then the key will be cancelled on the next poll
} finally {
// close directory stream
try {
stream.close();
} catch (IOException x) {
// ignore
}
}
//最后遍历`entries`,如果文件计数不等于当前最新的计数,说明该文件被删除了,则将该文件的缓存记录从`entries`中移除,如果注册了文件删除事件,则触发文件删除事件通知。
// iterate over cache to detect entries that have been deleted
Iterator<Map.Entry<Path, CacheEntry>> i = entries.entrySet().iterator();
while (i.hasNext()) {
Map.Entry<Path, CacheEntry> mapEntry = i.next();
CacheEntry entry = mapEntry.getValue();
if (entry.lastTickCount() != tickCount) {
Path name = mapEntry.getKey();
// remove from map and queue delete event (if enabled)
i.remove();
if (events.contains(StandardWatchEventKinds.ENTRY_DELETE)) {
signalEvent(StandardWatchEventKinds.ENTRY_DELETE, name);
}
}
}
}
获取当前文件之前缓存的最后修改时间。如果该文件之前不存在,说明这是一个新的文件,则将该文件Path及其最后修改的时间缓存到entries
,如果注册文件创建事件,则触发文件创建事件通知,否则,如果注册文件修改通知,则触发文件修改事件通知。否则对于已有文件,如果文件发生了修改,并且注册了文件修改事件,则触发文件修改事件通知。对于该目录下的所有文件都会更新entries
中该文件的最后修改时间、并将计数+1。
最后遍历entries
,如果文件计数不等于当前最新的计数,说明该文件被删除了,则将该文件的缓存记录从entries
中移除,如果注册了文件删除事件,则触发文件删除事件通知。
触发事件通知时,会先判断AbstractWatchKey#events
是否存在事件(pending)。(1)如果有的话则会获取上一个事件,如果上一个事件为OVERFLOW
事件,则说明已经堆积的事件超过了最大限制,则不再处理该事件,只是单纯的将上一个OVERFLOW
事件内部的计数+1;(2)类似的,如果该事件与上一个事件是同一文件的同一类型事件,则将两个当成一个事件,并将上一个事件内部的计数+1;然后直接返回。
否则,会检查AbstractWatchKey#lastModifyEvents
(如果该watchKey对应目录下的文件的最后一个通知事件的类型为ENTRY_MODIFY
,则会在lastModifyEvents中存储该事件,否则会删除原来存储的ENTRY_MODIFY
事件),判断当前文件(触发事件通知的文件)的上一次事件通知的事件类型是否是ENTRY_MODIFY
事件,如果是的话,将两个当成一个事件,并将上一个事件内部的计数+1,然后直接返回。否则会删除原来存储的ENTRY_MODIFY
事件。
最后,创建一个事件(包含通知的事件类型和文件名),记录到AbstractWatchKey#events
,如果是ENTRY_MODIFY
事件,会额外的记录到AbstractWatchKey#lastModifyEvents
。特别的,如果AbstractWatchKey#events
中堆积的事件超过了最大限制,则清空AbstractWatchKey#events
和AbstractWatchKey#lastModifyEvents
,并新增一个OVERFLOW
事件到AbstractWatchKey#events
。
最后,调用AbstractWatchKey#signal
方法,如果该watchkey的状态state
为State.SIGNALLED
说明已经将其加入到了AbstractWatchService#PENDING_KEYS
队列中,直接返回。否则,将watchkey的状态state
标记为State.SIGNALLED
,并将其加入到AbstractWatchService#PENDING_KEYS
队列中(调用AbstractWatchService#enqueueKey
方法)。
代码如下:
/**
* Adds the event to this key and signals it.
*/
@SuppressWarnings("unchecked")
final void signalEvent(WatchEvent.Kind<?> kind, Object context) {
boolean isModify = (kind == StandardWatchEventKinds.ENTRY_MODIFY);
synchronized (this) {
int size = events.size();
if (size > 0) {
// if the previous event is an OVERFLOW event or this is a
// repeated event then we simply increment the counter
WatchEvent<?> prev = events.get(size - 1);
if ((prev.kind() == StandardWatchEventKinds.OVERFLOW) ||
((kind == prev.kind() && Objects.equals(context, prev.context())))) {
((Event<?>) prev).increment();
return;
}
// if this is a modify event and the last entry for the context
// is a modify event then we simply increment the count
if (!lastModifyEvents.isEmpty()) {
if (isModify) {
WatchEvent<?> ev = lastModifyEvents.get(context);
if (ev != null) {
assert ev.kind() == StandardWatchEventKinds.ENTRY_MODIFY;
((Event<?>) ev).increment();
return;
}
} else {
// not a modify event so remove from the map as the
// last event will no longer be a modify event.
lastModifyEvents.remove(context);
}
}
// if the list has reached the limit then drop pending events
// and queue an OVERFLOW event
if (size >= MAX_EVENT_LIST_SIZE) {
kind = StandardWatchEventKinds.OVERFLOW;
isModify = false;
context = null;
}
}
// non-repeated event
Event<Object> ev = new Event<>((WatchEvent.Kind<Object>) kind, context);
if (isModify) {
lastModifyEvents.put(context, ev);
} else if (kind == StandardWatchEventKinds.OVERFLOW) {
// drop all pending events
events.clear();
lastModifyEvents.clear();
}
events.add(ev);
signal();
}
}
下面要讲的是如何消费上面的文件变更通知事件。
private void startWatchService(Set<File> dirSet) {
//返回注册的目录与watchKey的映射关系
Map<WatchKey, File> watchKeyFileMap = registerWatchService(dirSet);
Map<String, Long> fileLastModifyTs = Maps.newHashMap();
//服务未停止
while (!isStop.get()) {
try {
//主要执行以下操作:1、检查服务是否停止,停止抛出异常;2、阻塞的方式从`AbstractWatchService#PENDING_KEYS`队列中获取watchKey,检查watchKey是否是`CLOSE_KEY`(标记服务停止),如果是的话会再次将该watchKey放入队列,这很重要,因为需要通过此方式去唤醒其它阻塞在`PENDING_KEYS.take()`上的线程。这些线程最终会检查服务是否停止,停止抛出异常退出。
WatchKey key = watchService.take();
//获取该watchKey下所有的文件变更事件
for (WatchEvent<?> event : key.pollEvents()) {
//监听的目录
File fileDir = watchKeyFileMap.get(key);
//监听目录下的文件
String fileName = event.context().toString();
if (fileDir == null) {
LOGGER.error("该key,未找到与之对应的监控路径");
continue;
}
String filePathStr = fileDir.getAbsolutePath() + File.separator + fileName;
filePathStr = filePathStr.toLowerCase();
long lastModifyTs = 0;
//获取上次存储的文件变更时间戳
if (fileLastModifyTs.containsKey(filePathStr)) {
lastModifyTs = fileLastModifyTs.get(filePathStr);
}
File changedFile = new File(filePathStr);
//如果发生变更的文件是目录,则不处理。
//如果两次文件变更在200ms内,则不处理。
if (changedFile.isDirectory() || changedFile.lastModified() - lastModifyTs < MIN_CHANGE_INTERVAL) {
continue;
}
//保存下当前文件的更新时间戳
fileLastModifyTs.put(filePathStr, changedFile.lastModified());
if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
Set<LocalConfigMeta> localConfigMetas;
synchronized (FILE_CONFIGMETA_MAP) {
localConfigMetas = FILE_CONFIGMETA_MAP.get(filePathStr);
}
updateConfiguration(localConfigMetas);
} else {
LOGGER.info("未知的事件类型,请忽略此条提示,操作类型!" + event.kind().name());
}
}
//
key.reset();
} catch (InterruptedException e) {
LOGGER.error("初始化watchService失败", e);
} catch (ClosedWatchServiceException e) {
LOGGER.error("watch service 已经被关闭", e);
try {
Thread.sleep(EXCEPTION_RETRY_INTERVAL);
} catch (InterruptedException e1) {
LOGGER.error("sleep Interrupted", e1);
}
}
}
}
主要执行以下操作:1、检查服务是否停止,停止抛出异常;2、阻塞的方式从AbstractWatchService#PENDING_KEYS
队列中获取watchKey,检查watchKey是否是CLOSE_KEY
(标记服务停止),如果是的话会再次将该watchKey放入队列,这很重要,因为需要通过此方式去唤醒其它阻塞在PENDING_KEYS.take()
上的线程。这些线程最终会检查服务是否停止,停止抛出异常退出。
获取每一个watchKey下所有的文件变更事件。如果发生变更的文件是目录,则不处理。如果两次文件变更在200ms内,则不处理。下面就是相关的业务处理。
处理完业务后,对于state
为State.SIGNALLED
的watchKey,如果其事件(pending)都已处理,则将state
置为State.READY
,否则,将该watchKey重新入队,以便继续处理剩下的事件(这些事件来自于在该watchKey执行key.pollEvents()
操作之后轮询监听线程将新的文件变更事件追加到了该watchKey的AbstractWatchKey#events
)。
// used when closing watch service
private volatile boolean closed;
private final Object closeLock = new Object();
/**
* Closes this watch service. This method is invoked by the close
* method to perform the actual work of closing the watch service.
*/
abstract void implClose() throws IOException;
@Override
public final void close() throws IOException {
synchronized (closeLock) {
// nothing to do if already closed
//如果已经关闭,则不执行任何操作
if (closed)
return;
//标记状态为关闭
closed = true;
implClose();
// clear pending keys and queue special key to ensure that any
// threads blocked in take/poll wakeup
//目录下发生文件变更,会通过AbstractWatchKey#signal方法将该目录所对应的PollingWatchKey入队,这里是清空该队列
PENDING_KEYS.clear();
//由于会存在其它线程阻塞在WatchKey key = watchService.take();这里将CLOSE_KEY入队,就是为了唤醒这些线程。被唤醒的线程首先通过AbstractWatchService#checkOpen会判断服务状态,发现已关闭,抛出ClosedWatchServiceException异常。
PENDING_KEYS.offer(CLOSE_KEY);
}
}
implClose
方法由子类实现,PollingWatchService
的实现如下:
@Override
void implClose() throws IOException {
//map中存储的是注册监听的目录以及该目录对应的PollingWatchKey
synchronized (map) {
for (Map.Entry<Object, PollingWatchKey> entry : map.entrySet()) {
PollingWatchKey watchKey = entry.getValue();
//取消watchKey对应目录的轮询监听
watchKey.disable();
//watchKey标记为不可用
watchKey.invalidate();
}
//清除注册的信息
map.clear();
}
//关闭轮询的线程池
scheduledExecutor.shutdown();
}
private class PollingWatchKey extends AbstractWatchKey {
// disables periodic polling
void disable() {
synchronized (this) {
if (poller != null)
poller.cancel(false);
}
}
...
void invalidate() {
valid = false;
}