遇到了監聽配置檔案是否被修改的需求,因功能規模小,沒有加入 Apollo、config 等元件,所以得自己實現
1. 自行實現
第一想法是用定時任務去實現,下面是筆者的實現思路:FileModifyManager 來監聽管理全部檔案,要實現監聽介面 FileListener 並傳入給 FileModifyManager ,每當檔案發生變化就呼叫監聽介面的方法 doListener
1.1 FileListener
@FunctionalInterface
public interface FileListener {
void doListener();
}
看了 Hutool 文件才知道這種設計叫鉤子函式,那筆者和 Hutool 作者思路也有相似之處
1.2 FileModifyManager
/**
* @author Howl
* @date 2022/01/15
*/
public class FileModifyManager {
// 存放監聽的檔案及 FileNodeRunnable 節點
private static ConcurrentHashMap<File, FileNodeRunnable> data = new ConcurrentHashMap<>(16);
// 執行緒池執行定時監聽任務
private static ScheduledExecutorService service = Executors.newScheduledThreadPool(20);
// 單例模式--雙重校驗鎖
private volatile static FileModifyManager instance = null;
private FileModifyManager() {
}
public static FileModifyManager getInstance() {
if (instance == null) {
synchronized (FileModifyManager.class) {
if (instance == null) {
instance = new FileModifyManager();
}
}
}
return instance;
}
// 開始監聽,預設 10 秒監聽一次
public FileModifyManager startWatch(File file, FileListener fileListener) throws Exception {
return startWatch(file, fileListener, 0, 1000 * 10, TimeUnit.MILLISECONDS);
}
public FileModifyManager startWatch(File file, FileListener fileListener, long delay, long period, TimeUnit timeUnit) throws Exception {
FileNodeRunnable fileNodeRunnable = addFile(file, fileListener);
ScheduledFuture<?> scheduledFuture = service.scheduleAtFixedRate(fileNodeRunnable, delay, period, timeUnit);
fileNodeRunnable.setFuture(scheduledFuture);
return instance;
}
// 停止監聽
public FileModifyManager stopWatch(File file) {
return stopWatch(file, true);
}
public FileModifyManager stopWatch(File file, boolean mayInterruptIfRunning) {
FileNodeRunnable fileNodeRunnable = data.get(file);
fileNodeRunnable.getFuture().cancel(mayInterruptIfRunning);
removeFile(file);
return instance;
}
// 是否監聽
public boolean isWatching(File file) {
return containsFile(file);
}
// 監聽列表
public Set listWatching() {
return getFileList();
}
// 管理檔案
private FileNodeRunnable addFile(File file, FileListener fileListener) throws Exception {
isFileExists(file);
FileNodeRunnable fileNodeRunnable = new FileNodeRunnable(file, fileListener, file.lastModified());
data.put(file, fileNodeRunnable);
return fileNodeRunnable;
}
private void removeFile(File file) {
data.remove(file);
}
private boolean containsFile(File file) {
return data.containsKey(file);
}
private Set getFileList() {
return data.keySet();
}
// 判斷檔案存在與否
private void isFileExists(File file) throws Exception {
if (!file.exists()) {
throw new Exception("檔案或路徑不存在");
}
}
// 檔案節點及其定時任務
private class FileNodeRunnable implements Runnable {
private File file;
private long lastModifyTime;
private FileListener listener;
private ScheduledFuture future;
FileNodeRunnable(File file, FileListener listener, long lastModifyTime) {
this.file = file;
this.listener = listener;
this.lastModifyTime = lastModifyTime;
}
@Override
public void run() {
if (this.lastModifyTime != file.lastModified()) {
System.out.println(file.toString() + " lastModifyTime is " + this.lastModifyTime);
this.lastModifyTime = file.lastModified();
listener.doListener();
}
}
public ScheduledFuture getFuture() {
return future;
}
public void setFuture(ScheduledFuture future) {
this.future = future;
}
}
}
對外只暴露 startWatch、stopWatch、listWatching 三個方法,入參為 File 和 FileListener
Hutool 也是用了 HashMap 來存放對應的關係表,那筆者思路還是挺清晰的
1.3 使用案例
public class FileTest {
public static void main(String[] args) throws Exception {
File file1 = new File("C:\\Users\\Howl\\Desktop\\123.txt");
File file2 = new File("C:\\Users\\Howl\\Desktop\\1234.txt");
FileModifyManager manager = FileModifyManager.getInstance();
manager.startWatch(file1,() -> System.out.println("123.txt 檔案改變了"))
.startWatch(file2,() -> System.out.println("1234.txr 檔案改變了"));
}
}
2. WatchService
WatchService 是利用本機作業系統的檔案系統來實現監控檔案目錄(監控目錄),於 JDK1.7 引入的位於 NIO 包下的新機制,所以使用方式和 NIO 也很相似
JDK 自帶的 watchService 的缺點是修改檔案會觸發兩次事件,因作業系統有不同情況:
- 修改了檔案的 meta 資訊和日期
- 寫時複製效果,即舊檔案改名,並將內容複製到新建的檔案裡
watchService 只能監控本目錄的內容,不能檢測子目錄裡的內容,如需監控則遍歷新增子目錄
public class WatchServiceTest {
public static void main(String[] args) throws IOException, InterruptedException {
// 目錄路徑,不能輸入檔案否則報錯
Path path = Paths.get("C:\\Users\\Howl\\Desktop");
// 獲取監聽服務
WatchService watchService = FileSystems.getDefault().newWatchService();
// 只註冊修改事件(還有建立和刪除)
path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
// 監聽
while (true) {
// 獲取監聽到的事件 key
WatchKey watchKey = watchService.poll(3 * 1000, TimeUnit.MILLISECONDS);
// poll 的返回有可能為 null
if (watchKey == null) {
continue;
}
// 遍歷這些事件
for (WatchEvent<?> watchEvent : watchKey.pollEvents()) {
if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
Path watchPath = (Path) watchEvent.context();
File watchFile = watchPath.toFile();
System.out.println(watchFile.toString() + "檔案修改了");
}
}
// watchKey 復原,用於下次監聽
watchKey.reset();
}
}
}
3. Hutool(推薦)
Hutool 是國人維護的工具集,使用別人的輪子,總比自己重複造輪子高效(但也要了解輪子的設計思路),hutool 底層還是使用 WatchService ,其解決了修改檔案會觸發兩次事件,思路是在某個毫秒數範圍內的修改視為同一個修改。還可以監控子目錄,思路是遞迴遍歷
3.1 新增依賴
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.19</version>
</dependency>
參考文件 Hutool
3.2 示例
public class HutoolTest {
public static void main(String[] args) throws Exception {
File file = new File("C:\\Users\\Howl\\Desktop\\123.txt");
// 監聽檔案修改
WatchMonitor watchMonitor = WatchMonitor.create(file, WatchMonitor.ENTRY_MODIFY);
// 設定鉤子函式
watchMonitor.setWatcher(new SimpleWatcher() {
@Override
public void onModify(WatchEvent<?> event, Path currentPath) {
System.out.println(((Path) event.context()).toFile().toString() + "修改了");
}
});
// 設定監聽目錄的最大深入,目錄層級大於制定層級的變更將不被監聽,預設只監聽當前層級目錄
watchMonitor.setMaxDepth(1);
// 啟動監聽
watchMonitor.start();
}
}
思路是繼承 Thread 類,然後 run 方法一直迴圈監聽 watchService 事件
4. commons-io
commons-io 是 Apache 提供的實現 I/O 操作的工具集
4.1 新增依賴
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
4.2 示例
稍微看了一下使用的是觀察者模式
public class CommonsTest {
public static void main(String[] args) throws Exception {
// 也是隻能寫目錄
String filePath = "C:\\Users\\Howl\\Desktop";
// 檔案觀察者
FileAlterationObserver observer = new FileAlterationObserver(filePath);
// 新增監聽
observer.addListener(new FileAlterationListenerAdaptor() {
@Override
public void onFileChange(File file) {
System.out.println(file.toString() + "檔案修改了");
}
});
// 監視器
FileAlterationMonitor monitor = new FileAlterationMonitor(10);
// 新增觀察者
monitor.addObserver(observer);
// 啟動執行緒
monitor.start();
}
}