Java實(shí)現(xiàn)文件變化監(jiān)聽代碼實(shí)例
一、前言
1、簡介
在平時的開發(fā)過程中,會有很多場景需要實(shí)時監(jiān)聽文件的變化,如下:
通過實(shí)時監(jiān)控 mysql 的 binlog 日志實(shí)現(xiàn)數(shù)據(jù)同步
修改配置文件后,希望系統(tǒng)可以實(shí)時感知
應(yīng)用系統(tǒng)將日志寫入文件中,日志監(jiān)控系統(tǒng)可以實(shí)時抓取日志,分析日志內(nèi)容并進(jìn)行報警
類似 ide 工具,可以實(shí)時感知管理的工程下的文件變更
2、三種方法介紹
定時任務(wù) + File#lastModified
WatchService
Apache Commons-IO
二、三種方法實(shí)現(xiàn)
1、定時任務(wù) + File#lastModified
通過定時任務(wù),輪訓(xùn)查詢文件的最后修改時間,與上一次進(jìn)行對比。如果發(fā)生變化,則說明文件已經(jīng)修改,進(jìn)行重新加載或?qū)?yīng)的業(yè)務(wù)邏輯處理
對于文件低頻變動的場景,這種方案實(shí)現(xiàn)簡單,基本上可以滿足需求。但該方案如果用在文件目錄的變化上,缺點(diǎn)就有些明顯了,比如:操作頻繁,效率都損耗在遍歷、保存狀態(tài)、對比狀態(tài)上了,無法充分利用OS的功能。
public class FileWatchDemo { /** * 上次更新時間 */ public static long LAST_TIME = 0L; public static void main(String[] args) throws Exception { // 相對路徑代表這個功能相同的目錄下 String fileName = "static/test.json"; // 創(chuàng)建文件,僅為實(shí)例,實(shí)踐中由其他程序觸發(fā)文件的變更 createFile(fileName); // 循環(huán)執(zhí)行 while (true){ long timestamp = readLastModified(fileName); if (timestamp != LAST_TIME) { System.out.println("文件已被更新:" + timestamp); LAST_TIME = timestamp; // 重新加載,文件內(nèi)容 } else { System.out.println("文件未更新"); } Thread.sleep(1000); } } public static void createFile(String fileName) throws IOException { File file = new File(fileName); if (!file.exists()) { boolean result = file.createNewFile(); System.out.println("創(chuàng)建文件:" + result); } } // 獲取文件最后修改時間 public static long readLastModified(String fileName) { File file = new File(fileName); return file.lastModified(); } }
同時該方案存在Bug:在Java8和9的某些版本下,lastModified方法返回時間戳并不是毫秒,而是秒,也就是說返回結(jié)果的后三位始終為0
2、WatchService
2.1 介紹
在Java 7中新增了java.nio.file.WatchService,通過它可以實(shí)現(xiàn)文件變動的監(jiān)聽。WatchService是基于操作系統(tǒng)的文件系統(tǒng)監(jiān)控器,可以監(jiān)控系統(tǒng)所有文件的變化,無需遍歷、無需比較,是一種基于信號收發(fā)的監(jiān)控,效率高
相對于方案一,實(shí)現(xiàn)起來簡單,效率高。不足的地方也很明顯,只能監(jiān)聽當(dāng)前目錄下的文件和目錄,不能監(jiān)視子目錄。另外對于jdk8之后版本來說,該方案已經(jīng)實(shí)現(xiàn)實(shí)時監(jiān)聽,不存在準(zhǔn)實(shí)時的問題
2.2 簡單示例
public class WatchServiceDemo { public static void main(String[] args) throws IOException { // 這里的監(jiān)聽必須是目錄 Path path = Paths.get("static"); // 創(chuàng)建WatchService,它是對操作系統(tǒng)的文件監(jiān)視器的封裝,相對之前,不需要遍歷文件目錄,效率要高很多 WatchService watcher = FileSystems.getDefault().newWatchService(); // 注冊指定目錄使用的監(jiān)聽器,監(jiān)視目錄下文件的變化; // PS:Path必須是目錄,不能是文件; // StandardWatchEventKinds.ENTRY_MODIFY,表示監(jiān)視文件的修改事件 path.register(watcher, new WatchEvent.Kind[]{StandardWatchEventKinds.ENTRY_MODIFY}, SensitivityWatchEventModifier.LOW); // 創(chuàng)建一個線程,等待目錄下的文件發(fā)生變化 try { while (true) { // 獲取目錄的變化: // take()是一個阻塞方法,會等待監(jiān)視器發(fā)出的信號才返回。 // 還可以使用watcher.poll()方法,非阻塞方法,會立即返回當(dāng)時監(jiān)視器中是否有信號。 // 返回結(jié)果WatchKey,是一個單例對象,與前面的register方法返回的實(shí)例是同一個; WatchKey key = watcher.take(); // 處理文件變化事件: // key.pollEvents()用于獲取文件變化事件,只能獲取一次,不能重復(fù)獲取,類似隊(duì)列的形式。 for (WatchEvent<?> event : key.pollEvents()) { // event.kind():事件類型 if (event.kind() == StandardWatchEventKinds.OVERFLOW) { //事件可能lost or discarded continue; } // 返回觸發(fā)事件的文件或目錄的路徑(相對路徑) Path fileName = (Path) event.context(); System.out.println("文件更新: " + fileName); } // 每次調(diào)用WatchService的take()或poll()方法時需要通過本方法重置 if (!key.reset()) { break; } } } catch (Exception e) { e.printStackTrace(); } } }
2.3 完整示例
創(chuàng)建FileWatchedListener接口
public interface FileWatchedListener { void onCreated(WatchEvent<Path> watchEvent); void onDeleted(WatchEvent<Path> watchEvent); void onModified(WatchEvent<Path> watchEvent); void onOverflowed(WatchEvent<Path> watchEvent); }
創(chuàng)建FileWatchedAdapter 實(shí)現(xiàn)類,實(shí)現(xiàn)文件監(jiān)聽的方法
public class FileWatchedAdapter implements FileWatchedListener { @Override public void onCreated(WatchEvent<Path> watchEvent) { Path fileName = watchEvent.context(); System.out.println(String.format("文件【%s】被創(chuàng)建,時間:%s", fileName, now())); } @Override public void onDeleted(WatchEvent<Path> watchEvent) { Path fileName = watchEvent.context(); System.out.println(String.format("文件【%s】被刪除,時間:%s", fileName, now())); } @Override public void onModified(WatchEvent<Path> watchEvent) { Path fileName = watchEvent.context(); System.out.println(String.format("文件【%s】被修改,時間:%s", fileName, now())); } @Override public void onOverflowed(WatchEvent<Path> watchEvent) { Path fileName = watchEvent.context(); System.out.println(String.format("文件【%s】被丟棄,時間:%s", fileName, now())); } private String now(){ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS"); return dateFormat.format(Calendar.getInstance().getTime()); } }
創(chuàng)建FileWatchedService 監(jiān)聽類,監(jiān)聽文件
public class FileWatchedService { private WatchService watchService; private FileWatchedListener listener; /** * * @param path 要監(jiān)聽的目錄,注意該 Path 只能是目錄,否則會報錯 java.nio.file.NotDirectoryException: * @param listener 自定義的 listener,用來處理監(jiān)聽到的創(chuàng)建、修改、刪除事件 * @throws IOException */ public FileWatchedService(Path path, FileWatchedListener listener) throws IOException { watchService = FileSystems.getDefault().newWatchService(); path.register(watchService, /// 監(jiān)聽文件創(chuàng)建事件 StandardWatchEventKinds.ENTRY_CREATE, /// 監(jiān)聽文件刪除事件 StandardWatchEventKinds.ENTRY_DELETE, /// 監(jiān)聽文件修改事件 StandardWatchEventKinds.ENTRY_MODIFY); this.listener = listener; } private void watch() throws InterruptedException { while (true) { WatchKey watchKey = watchService.take(); List<WatchEvent<?>> watchEventList = watchKey.pollEvents(); for (WatchEvent<?> watchEvent : watchEventList) { WatchEvent.Kind<?> kind = watchEvent.kind(); WatchEvent<Path> curEvent = (WatchEvent<Path>) watchEvent; if (kind == StandardWatchEventKinds.OVERFLOW) { listener.onOverflowed(curEvent); continue; } else if (kind == StandardWatchEventKinds.ENTRY_CREATE) { listener.onCreated(curEvent); continue; } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) { listener.onModified(curEvent); continue; } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) { listener.onDeleted(curEvent); continue; } } /** * WatchKey 有兩個狀態(tài): * {@link sun.nio.fs.AbstractWatchKey.State.READY ready} 就緒狀態(tài):表示可以監(jiān)聽事件 * {@link sun.nio.fs.AbstractWatchKey.State.SIGNALLED signalled} 有信息狀態(tài):表示已經(jīng)監(jiān)聽到事件,不可以接續(xù)監(jiān)聽事件 * 每次處理完事件后,必須調(diào)用 reset 方法重置 watchKey 的狀態(tài)為 ready,否則 watchKey 無法繼續(xù)監(jiān)聽事件 */ if (!watchKey.reset()) { break; } } } public static void main(String[] args) { try { Path path = Paths.get("static"); FileWatchedService fileWatchedService = new FileWatchedService(path, new FileWatchedAdapter()); fileWatchedService.watch(); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } }
3、Apache Commons-IO
3.1 介紹與環(huán)境準(zhǔn)備
commons-io對實(shí)現(xiàn)文件監(jiān)聽的實(shí)現(xiàn)位于org.apache.commons.io.monitor包下,基本使用流程如下:
- 自定義文件監(jiān)聽類并繼承 FileAlterationListenerAdaptor 實(shí)現(xiàn)對文件與目錄的創(chuàng)建、修改、刪除事件的處理;
- 自定義文件監(jiān)控類,通過指定目錄創(chuàng)建一個觀察者 FileAlterationObserver;
- 向監(jiān)視器添加文件系統(tǒng)觀察器,并添加文件監(jiān)聽器;
- 調(diào)用并執(zhí)行。
<!--注意,不同的版本需要不同的JDK支持,2.7需要Java 8及以上版本--> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency>
3.2 原理講解
該方案中監(jiān)聽器本身會啟動一個線程定時處理。在每次運(yùn)行時,都會先調(diào)用事件監(jiān)聽處理類的onStart方法,然后檢查是否有變動,并調(diào)用對應(yīng)事件的方法;比如,onChange文件內(nèi)容改變,檢查完后,再調(diào)用onStop方法,釋放當(dāng)前線程占用的CPU資源,等待下次間隔時間到了被再次喚醒運(yùn)行。
監(jiān)聽器是基于文件目錄為根源的,也可以可以設(shè)置過濾器,來實(shí)現(xiàn)對應(yīng)文件變動的監(jiān)聽。過濾器的設(shè)置可查看FileAlterationObserver的構(gòu)造方法:
public FileAlterationObserver(String directoryName, FileFilter fileFilter, IOCase caseSensitivity) { this(new File(directoryName), fileFilter, caseSensitivity); }
3.3 實(shí)戰(zhàn)演示
創(chuàng)建文件監(jiān)聽器。根據(jù)需要在不同的方法內(nèi)實(shí)現(xiàn)對應(yīng)的業(yè)務(wù)邏輯處理
public class FileListener extends FileAlterationListenerAdaptor { @Override public void onStart(FileAlterationObserver observer) { super.onStart(observer); // System.out.println("一輪輪詢開始,被監(jiān)視路徑:" + observer.getDirectory()); } @Override public void onDirectoryCreate(File directory) { System.out.println("創(chuàng)建文件夾:" + directory.getAbsolutePath()); } @Override public void onDirectoryChange(File directory) { System.out.println("修改文件夾:" + directory.getAbsolutePath()); } @Override public void onDirectoryDelete(File directory) { System.out.println("刪除文件夾:" + directory.getAbsolutePath()); } @Override public void onFileCreate(File file) { String compressedPath = file.getAbsolutePath(); System.out.println("新建文件:" + compressedPath); if (file.canRead()) { // TODO 讀取或重新加載文件內(nèi)容 System.out.println("文件變更,進(jìn)行處理"); } } @Override public void onFileChange(File file) { String compressedPath = file.getAbsolutePath(); System.out.println("修改文件:" + compressedPath); } @Override public void onFileDelete(File file) { System.out.println("刪除文件:" + file.getAbsolutePath()); } @Override public void onStop(FileAlterationObserver observer) { super.onStop(observer); // System.out.println("一輪輪詢結(jié)束,被監(jiān)視路徑:" + fileAlterationObserver.getDirectory()); } }
封裝一個文件監(jiān)控的工具類,核心就是創(chuàng)建一個觀察者FileAlterationObserver,將文件路徑Path和監(jiān)聽器FileAlterationListener進(jìn)行封裝,然后交給FileAlterationMonitor
public class FileMonitor { private FileAlterationMonitor monitor; public FileMonitor(long interval) { monitor = new FileAlterationMonitor(interval); } /** * 給文件添加監(jiān)聽 * * @param path 文件路徑 * @param listener 文件監(jiān)聽器 */ public void monitor(String path, FileAlterationListener listener) { FileAlterationObserver observer = new FileAlterationObserver(new File(path)); monitor.addObserver(observer); observer.addListener(listener); } public void stop() throws Exception { monitor.stop(); } public void start() throws Exception { monitor.start(); } }
調(diào)用執(zhí)行
public class FileRunner { public static void main(String[] args) throws Exception { // 監(jiān)控間隔 FileMonitor fileMonitor = new FileMonitor(10_000L); fileMonitor.monitor("static", new FileListener()); fileMonitor.start(); } }
到此這篇關(guān)于Java實(shí)現(xiàn)文件變化監(jiān)聽代碼實(shí)例的文章就介紹到這了,更多相關(guān)Java實(shí)現(xiàn)文件變化監(jiān)聽內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Log4j定時打印日志及添加模塊名配置的Java代碼實(shí)例
這篇文章主要介紹了Log4j定時打印日志及添加模塊名配置的Java代碼實(shí)例,Log4j是Apache的一個開源Java日志項(xiàng)目,需要的朋友可以參考下2016-01-01Java基于面向?qū)ο髮?shí)現(xiàn)一個戰(zhàn)士小游戲
這篇文章主要為大家詳細(xì)介紹了Java如何基于面向?qū)ο髮?shí)現(xiàn)一個戰(zhàn)士小游戲,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以動手嘗試一下2022-07-07Java代碼如何判斷l(xiāng)inux系統(tǒng)windows系統(tǒng)
這篇文章主要介紹了Java代碼如何判斷l(xiāng)inux系統(tǒng)windows系統(tǒng)問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01MyBatis 動態(tài)SQL和緩存機(jī)制實(shí)例詳解
這篇文章主要介紹了MyBatis 動態(tài)SQL和緩存機(jī)制實(shí)例詳解,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-09-09解決springboot中配置過濾器以及可能出現(xiàn)的問題
這篇文章主要介紹了解決springboot中配置過濾器以及可能出現(xiàn)的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09