Java實(shí)現(xiàn)監(jiān)聽(tīng)文件變化的三種方案詳解
背景
在研究規(guī)則引擎時(shí),如果規(guī)則以文件的形式存儲(chǔ),那么就需要監(jiān)聽(tīng)指定的目錄或文件來(lái)感知規(guī)則是否變化,進(jìn)而進(jìn)行加載。當(dāng)然,在其他業(yè)務(wù)場(chǎng)景下,比如想實(shí)現(xiàn)配置文件的動(dòng)態(tài)加載、日志文件的監(jiān)聽(tīng)、FTP文件變動(dòng)監(jiān)聽(tīng)等都會(huì)遇到類(lèi)似的場(chǎng)景。
本文給大家提供三種解決方案,并分析其中的利弊,建議收藏,以備不時(shí)之需。
方案一:定時(shí)任務(wù) + File#lastModified
這個(gè)方案是最簡(jiǎn)單,最能直接想到的解決方案。通過(guò)定時(shí)任務(wù),輪訓(xùn)查詢(xún)文件的最后修改時(shí)間,與上一次進(jìn)行對(duì)比。如果發(fā)生變化,則說(shuō)明文件已經(jīng)修改,進(jìn)行重新加載或?qū)?yīng)的業(yè)務(wù)邏輯處理。
在上篇文章《JDK的一個(gè)Bug,監(jiān)聽(tīng)文件變更要小心了》中已經(jīng)編寫(xiě)了具體的實(shí)例,并且也提出了其中的不足。
這里再把實(shí)例代碼貼出來(lái):
public class FileWatchDemo { ? /** * 上次更新時(shí)間 */ public static long LAST_TIME = 0L; ? public static void main(String[] args) throws IOException { ? String fileName = "/Users/zzs/temp/1.txt"; // 創(chuàng)建文件,僅為實(shí)例,實(shí)踐中由其他程序觸發(fā)文件的變更 createFile(fileName); ? // 執(zhí)行2次 for (int i = 0; i < 2; i++) { long timestamp = readLastModified(fileName); if (timestamp != LAST_TIME) { System.out.println("文件已被更新:" + timestamp); LAST_TIME = timestamp; // 重新加載,文件內(nèi)容 } else { System.out.println("文件未更新"); } } } ? 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(); } }
對(duì)于文件低頻變動(dòng)的場(chǎng)景,這種方案實(shí)現(xiàn)簡(jiǎn)單,基本上可以滿足需求。不過(guò)像上篇文章中提到的那樣,需要注意Java 8和Java 9中File#lastModified的Bug問(wèn)題。
但該方案如果用在文件目錄的變化上,缺點(diǎn)就有些明顯了,比如:操作頻繁,效率都損耗在遍歷、保存狀態(tài)、對(duì)比狀態(tài)上了,無(wú)法充分利用OS的功能。
方案二:WatchService
在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)控,效率高。
public class WatchServiceDemo { public static void main(String[] args) throws IOException { // 這里的監(jiān)聽(tīng)必須是目錄 Path path = Paths.get("/Users/zzs/temp/"); // 創(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, StandardWatchEventKinds.ENTRY_MODIFY); ? // 創(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(); } } } 復(fù)制代碼
上述demo展示了WatchService的基本使用方式,注解部分也說(shuō)明了每個(gè)API的具體作用。
通過(guò)WatchService監(jiān)聽(tīng)文件的類(lèi)型也變得更加豐富:
- ENTRY_CREATE 目標(biāo)被創(chuàng)建
- ENTRY_DELETE 目標(biāo)被刪除
- ENTRY_MODIFY 目標(biāo)被修改
- OVERFLOW 一個(gè)特殊的Event,表示Event被放棄或者丟失
如果查看WatchService實(shí)現(xiàn)類(lèi)(PollingWatchService)的源碼,會(huì)發(fā)現(xiàn),本質(zhì)上就是開(kāi)啟了一個(gè)獨(dú)立的線程來(lái)監(jiān)控文件的變化:
PollingWatchService() { ? ? ? // TBD: Make the number of threads configurable ? ? ? scheduledExecutor = Executors ? ? ? ? ? .newSingleThreadScheduledExecutor(new ThreadFactory() { ? ? ? ? ? ? ? ? @Override ? ? ? ? ? ? ? ? public Thread newThread(Runnable r) { ? ? ? ? ? ? ? ? ? ? Thread t = new Thread(null, r, "FileSystemWatcher", 0, false); ? ? ? ? ? ? ? ? ? ? t.setDaemon(true); ? ? ? ? ? ? ? ? ? ? return t; ? ? ? ? ? ? ? ? }}); ? }
也就是說(shuō),本來(lái)需要我們手動(dòng)實(shí)現(xiàn)的部分,也由WatchService內(nèi)部幫我們完成了。
如果你編寫(xiě)一個(gè)demo,進(jìn)行驗(yàn)證時(shí),會(huì)很明顯的感覺(jué)到WatchService監(jiān)控文件的變化并不是實(shí)時(shí)的,有時(shí)候要等幾秒才監(jiān)聽(tīng)到文件的變化。以實(shí)現(xiàn)類(lèi)PollingWatchService為例,查看源碼,可以看到如下代碼:
void enable(Set<? extends Kind<?>> var1, long var2) { ? ? ? ? ? synchronized(this) { ? ? ? ? ? ? ? this.events = var1; ? ? ? ? ? ? ? Runnable var5 = new Runnable() { ? ? ? ? ? ? ? ? ? public void run() { ? ? ? ? ? ? ? ? ? ? ? PollingWatchKey.this.poll(); ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? }; ? ? ? ? ? ? ? this.poller = PollingWatchService.this.scheduledExecutor.scheduleAtFixedRate(var5, var2, var2, TimeUnit.SECONDS); ? ? ? ? ? } ? ? ? }
也就是說(shuō)監(jiān)聽(tīng)器由按照固定時(shí)間間隔的調(diào)度器來(lái)控制的,而這個(gè)時(shí)間間隔在SensitivityWatchEventModifier類(lèi)中定義:
public enum SensitivityWatchEventModifier implements Modifier { ? HIGH(2), ? MEDIUM(10), ? LOW(30); // ... }
該類(lèi)提供了3個(gè)級(jí)別的時(shí)間間隔,分別為2秒、10秒、30秒,默認(rèn)值為10秒。這個(gè)時(shí)間間隔可以在path#register時(shí)進(jìn)行傳遞:
path.register(watcher, new WatchEvent.Kind[]{StandardWatchEventKinds.ENTRY_MODIFY}, SensitivityWatchEventModifier.HIGH);
相對(duì)于方案一,實(shí)現(xiàn)起來(lái)簡(jiǎn)單,效率高。不足的地方也很明顯,只能監(jiān)聽(tīng)當(dāng)前目錄下的文件和目錄,不能監(jiān)視子目錄,而且我們也看到監(jiān)聽(tīng)只能算是準(zhǔn)實(shí)時(shí)的,而且監(jiān)聽(tīng)時(shí)間只能取API默認(rèn)提供的三個(gè)值。
該API在Stack Overflow上也有人提出Java 7在Mac OS下有延遲的問(wèn)題,甚至涉及到Windows和Linux系統(tǒng),筆者沒(méi)有進(jìn)行其他操作系統(tǒng)的驗(yàn)證,如果你遇到類(lèi)似的問(wèn)題,可參考對(duì)應(yīng)的文章,尋求解決方案:http://www.dbjr.com.cn/article/249820.htm。
方案三:Apache Commons-IO
方案一我們自己來(lái)實(shí)現(xiàn),方案二借助于JDK的API來(lái)實(shí)現(xiàn),方案三便是借助于開(kāi)源的框架來(lái)實(shí)現(xiàn),這就是幾乎每個(gè)項(xiàng)目都會(huì)引入的commons-io類(lèi)庫(kù)。
引入相應(yīng)依賴(lài):
<dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.7</version> </dependency>
注意,不同的版本需要不同的JDK支持,2.7需要Java 8及以上版本。
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í)行。
第一步:創(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("onStart"); } ? @Override public void onDirectoryCreate(File directory) { System.out.println("新建:" + 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("onStop"); } }
第二步:封裝一個(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 { FileMonitor fileMonitor = new FileMonitor(1000); fileMonitor.monitor("/Users/zzs/temp/", new FileListener()); fileMonitor.start(); } }
執(zhí)行程序,會(huì)發(fā)現(xiàn)每隔1秒輸入一次日志。當(dāng)文件發(fā)生變更時(shí),也會(huì)打印出對(duì)應(yīng)的日志:
onStart
修改:/Users/zzs/temp/1.txt
onStop
onStart
onStop
當(dāng)然,對(duì)應(yīng)的監(jiān)聽(tīng)時(shí)間間隔,可以通過(guò)在創(chuàng)建FileMonitor時(shí)進(jìn)行修改。
該方案中監(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); }
小結(jié)
至此,基于Java實(shí)現(xiàn)監(jiān)聽(tīng)文件變化的三種方案便介紹完畢。經(jīng)過(guò)上述分析及實(shí)例,大家已經(jīng)看到,并沒(méi)有完美的解決方案,根據(jù)自己的業(yè)務(wù)情況及系統(tǒng)的容忍度可選擇最適合的方案。而且,在此基礎(chǔ)上可以新增一些其他的輔助措施,來(lái)避免具體方案中的不足之處。
到此這篇關(guān)于Java實(shí)現(xiàn)監(jiān)聽(tīng)文件變化的三種方法的文章就介紹到這了,更多相關(guān)Java監(jiān)聽(tīng)文件變化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java多態(tài)中的向上轉(zhuǎn)型與向下轉(zhuǎn)型淺析
多態(tài)是指不同類(lèi)的對(duì)象在調(diào)用同一個(gè)方法是所呈現(xiàn)出的多種不同行為,下面這篇文章主要給大家介紹了關(guān)于Java多態(tài)中向上轉(zhuǎn)型與向下轉(zhuǎn)型的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-02-02SpringCloud turbine監(jiān)控實(shí)現(xiàn)過(guò)程解析
這篇文章主要介紹了SpringCloud turbine監(jiān)控實(shí)現(xiàn)過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12springboot封裝響應(yīng)實(shí)體的實(shí)例代碼
這篇文章主要介紹了springboot封裝響應(yīng)實(shí)體,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07java注釋轉(zhuǎn)json插件開(kāi)發(fā)實(shí)戰(zhàn)詳解
這篇文章主要為大家介紹了java注釋轉(zhuǎn)json插件開(kāi)發(fā)實(shí)戰(zhàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06SpringCloud實(shí)戰(zhàn)之Zuul網(wǎng)關(guān)服務(wù)
服務(wù)網(wǎng)關(guān)是分布式架構(gòu)中不可缺少的組成部分,是外部網(wǎng)絡(luò)和內(nèi)部服務(wù)之間的屏障。這篇文章主要介紹了SpringCloud實(shí)戰(zhàn)之Zuul網(wǎng)關(guān)服務(wù)。一起跟隨小編過(guò)來(lái)看看吧2018-05-05Java8?Stream?collect(Collectors.toMap())的使用
這篇文章主要介紹了Java8?Stream?collect(Collectors.toMap())的使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05解決idea每次新建項(xiàng)目都需要重新指定maven目錄
這篇文章主要介紹了解決idea每次新建項(xiàng)目都需要配置maven,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09