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;
	}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章