JDK的一個(gè)Bug監(jiān)聽文件變更的初步實(shí)現(xiàn)思路
背景
在某些業(yè)務(wù)場(chǎng)景下,我們需要自己實(shí)現(xiàn)文件內(nèi)容變更監(jiān)聽的功能,比如:監(jiān)聽某個(gè)文件是否發(fā)生變更,當(dāng)變更時(shí)重新加載文件的內(nèi)容。
看似比較簡(jiǎn)單的一個(gè)功能,但如果在某些JDK版本下,可能會(huì)出現(xiàn)意想不到的Bug。
本篇文章就帶大家簡(jiǎn)單實(shí)現(xiàn)一個(gè)對(duì)應(yīng)的功能,并分析一下對(duì)應(yīng)的Bug和優(yōu)缺點(diǎn)。
初步實(shí)現(xiàn)思路
監(jiān)聽文件變動(dòng)并讀取文件,簡(jiǎn)單的思路如下:
單起一個(gè)線程,定時(shí)獲取文件最后更新的時(shí)間戳(單位:毫秒);
對(duì)比上一次的時(shí)間戳,如果不一致,則說明文件被改動(dòng),則重新進(jìn)行加載;
這里寫一個(gè)簡(jiǎn)單功能實(shí)現(xiàn)(不包含定時(shí)任務(wù)部分)的demo:
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(); ?} }
在上述代碼中,先創(chuàng)建一個(gè)文件(方便測(cè)試),然后兩次讀取文件的修改時(shí)間,并用LAST_TIME記錄上次修改時(shí)間。如果文件的最新更改時(shí)間與上一次不一致,則更新修改時(shí)間,并進(jìn)行業(yè)務(wù)處理。
示例代碼中for循環(huán)兩次,便是為了演示變更與不變更的兩種情況。執(zhí)行程序,打印日志如下:
文件已被更新:1653557504000
文件未更新
執(zhí)行結(jié)果符合預(yù)期。
這種解決方案很明顯有兩個(gè)缺點(diǎn):
無法實(shí)時(shí)感知文件的變動(dòng),程序輪訓(xùn)畢竟有一個(gè)時(shí)間差;
lastModified返回的時(shí)間單位是毫秒,如果同一毫秒內(nèi)容出現(xiàn)兩次改動(dòng),而定時(shí)任務(wù)查詢時(shí)恰好落在兩次變動(dòng)之間,則后一次變動(dòng)則無法被感知到。
第一個(gè)缺點(diǎn),對(duì)業(yè)務(wù)的影響不大;第二個(gè)缺點(diǎn)的概率比較小,可以忽略不計(jì);
JDK的Bug登場(chǎng)
上面的代碼實(shí)現(xiàn),正常情況下是沒什么問題的,但如果你使用的Java版本為8或9時(shí),則可能出現(xiàn)意想不到的Bug,這是由JDK本身的Bug導(dǎo)致的。
編號(hào)為JDK-8177809的Bug是這樣描述的:
JDK-8177809
Bug地址為:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8177809
這個(gè)Bug的基本描述就是:在Java8和9的某些版本下,lastModified方法返回時(shí)間戳并不是毫秒,而是秒,也就是說返回結(jié)果的后三位始終為0。
我們來寫一個(gè)程序驗(yàn)證一下:
public?class?FileReadDemo?{ ?public?static?void?main(String[]?args)?throws?IOException,?InterruptedException?{ ??String?fileName?=?"/Users/zzs/temp/1.txt"; ??//?創(chuàng)建文件 ??createFile(fileName); ??for?(int?i?=?0;?i?<?10;?i++)?{ ???//?向文件內(nèi)寫入數(shù)據(jù) ???writeToFile(fileName); ???//?讀取文件修改時(shí)間 ???long?timestamp?=?readLastModified(fileName); ???System.out.println("文件修改時(shí)間:"?+?timestamp); ???//?睡眠100ms ???Thread.sleep(100); ??} ?} ?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?void?writeToFile(String?fileName)?throws?IOException?{ ??FileWriter?fileWriter?=?new?FileWriter(fileName); ??//?寫入隨機(jī)數(shù)字 ??fileWriter.write(new?Random(1000).nextInt()); ??fileWriter.close(); ?} ?public?static?long?readLastModified(String?fileName)?{ ??File?file?=?new?File(fileName); ??return?file.lastModified(); ?} }
在上述代碼中,先創(chuàng)建一個(gè)文件,然后在for循環(huán)中不停的向文件寫入內(nèi)容,并讀取修改時(shí)間。每次操作睡眠100ms。這樣,同一秒就可以多次寫文件和讀修改時(shí)間。
執(zhí)行結(jié)果如下:
文件修改時(shí)間:1653558619000
文件修改時(shí)間:1653558619000
文件修改時(shí)間:1653558619000
文件修改時(shí)間:1653558619000
文件修改時(shí)間:1653558619000
文件修改時(shí)間:1653558619000
文件修改時(shí)間:1653558620000
文件修改時(shí)間:1653558620000
文件修改時(shí)間:1653558620000
文件修改時(shí)間:1653558620000
修改了10次文件的內(nèi)容,只感知到了2次。JDK的這個(gè)bug讓這種實(shí)現(xiàn)方式的第2個(gè)缺點(diǎn)無限放大了,同一秒發(fā)生變更的概率可比同一毫秒發(fā)生的概率要大太多了。
PS:在官方Bug描述中提到可以通過Files.getLastModifiedTime來實(shí)現(xiàn)獲取時(shí)間戳,但筆者驗(yàn)證的結(jié)果是依舊無效,可能不同版本有不同的表現(xiàn)吧。
更新解決方案
Java 8目前是主流版本,不可能因?yàn)镴DK的該bug就換JDK吧。所以,我們要通過其他方式來實(shí)現(xiàn)這個(gè)業(yè)務(wù)功能,那就是新增一個(gè)用來記錄文件版本(version)的文件(或其他存儲(chǔ)方式)。這個(gè)version的值,可在寫文件時(shí)按照遞增生成版本號(hào),也可以通過對(duì)文件的內(nèi)容做MD5計(jì)算獲得。
如果能保證版本順序生成,使用時(shí)只需讀取版本文件中的值進(jìn)行比對(duì)即可,如果變更則重新加載,如果未變更則不做處理。
如果使用MD5的形式,則需考慮MD5算法的性能,以及MD5結(jié)果的碰撞(概率很小,可以忽略)。
下面以版本的形式來展示一下demo:
public?class?FileReadVersionDemo?{ ?public?static?int?version?=?0; ?public?static?void?main(String[]?args)?throws?IOException,?InterruptedException?{ ??String?fileName?=?"/Users/zzs/temp/1.txt"; ??String?versionName?=?"/Users/zzs/temp/version.txt"; ??//?創(chuàng)建文件 ??createFile(fileName); ??createFile(versionName); ??for?(int?i?=?1;?i?<?10;?i++)?{ ???//?向文件內(nèi)寫入數(shù)據(jù) ???writeToFile(fileName); ???//?同時(shí)寫入版本 ???writeToFile(versionName,?i); ???//?監(jiān)聽器讀取文件版本 ???int?fileVersion?=?Integer.parseInt(readOneLineFromFile(versionName)); ???if?(version?==?fileVersion)?{ ????System.out.println("版本未變更"); ???}?else?{ ????System.out.println("版本已變化,進(jìn)行業(yè)務(wù)處理"); ???} ???//?睡眠100ms ???Thread.sleep(100); ??} ?} ?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?void?writeToFile(String?fileName)?throws?IOException?{ ??writeToFile(fileName,?new?Random(1000).nextInt()); ?} ?public?static?void?writeToFile(String?fileName,?int?version)?throws?IOException?{ ??FileWriter?fileWriter?=?new?FileWriter(fileName); ??fileWriter.write(version?+""); ??fileWriter.close(); ?} ?public?static?String?readOneLineFromFile(String?fileName)?{ ??File?file?=?new?File(fileName); ??String?tempString?=?null; ??try?(BufferedReader?reader?=?new?BufferedReader(new?FileReader(file)))?{ ???//一次讀一行,讀入null時(shí)文件結(jié)束 ???tempString?=?reader.readLine(); ??}?catch?(IOException?e)?{ ???e.printStackTrace(); ??} ??return?tempString; ?} }
執(zhí)行上述代碼,打印日志如下:
版本已變化,進(jìn)行業(yè)務(wù)處理
版本已變化,進(jìn)行業(yè)務(wù)處理
版本已變化,進(jìn)行業(yè)務(wù)處理
版本已變化,進(jìn)行業(yè)務(wù)處理
版本已變化,進(jìn)行業(yè)務(wù)處理
版本已變化,進(jìn)行業(yè)務(wù)處理
版本已變化,進(jìn)行業(yè)務(wù)處理
版本已變化,進(jìn)行業(yè)務(wù)處理
版本已變化,進(jìn)行業(yè)務(wù)處理
可以看到,每次文件變更都能夠感知到。當(dāng)然,上述代碼只是示例,在使用的過程中還是需要更多地完善邏輯。
小結(jié)
本文實(shí)踐了一個(gè)很常見的功能,起初采用很符合常規(guī)思路的方案來解決,結(jié)果恰好碰到了JDK的Bug,只好變更策略來實(shí)現(xiàn)。當(dāng)然,如果業(yè)務(wù)環(huán)境中已經(jīng)存在了一些基礎(chǔ)的中間件還有更多解決方案。
而通過本篇文章我們學(xué)到了JDK Bug導(dǎo)致的連鎖反應(yīng),同時(shí)也見證了:實(shí)踐見真知。很多技術(shù)方案是否可行,還是需要經(jīng)得起實(shí)踐的考驗(yàn)才行。趕快檢查一下你的代碼實(shí)現(xiàn),是否命中該Bug?
到此這篇關(guān)于JDK的一個(gè)Bug監(jiān)聽文件變更要小心了的文章就介紹到這了,更多相關(guān)JDK監(jiān)聽文件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot用戶數(shù)據(jù)修改的詳細(xì)實(shí)現(xiàn)
用戶管理功能作為所有的系統(tǒng)是必不可少的一部分,下面這篇文章主要給大家介紹了關(guān)于springboot用戶數(shù)據(jù)修改的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-04-04快速搭建Spring Boot+MyBatis的項(xiàng)目IDEA(附源碼下載)
這篇文章主要介紹了快速搭建Spring Boot+MyBatis的項(xiàng)目IDEA(附源碼下載),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12MyBatis中使用foreach循環(huán)的坑及解決
這篇文章主要介紹了MyBatis中使用foreach循環(huán)的坑及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01詳解Java使用Jsch與sftp服務(wù)器實(shí)現(xiàn)ssh免密登錄
這篇文章主要介紹了詳解Java使用Jsch與sftp服務(wù)器實(shí)現(xiàn)ssh免密登錄,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10java動(dòng)態(tài)添加外部jar包到classpath的實(shí)例詳解
這篇文章主要介紹了java動(dòng)態(tài)添加外部jar包到classpath的實(shí)例詳解的相關(guān)資料,希望通過本文能幫助到大家,需要的朋友可以參考下2017-09-09簡(jiǎn)單了解mybatis攔截器實(shí)現(xiàn)原理及實(shí)例
這篇文章主要介紹了簡(jiǎn)單了解mybatis攔截器實(shí)現(xiàn)原理及實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01java Socket編程實(shí)現(xiàn)I/O多路復(fù)用的示例
本文主要介紹了java Socket編程實(shí)現(xiàn)I/O多路復(fù)用的示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-09-09springboot與vue實(shí)現(xiàn)簡(jiǎn)單的CURD過程詳析
這篇文章主要介紹了springboot與vue實(shí)現(xiàn)簡(jiǎn)單的CURD過程詳析,圍繞springboot與vue的相關(guān)資料展開實(shí)現(xiàn)CURD過程的過程介紹,需要的小伙伴可以參考一下2022-01-01