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