Java實(shí)現(xiàn)文件變化監(jiān)聽代碼實(shí)例
一、前言
1、簡(jiǎn)介
在平時(shí)的開發(fā)過程中,會(huì)有很多場(chǎng)景需要實(shí)時(shí)監(jiān)聽文件的變化,如下:
通過實(shí)時(shí)監(jiān)控 mysql 的 binlog 日志實(shí)現(xiàn)數(shù)據(jù)同步
修改配置文件后,希望系統(tǒng)可以實(shí)時(shí)感知
應(yīng)用系統(tǒng)將日志寫入文件中,日志監(jiān)控系統(tǒng)可以實(shí)時(shí)抓取日志,分析日志內(nèi)容并進(jìn)行報(bào)警
類似 ide 工具,可以實(shí)時(shí)感知管理的工程下的文件變更
2、三種方法介紹
定時(shí)任務(wù) + File#lastModified
WatchService
Apache Commons-IO
二、三種方法實(shí)現(xiàn)
1、定時(shí)任務(wù) + File#lastModified
通過定時(shí)任務(wù),輪訓(xùn)查詢文件的最后修改時(shí)間,與上一次進(jìn)行對(duì)比。如果發(fā)生變化,則說明文件已經(jīng)修改,進(jìn)行重新加載或?qū)?yīng)的業(yè)務(wù)邏輯處理
對(duì)于文件低頻變動(dòng)的場(chǎng)景,這種方案實(shí)現(xiàn)簡(jiǎ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í)間戳并不是毫秒,而是秒,也就是說返回結(jié)果的后三位始終為0
2、WatchService
2.1 介紹
在Java 7中新增了java.nio.file.WatchService,通過它可以實(shí)現(xiàn)文件變動(dòng)的監(jiān)聽。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)聽當(dāng)前目錄下的文件和目錄,不能監(jiān)視子目錄。另外對(duì)于jdk8之后版本來(lái)說,該方案已經(jīng)實(shí)現(xiàn)實(shí)時(shí)監(jiān)聽,不存在準(zhǔn)實(shí)時(shí)的問題
2.2 簡(jiǎn)單示例
public class WatchServiceDemo {
public static void main(String[] args) throws IOException {
// 這里的監(jiān)聽必須是目錄
Path path = Paths.get("static");
// 創(chuàng)建WatchService,它是對(duì)操作系統(tǒng)的文件監(jiān)視器的封裝,相對(duì)之前,不需要遍歷文件目錄,效率要高很多
WatchService watcher = FileSystems.getDefault().newWatchService();
// 注冊(cè)指定目錄使用的監(jiān)聽器,監(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ù)獲取,類似隊(duì)列的形式。
for (WatchEvent<?> event : key.pollEvents()) {
// event.kind():事件類型
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í)需要通過本方法重置
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)建,時(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)聽類,監(jiān)聽文件
public class FileWatchedService {
private WatchService watchService;
private FileWatchedListener listener;
/**
*
* @param path 要監(jiān)聽的目錄,注意該 Path 只能是目錄,否則會(huì)報(bào)錯(cuò) java.nio.file.NotDirectoryException:
* @param listener 自定義的 listener,用來(lái)處理監(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 有兩個(gè)狀態(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 無(wú)法繼續(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對(duì)實(shí)現(xiàn)文件監(jiān)聽的實(shí)現(xiàn)位于org.apache.commons.io.monitor包下,基本使用流程如下:
- 自定義文件監(jiān)聽類并繼承 FileAlterationListenerAdaptor 實(shí)現(xiàn)對(duì)文件與目錄的創(chuàng)建、修改、刪除事件的處理;
- 自定義文件監(jiān)控類,通過指定目錄創(chuàng)建一個(gè)觀察者 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)聽器本身會(huì)啟動(dòng)一個(gè)線程定時(shí)處理。在每次運(yùn)行時(shí),都會(huì)先調(diào)用事件監(jiān)聽處理類的onStart方法,然后檢查是否有變動(dòng),并調(diào)用對(duì)應(yīng)事件的方法;比如,onChange文件內(nèi)容改變,檢查完后,再調(diào)用onStop方法,釋放當(dāng)前線程占用的CPU資源,等待下次間隔時(shí)間到了被再次喚醒運(yùn)行。
監(jiān)聽器是基于文件目錄為根源的,也可以可以設(shè)置過濾器,來(lái)實(shí)現(xiàn)對(duì)應(yīng)文件變動(dò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)對(duì)應(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());
}
}封裝一個(gè)文件監(jiān)控的工具類,核心就是創(chuàng)建一個(gè)觀察者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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Log4j定時(shí)打印日志及添加模塊名配置的Java代碼實(shí)例
這篇文章主要介紹了Log4j定時(shí)打印日志及添加模塊名配置的Java代碼實(shí)例,Log4j是Apache的一個(gè)開源Java日志項(xiàng)目,需要的朋友可以參考下2016-01-01
Java基于面向?qū)ο髮?shí)現(xiàn)一個(gè)戰(zhàn)士小游戲
這篇文章主要為大家詳細(xì)介紹了Java如何基于面向?qū)ο髮?shí)現(xiàn)一個(gè)戰(zhàn)士小游戲,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以動(dòng)手嘗試一下2022-07-07
Java代碼如何判斷l(xiāng)inux系統(tǒng)windows系統(tǒng)
這篇文章主要介紹了Java代碼如何判斷l(xiāng)inux系統(tǒng)windows系統(tǒng)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01
MyBatis 動(dòng)態(tài)SQL和緩存機(jī)制實(shí)例詳解
這篇文章主要介紹了MyBatis 動(dòng)態(tài)SQL和緩存機(jī)制實(shí)例詳解,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-09-09
解決springboot中配置過濾器以及可能出現(xiàn)的問題
這篇文章主要介紹了解決springboot中配置過濾器以及可能出現(xiàn)的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧2020-09-09
Java編寫網(wǎng)上超市購(gòu)物結(jié)算功能程序
這篇文章主要為大家詳細(xì)介紹了Java編寫網(wǎng)上超市購(gòu)物結(jié)算功能程序的具體代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-06-06

