新聞中心
背景
本文給大家提供三種解決方案,并分析其中的利弊,建議收藏,以備不時之需。

專注于為中小企業(yè)提供做網站、網站制作服務,電腦端+手機端+微信端的三站合一,更高效的管理,為中小企業(yè)臨邑免費做網站提供優(yōu)質的服務。我們立足成都,凝聚了一批互聯網行業(yè)人才,有力地推動了上1000家企業(yè)的穩(wěn)健成長,幫助中小企業(yè)通過網站建設實現規(guī)模擴充和轉變。
方案一:定時任務 + File#lastModified
這個方案是最簡單,最能直接想到的解決方案。通過定時任務,輪訓查詢文件的最后修改時間,與上一次進行對比。如果發(fā)生變化,則說明文件已經修改,進行重新加載或對應的業(yè)務邏輯處理。
這里再把實例代碼貼出來:
public class FileWatchDemo {
/**
* 上次更新時間
*/
public static long LAST_TIME = 0L;
public static void main(String[] args) throws IOException {
String fileName = "/Users/zzs/temp/1.txt";
// 創(chuàng)建文件,僅為實例,實踐中由其他程序觸發(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;
// 重新加載,文件內容
} 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();
}
}對于文件低頻變動的場景,這種方案實現簡單,基本上可以滿足需求。不過像上篇文章中提到的那樣,需要注意Java 8和Java 9中File#lastModified的Bug問題。
但該方案如果用在文件目錄的變化上,缺點就有些明顯了,比如:操作頻繁,效率都損耗在遍歷、保存狀態(tài)、對比狀態(tài)上了,無法充分利用OS的功能。
方案二:WatchService
在Java 7中新增了java.nio.file.WatchService,通過它可以實現文件變動的監(jiān)聽。WatchService是基于操作系統的文件系統監(jiān)控器,可以監(jiān)控系統所有文件的變化,無需遍歷、無需比較,是一種基于信號收發(fā)的監(jiān)控,效率高。
public class WatchServiceDemo {
public static void main(String[] args) throws IOException {
// 這里的監(jiān)聽必須是目錄
Path path = Paths.get("/Users/zzs/temp/");
// 創(chuàng)建WatchService,它是對操作系統的文件監(jiān)視器的封裝,相對之前,不需要遍歷文件目錄,效率要高很多
WatchService watcher = FileSystems.getDefault().newWatchService();
// 注冊指定目錄使用的監(jiān)聽器,監(jiān)視目錄下文件的變化;
// PS:Path必須是目錄,不能是文件;
// StandardWatchEventKinds.ENTRY_MODIFY,表示監(jiān)視文件的修改事件
path.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
// 創(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);
}
// 每次調用WatchService的take()或poll()方法時需要通過本方法重置
if (!key.reset()) {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}上述demo展示了WatchService的基本使用方式,注解部分也說明了每個API的具體作用。
通過WatchService監(jiān)聽文件的類型也變得更加豐富:
- ENTRY_CREATE 目標被創(chuàng)建
- ENTRY_DELETE 目標被刪除
- ENTRY_MODIFY 目標被修改
- OVERFLOW 一個特殊的Event,表示Event被放棄或者丟失
如果查看WatchService實現類(PollingWatchService)的源碼,會發(fā)現,本質上就是開啟了一個獨立的線程來監(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;
}});
}也就是說,本來需要我們手動實現的部分,也由WatchService內部幫我們完成了。
如果你編寫一個demo,進行驗證時,會很明顯的感覺到WatchService監(jiān)控文件的變化并不是實時的,有時候要等幾秒才監(jiān)聽到文件的變化。以實現類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);
}
}也就是說監(jiān)聽器由按照固定時間間隔的調度器來控制的,而這個時間間隔在SensitivityWatchEventModifier類中定義:
public enum SensitivityWatchEventModifier implements Modifier {
HIGH(2),
MEDIUM(10),
LOW(30);
// ...
}該類提供了3個級別的時間間隔,分別為2秒、10秒、30秒,默認值為10秒。這個時間間隔可以在path#register時進行傳遞:
path.register(watcher, new WatchEvent.Kind[]{StandardWatchEventKinds.ENTRY_MODIFY},
SensitivityWatchEventModifier.HIGH);相對于方案一,實現起來簡單,效率高。不足的地方也很明顯,只能監(jiān)聽當前目錄下的文件和目錄,不能監(jiān)視子目錄,而且我們也看到監(jiān)聽只能算是準實時的,而且監(jiān)聽時間只能取API默認提供的三個值。
該API在Stack Overflow上也有人提出Java 7在Mac OS下有延遲的問題,甚至涉及到Windows和Linux系統。
方案三:Apache Commons-IO
方案一我們自己來實現,方案二借助于JDK的API來實現,方案三便是借助于開源的框架來實現,這就是幾乎每個項目都會引入的commons-io類庫。
引入相應依賴:
commons-io
commons-io
2.7
注意,不同的版本需要不同的JDK支持,2.7需要Java 8及以上版本。
commons-io對實現文件監(jiān)聽的實現位于org.apache.commons.io.monitor包下,基本使用流程如下:
- 自定義文件監(jiān)聽類并繼承 FileAlterationListenerAdaptor 實現對文件與目錄的創(chuàng)建、修改、刪除事件的處理;
- 自定義文件監(jiān)控類,通過指定目錄創(chuàng)建一個觀察者 FileAlterationObserver;
- 向監(jiān)視器添加文件系統觀察器,并添加文件監(jiān)聽器;
- 調用并執(zhí)行。
第一步:創(chuàng)建文件監(jiān)聽器。根據需要在不同的方法內實現對應的業(yè)務邏輯處理。
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 讀取或重新加載文件內容
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("onStop");
}
}第二步:封裝一個文件監(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();
}
}第三步:調用并執(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í)行程序,會發(fā)現每隔1秒輸入一次日志。當文件發(fā)生變更時,也會打印出對應的日志:
onStart
修改:/Users/zzs/temp/1.txt
onStop
onStart
onStop
當然,對應的監(jiān)聽時間間隔,可以通過在創(chuàng)建FileMonitor時進行修改。
該方案中監(jiān)聽器本身會啟動一個線程定時處理。在每次運行時,都會先調用事件監(jiān)聽處理類的onStart方法,然后檢查是否有變動,并調用對應事件的方法;比如,onChange文件內容改變,檢查完后,再調用onStop方法,釋放當前線程占用的CPU資源,等待下次間隔時間到了被再次喚醒運行。
監(jiān)聽器是基于文件目錄為根源的,也可以可以設置過濾器,來實現對應文件變動的監(jiān)聽。過濾器的設置可查看FileAlterationObserver的構造方法:
public FileAlterationObserver(String directoryName, FileFilter fileFilter, IOCase caseSensitivity) {
this(new File(directoryName), fileFilter, caseSensitivity);
}
小結
至此,基于Java實現監(jiān)聽文件變化的三種方案便介紹完畢。經過上述分析及實例,大家已經看到,并沒有完美的解決方案,根據自己的業(yè)務情況及系統的容忍度可選擇最適合的方案。
新聞名稱:監(jiān)聽日志文件變化的三種方法,推薦第三種!
URL鏈接:http://www.dlmjj.cn/article/djcgoeh.html


咨詢
建站咨詢
