Java實現(xiàn)文件變化監(jiān)聽代碼實例
一、前言
1、簡介
在平時的開發(fā)過程中,會有很多場景需要實時監(jiān)聽文件的變化,如下:
通過實時監(jiān)控 mysql 的 binlog 日志實現(xiàn)數(shù)據(jù)同步
修改配置文件后,希望系統(tǒng)可以實時感知
應用系統(tǒng)將日志寫入文件中,日志監(jiān)控系統(tǒng)可以實時抓取日志,分析日志內(nèi)容并進行報警
類似 ide 工具,可以實時感知管理的工程下的文件變更
2、三種方法介紹
定時任務 + File#lastModified
WatchService
Apache Commons-IO
二、三種方法實現(xiàn)
1、定時任務 + File#lastModified
通過定時任務,輪訓查詢文件的最后修改時間,與上一次進行對比。如果發(fā)生變化,則說明文件已經(jīng)修改,進行重新加載或?qū)臉I(yè)務邏輯處理
對于文件低頻變動的場景,這種方案實現(xià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)建文件,僅為實例,實踐中由其他程序觸發(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方法返回時間戳并不是毫秒,而是秒,也就是說返回結果的后三位始終為0
2、WatchService
2.1 介紹
在Java 7中新增了java.nio.file.WatchService,通過它可以實現(xiàn)文件變動的監(jiān)聽。WatchService是基于操作系統(tǒng)的文件系統(tǒng)監(jiān)控器,可以監(jiān)控系統(tǒng)所有文件的變化,無需遍歷、無需比較,是一種基于信號收發(fā)的監(jiān)控,效率高
相對于方案一,實現(xiàn)起來簡單,效率高。不足的地方也很明顯,只能監(jiān)聽當前目錄下的文件和目錄,不能監(jiān)視子目錄。另外對于jdk8之后版本來說,該方案已經(jīng)實現(xiàn)實時監(jiān)聽,不存在準實時的問題
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()方法,非阻塞方法,會立即返回當時監(jiān)視器中是否有信號。
// 返回結果WatchKey,是一個單例對象,與前面的register方法返回的實例是同一個;
WatchKey key = watcher.take();
// 處理文件變化事件:
// key.pollEvents()用于獲取文件變化事件,只能獲取一次,不能重復獲取,類似隊列的形式。
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 實現(xiàn)類,實現(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)境準備
commons-io對實現(xiàn)文件監(jiān)聽的實現(xiàn)位于org.apache.commons.io.monitor包下,基本使用流程如下:
- 自定義文件監(jiān)聽類并繼承 FileAlterationListenerAdaptor 實現(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)聽器本身會啟動一個線程定時處理。在每次運行時,都會先調(diào)用事件監(jiān)聽處理類的onStart方法,然后檢查是否有變動,并調(diào)用對應事件的方法;比如,onChange文件內(nèi)容改變,檢查完后,再調(diào)用onStop方法,釋放當前線程占用的CPU資源,等待下次間隔時間到了被再次喚醒運行。
監(jiān)聽器是基于文件目錄為根源的,也可以可以設置過濾器,來實現(xiàn)對應文件變動的監(jiān)聽。過濾器的設置可查看FileAlterationObserver的構造方法:
public FileAlterationObserver(String directoryName, FileFilter fileFilter, IOCase caseSensitivity) {
this(new File(directoryName), fileFilter, caseSensitivity);
}3.3 實戰(zhàn)演示
創(chuàng)建文件監(jiān)聽器。根據(jù)需要在不同的方法內(nèi)實現(xiàn)對應的業(yè)務邏輯處理
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("文件變更,進行處理");
}
}
@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ān)視路徑:" + fileAlterationObserver.getDirectory());
}
}封裝一個文件監(jiān)控的工具類,核心就是創(chuàng)建一個觀察者FileAlterationObserver,將文件路徑Path和監(jiān)聽器FileAlterationListener進行封裝,然后交給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();
}
}到此這篇關于Java實現(xiàn)文件變化監(jiān)聽代碼實例的文章就介紹到這了,更多相關Java實現(xiàn)文件變化監(jiān)聽內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java基于面向?qū)ο髮崿F(xiàn)一個戰(zhàn)士小游戲
這篇文章主要為大家詳細介紹了Java如何基于面向?qū)ο髮崿F(xiàn)一個戰(zhàn)士小游戲,文中的示例代碼講解詳細,感興趣的小伙伴可以動手嘗試一下2022-07-07
Java代碼如何判斷l(xiāng)inux系統(tǒng)windows系統(tǒng)
這篇文章主要介紹了Java代碼如何判斷l(xiāng)inux系統(tǒng)windows系統(tǒng)問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01
解決springboot中配置過濾器以及可能出現(xiàn)的問題
這篇文章主要介紹了解決springboot中配置過濾器以及可能出現(xiàn)的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09

