PollingWatchService原理剖析

PollingWatchService

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,标记状态stateState.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#eventsAbstractWatchKey#lastModifyEvents,并新增一个OVERFLOW事件到AbstractWatchKey#events

最后,调用AbstractWatchKey#signal方法,如果该watchkey的状态stateState.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内,则不处理。下面就是相关的业务处理。

处理完业务后,对于stateState.SIGNALLED的watchKey,如果其事件(pending)都已处理,则将state置为State.READY,否则,将该watchKey重新入队,以便继续处理剩下的事件(这些事件来自于在该watchKey执行key.pollEvents()操作之后轮询监听线程将新的文件变更事件追加到了该watchKey的AbstractWatchKey#events)。

AbstractWatchService#close

// 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;
	}

你可能感兴趣的:(源码剖析,java)