Java可以如何實(shí)現(xiàn)文件變動(dòng)的監(jiān)聽的示例
應(yīng)用中使用logback作為日志輸出組件的話,大部分會(huì)去配置 `logback.xml` 這個(gè)文件,而且生產(chǎn)環(huán)境下,直接去修改logback.xml文件中的日志級(jí)別,不用重啟應(yīng)用就可以生效 那么,這個(gè)功能是怎么實(shí)現(xiàn)的呢?
應(yīng)用中使用logback作為日志輸出組件的話,大部分會(huì)去配置 logback.xml 這個(gè)文件,而且生產(chǎn)環(huán)境下,直接去修改logback.xml文件中的日志級(jí)別,不用重啟應(yīng)用就可以生效
那么,這個(gè)功能是怎么實(shí)現(xiàn)的呢?
I. 問(wèn)題描述及分析
針對(duì)上面的這個(gè)問(wèn)題,首先拋出一個(gè)實(shí)際的case,在我的個(gè)人網(wǎng)站 Z+中,所有的小工具都是通過(guò)配置文件來(lái)動(dòng)態(tài)新增和隱藏的,因?yàn)橹挥幸慌_(tái)服務(wù)器,所以配置文件就簡(jiǎn)化的直接放在了服務(wù)器的某個(gè)目錄下
現(xiàn)在的問(wèn)題時(shí),我需要在這個(gè)文件的內(nèi)容發(fā)生變動(dòng)時(shí),應(yīng)用可以感知這種變動(dòng),并重新加載文件內(nèi)容,更新應(yīng)用內(nèi)部緩存
一個(gè)最容易想到的方法,就是輪詢,判斷文件是否發(fā)生修改,如果修改了,則重新加載,并刷新內(nèi)存,所以主要需要關(guān)心的問(wèn)題如下:
- 如何輪詢?
- 如何判斷文件是否修改?
- 配置異常,會(huì)不會(huì)導(dǎo)致服務(wù)不可用?(即容錯(cuò),這個(gè)與本次主題關(guān)聯(lián)不大,但又比較重要...)
II. 設(shè)計(jì)與實(shí)現(xiàn)
問(wèn)題抽象出來(lái)之后,對(duì)應(yīng)的解決方案就比較清晰了
- 如何輪詢 ? --》 定時(shí)器 Timer, ScheduledExecutorService 都可以實(shí)現(xiàn)
- 如何判斷文件修改? --》根據(jù) java.io.File#lastModified 獲取文件的上次修改時(shí)間,比對(duì)即可
那么一個(gè)很簡(jiǎn)單的實(shí)現(xiàn)就比較容易了:
public class FileUpTest {
private long lastTime;
@Test
public void testFileUpdate() {
File file = new File("/tmp/alarmConfig");
// 首先文件的最近一次修改時(shí)間戳
lastTime = file.lastModified();
// 定時(shí)任務(wù),每秒來(lái)判斷一下文件是否發(fā)生變動(dòng),即判斷l(xiāng)astModified是否改變
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (file.lastModified() > lastTime) {
System.out.println("file update! time : " + file.lastModified());
lastTime = file.lastModified();
}
}
},0, 1, TimeUnit.SECONDS);
try {
Thread.sleep(1000 * 60);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上面這個(gè)屬于一個(gè)非常簡(jiǎn)單,非?;A(chǔ)的實(shí)現(xiàn)了,基本上也可以滿足我們的需求,那么這個(gè)實(shí)現(xiàn)有什么問(wèn)題呢?
定時(shí)任務(wù)的執(zhí)行中,如果出現(xiàn)了異常會(huì)怎樣?
對(duì)上面的代碼稍作修改
public class FileUpTest {
private long lastTime;
private void ttt() {
throw new NullPointerException();
}
@Test
public void testFileUpdate() {
File file = new File("/tmp/alarmConfig");
lastTime = file.lastModified();
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (file.lastModified() > lastTime) {
System.out.println("file update! time : " + file.lastModified());
lastTime = file.lastModified();
ttt();
}
}
}, 0, 1, TimeUnit.SECONDS);
try {
Thread.sleep(1000 * 60 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
實(shí)際測(cè)試,發(fā)現(xiàn)只有首次修改的時(shí)候,觸發(fā)了上面的代碼,但是再次修改則沒(méi)有效果了,即當(dāng)拋出異常之后,定時(shí)任務(wù)將不再繼續(xù)執(zhí)行了,這個(gè)問(wèn)題的主要原因是因?yàn)?ScheduledExecutorService 的原因了
直接查看ScheduledExecutorService的源碼注釋說(shuō)明
If any execution of the task encounters an exception, subsequent executions are suppressed.Otherwise, the task will only terminate via cancellation or termination of the executor. 即如果定時(shí)任務(wù)執(zhí)行過(guò)程中遇到發(fā)生異常,則后面的任務(wù)將不再執(zhí)行。
所以,使用這種姿勢(shì)的時(shí)候,得確保自己的任務(wù)不會(huì)拋出異常,否則后面就沒(méi)法玩了
對(duì)應(yīng)的解決方法也比較簡(jiǎn)單,整個(gè)catch一下就好
III. 進(jìn)階版
前面是一個(gè)基礎(chǔ)的實(shí)現(xiàn)版本了,當(dāng)然在java圈,基本上很多常見的需求,都是可以找到對(duì)應(yīng)的開源工具來(lái)使用的,當(dāng)然這個(gè)也不例外,而且應(yīng)該還是大家比較屬性的apache系列
首先maven依賴
<dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency>
主要是借助這個(gè)工具中的 FileAlterationObserver, FileAlterationListener, FileAlterationMonitor 三個(gè)類來(lái)實(shí)現(xiàn)相關(guān)的需求場(chǎng)景了,當(dāng)然使用也算是很簡(jiǎn)單了,以至于都不太清楚可以再怎么去說(shuō)明了,直接看下面從我的一個(gè)開源項(xiàng)目quick-alarm中拷貝出來(lái)的代碼
public class PropertiesConfListenerHelper {
public static boolean registerConfChangeListener(File file, Function<File, Map<String, AlarmConfig>> func) {
try {
// 輪詢間隔 5 秒
long interval = TimeUnit.SECONDS.toMillis(5);
// 因?yàn)楸O(jiān)聽是以目錄為單位進(jìn)行的,所以這里直接獲取文件的根目錄
File dir = file.getParentFile();
// 創(chuàng)建一個(gè)文件觀察器用于過(guò)濾
FileAlterationObserver observer = new FileAlterationObserver(dir,
FileFilterUtils.and(FileFilterUtils.fileFileFilter(),
FileFilterUtils.nameFileFilter(file.getName())));
//設(shè)置文件變化監(jiān)聽器
observer.addListener(new MyFileListener(func));
FileAlterationMonitor monitor = new FileAlterationMonitor(interval, observer);
monitor.start();
return true;
} catch (Exception e) {
log.error("register properties change listener error! e:{}", e);
return false;
}
}
static final class MyFileListener extends FileAlterationListenerAdaptor {
private Function<File, Map<String, AlarmConfig>> func;
public MyFileListener(Function<File, Map<String, AlarmConfig>> func) {
this.func = func;
}
@Override
public void onFileChange(File file) {
Map<String, AlarmConfig> ans = func.apply(file); // 如果加載失敗,打印一條日志
log.warn("PropertiesConfig changed! reload ans: {}", ans);
}
}
}
針對(duì)上面的實(shí)現(xiàn),簡(jiǎn)單說(shuō)明幾點(diǎn):
- 這個(gè)文件監(jiān)聽,是以目錄為根源,然后可以設(shè)置過(guò)濾器,來(lái)實(shí)現(xiàn)對(duì)應(yīng)文件變動(dòng)的監(jiān)聽
- 如上面registerConfChangeListener方法,傳入的file是具體的配置文件,因此構(gòu)建參數(shù)的時(shí)候,撈出了目錄,撈出了文件名作為過(guò)濾
- 第二參數(shù)是jdk8語(yǔ)法,其中為具體的讀取配置文件內(nèi)容,并映射為對(duì)應(yīng)的實(shí)體對(duì)象
一個(gè)問(wèn)題,如果 func方法執(zhí)行時(shí),也拋出了異常,會(huì)怎樣?
實(shí)際測(cè)試表現(xiàn)結(jié)果和上面一樣,拋出異常之后,依然跪,所以依然得注意,不要跑異常
那么簡(jiǎn)單來(lái)看一下上面的實(shí)現(xiàn)邏輯,直接扣出核心模塊
public void run() {
while(true) {
if(this.running) {
Iterator var1 = this.observers.iterator();
while(var1.hasNext()) {
FileAlterationObserver observer = (FileAlterationObserver)var1.next();
observer.checkAndNotify();
}
if(this.running) {
try {
Thread.sleep(this.interval);
} catch (InterruptedException var3) {
;
}
continue;
}
}
return;
}
}
從上面基本上一目了然,整個(gè)的實(shí)現(xiàn)邏輯了,和我們的第一種定時(shí)任務(wù)的方法不太一樣,這兒直接使用線程,死循環(huán),內(nèi)部采用sleep的方式來(lái)來(lái)暫停,因此出現(xiàn)異常時(shí),相當(dāng)于直接拋出去了,這個(gè)線程就跪了
補(bǔ)充JDK版本
jdk1.7,提供了一個(gè)WatchService,也可以用來(lái)實(shí)現(xiàn)文件變動(dòng)的監(jiān)聽,之前也沒(méi)有接觸過(guò),才知道有這個(gè)東西,然后搜了一下使用相關(guān),發(fā)現(xiàn)也挺簡(jiǎn)單的,看到有博文說(shuō)明是基于事件驅(qū)動(dòng)式的,效率更高,下面也給出一個(gè)簡(jiǎn)單的示例demo
@Test
public void testFileUpWather() throws IOException {
// 說(shuō)明,這里的監(jiān)聽也必須是目錄
Path path = Paths.get("/tmp");
WatchService watcher = FileSystems.getDefault().newWatchService();
path.register(watcher, ENTRY_MODIFY);
new Thread(() -> {
try {
while (true) {
WatchKey key = watcher.take();
for (WatchEvent<?> event : key.pollEvents()) {
if (event.kind() == OVERFLOW) {
//事件可能lost or discarded
continue;
}
Path fileName = (Path) event.context();
System.out.println("文件更新: " + fileName);
}
if (!key.reset()) { // 重設(shè)WatchKey
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
try {
Thread.sleep(1000 * 60 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
IV. 小結(jié)
使用Java來(lái)實(shí)現(xiàn)配置文件變動(dòng)的監(jiān)聽,主要涉及到的就是兩個(gè)點(diǎn)
- 如何輪詢: 定時(shí)器(Timer, ScheduledExecutorService), 線程死循環(huán)+sleep
- 文件修改: File#lastModified
整體來(lái)說(shuō),這個(gè)實(shí)現(xiàn)還是比較簡(jiǎn)單的,無(wú)論是自定義實(shí)現(xiàn),還是依賴 commos-io來(lái)做,都沒(méi)太大的技術(shù)成本,但是需要注意的一點(diǎn)是:
- 千萬(wàn)不要在定時(shí)任務(wù) or 文件變動(dòng)的回調(diào)方法中拋出異常?。?!
為了避免上面這個(gè)情況,一個(gè)可以做的實(shí)現(xiàn)是借助EventBus的異步消息通知來(lái)實(shí)現(xiàn),當(dāng)文件變動(dòng)之后,發(fā)送一個(gè)消息即可,然后在具體的重新加載文件內(nèi)容的方法上,添加一個(gè) @Subscribe注解即可,這樣既實(shí)現(xiàn)了解耦,也避免了異常導(dǎo)致的服務(wù)異常 (如果對(duì)這個(gè)實(shí)現(xiàn)有興趣的可以評(píng)論說(shuō)明)
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Javaweb監(jiān)聽器實(shí)例之統(tǒng)計(jì)在線人數(shù)
- java監(jiān)聽器實(shí)現(xiàn)在線人數(shù)統(tǒng)計(jì)
- java-RGB調(diào)色面板的實(shí)現(xiàn)(事件監(jiān)聽器之匿名內(nèi)部類)
- Java NIO.2 使用Path接口來(lái)監(jiān)聽文件、文件夾變化
- Java設(shè)計(jì)模式之監(jiān)聽器模式實(shí)例詳解
- Java Swing中JList選擇事件監(jiān)聽器ListSelectionListener用法示例
- Java監(jiān)聽器的作用及用法代碼示例
- 淺談java監(jiān)聽器的作用
- Java基于ServletContextListener實(shí)現(xiàn)UDP監(jiān)聽
相關(guān)文章
基于Java代碼實(shí)現(xiàn)游戲服務(wù)器生成全局唯一ID的方法匯總
我們?cè)谧龇?wù)器系統(tǒng)開發(fā)的時(shí)候,為了適應(yīng)數(shù)據(jù)大并發(fā)的請(qǐng)求,需要插入數(shù)據(jù)庫(kù)之前生成一個(gè)全局的唯一id,糾結(jié)全局唯一id怎么生成呢?下面小編給大家分享Java代碼實(shí)現(xiàn)游戲服務(wù)器生成全局唯一ID的方法匯總,涉及到優(yōu)劣勢(shì)方面的知識(shí)點(diǎn),對(duì)此感興趣的朋友一起看看吧2016-10-10
Java利用Jackson序列化實(shí)現(xiàn)數(shù)據(jù)脫敏
這篇文章主要介紹了利用Jackson序列化實(shí)現(xiàn)數(shù)據(jù)脫敏,首先在需要進(jìn)行脫敏的VO字段上面標(biāo)注相關(guān)脫敏注解,具體實(shí)例代碼文中給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-10-10
JAVA后臺(tái)轉(zhuǎn)換成樹結(jié)構(gòu)數(shù)據(jù)返回給前端的實(shí)現(xiàn)方法
這篇文章主要介紹了JAVA后臺(tái)轉(zhuǎn)換成樹結(jié)構(gòu)數(shù)據(jù)返回給前端的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03
SpringMVC+Mybatis實(shí)現(xiàn)的Mysql分頁(yè)數(shù)據(jù)查詢的示例
本篇文章主要介紹了SpringMVC+Mybatis實(shí)現(xiàn)的Mysql分頁(yè)數(shù)據(jù)查詢的示例,具有一定的參考價(jià)值,有興趣的可以了解一下2017-08-08
SpringCloud 服務(wù)注冊(cè)IP錯(cuò)誤的解決
這篇文章主要介紹了SpringCloud 服務(wù)注冊(cè)IP錯(cuò)誤的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07

