新聞中心
背景
看似比較簡單的一個功能,但如果在某些JDK版本下,可能會出現(xiàn)意想不到的Bug。

公司主營業(yè)務(wù):網(wǎng)站設(shè)計、成都網(wǎng)站設(shè)計、移動網(wǎng)站開發(fā)等業(yè)務(wù)。幫助企業(yè)客戶真正實現(xiàn)互聯(lián)網(wǎng)宣傳,提高企業(yè)的競爭能力。創(chuàng)新互聯(lián)公司是一支青春激揚、勤奮敬業(yè)、活力青春激揚、勤奮敬業(yè)、活力澎湃、和諧高效的團隊。公司秉承以“開放、自由、嚴(yán)謹(jǐn)、自律”為核心的企業(yè)文化,感謝他們對我們的高要求,感謝他們從不同領(lǐng)域給我們帶來的挑戰(zhàn),讓我們激情的團隊有機會用頭腦與智慧不斷的給客戶帶來驚喜。創(chuàng)新互聯(lián)公司推出寶塔免費做網(wǎng)站回饋大家。
本篇文章就帶大家簡單實現(xiàn)一個對應(yīng)的功能,并分析一下對應(yīng)的Bug和優(yōu)缺點。
初步實現(xiàn)思路
監(jiān)聽文件變動并讀取文件,簡單的思路如下:
- 單起一個線程,定時獲取文件最后更新的時間戳(單位:毫秒);
- 對比上一次的時間戳,如果不一致,則說明文件被改動,則重新進行加載;
這里寫一個簡單功能實現(xiàn)(不包含定時任務(wù)部分)的demo:
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;
// 重新加載,文件內(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)建一個文件(方便測試),然后兩次讀取文件的修改時間,并用LAST_TIME記錄上次修改時間。如果文件的最新更改時間與上一次不一致,則更新修改時間,并進行業(yè)務(wù)處理。
示例代碼中for循環(huán)兩次,便是為了演示變更與不變更的兩種情況。執(zhí)行程序,打印日志如下:
文件已被更新:1653557504000
文件未更新
執(zhí)行結(jié)果符合預(yù)期。
這種解決方案很明顯有兩個缺點:
- 無法實時感知文件的變動,程序輪訓(xùn)畢竟有一個時間差;
- lastModified返回的時間單位是毫秒,如果同一毫秒內(nèi)容出現(xiàn)兩次改動,而定時任務(wù)查詢時恰好落在兩次變動之間,則后一次變動則無法被感知到。
第一個缺點,對業(yè)務(wù)的影響不大;第二個缺點的概率比較小,可以忽略不計;
JDK的Bug登場
上面的代碼實現(xiàn),正常情況下是沒什么問題的,但如果你使用的Java版本為8或9時,則可能出現(xiàn)意想不到的Bug,這是由JDK本身的Bug導(dǎo)致的。
編號為JDK-8177809的Bug是這樣描述的:
Bug地址為:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8177809
這個Bug的基本描述就是:在Java8和9的某些版本下,lastModified方法返回時間戳并不是毫秒,而是秒,也就是說返回結(jié)果的后三位始終為0。
我們來寫一個程序驗證一下:
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);
// 讀取文件修改時間
long timestamp = readLastModified(fileName);
System.out.println("文件修改時間:" + 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);
// 寫入隨機數(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)建一個文件,然后在for循環(huán)中不停的向文件寫入內(nèi)容,并讀取修改時間。每次操作睡眠100ms。這樣,同一秒就可以多次寫文件和讀修改時間。
執(zhí)行結(jié)果如下:
文件修改時間:1653558619000
文件修改時間:1653558619000
文件修改時間:1653558619000
文件修改時間:1653558619000
文件修改時間:1653558619000
文件修改時間:1653558619000
文件修改時間:1653558620000
文件修改時間:1653558620000
文件修改時間:1653558620000
文件修改時間:1653558620000
修改了10次文件的內(nèi)容,只感知到了2次。JDK的這個bug讓這種實現(xiàn)方式的第2個缺點無限放大了,同一秒發(fā)生變更的概率可比同一毫秒發(fā)生的概率要大太多了。
PS:在官方Bug描述中提到可以通過Files.getLastModifiedTime來實現(xiàn)獲取時間戳,但筆者驗證的結(jié)果是依舊無效,可能不同版本有不同的表現(xiàn)吧。
更新解決方案
Java 8目前是主流版本,不可能因為JDK的該bug就換JDK吧。所以,我們要通過其他方式來實現(xiàn)這個業(yè)務(wù)功能,那就是新增一個用來記錄文件版本(version)的文件(或其他存儲方式)。這個version的值,可在寫文件時按照遞增生成版本號,也可以通過對文件的內(nèi)容做MD5計算獲得。
如果能保證版本順序生成,使用時只需讀取版本文件中的值進行比對即可,如果變更則重新加載,如果未變更則不做處理。
如果使用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);
// 同時寫入版本
writeToFile(versionName, i);
// 監(jiān)聽器讀取文件版本
int fileVersion = Integer.parseInt(readOneLineFromFile(versionName));
if (version == fileVersion) {
System.out.println("版本未變更");
} else {
System.out.println("版本已變化,進行業(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時文件結(jié)束
tempString = reader.readLine();
} catch (IOException e) {
e.printStackTrace();
}
return tempString;
}
}執(zhí)行上述代碼,打印日志如下:
版本已變化,進行業(yè)務(wù)處理
版本已變化,進行業(yè)務(wù)處理
版本已變化,進行業(yè)務(wù)處理
版本已變化,進行業(yè)務(wù)處理
版本已變化,進行業(yè)務(wù)處理
版本已變化,進行業(yè)務(wù)處理
版本已變化,進行業(yè)務(wù)處理
版本已變化,進行業(yè)務(wù)處理
版本已變化,進行業(yè)務(wù)處理
可以看到,每次文件變更都能夠感知到。當(dāng)然,上述代碼只是示例,在使用的過程中還是需要更多地完善邏輯。
小結(jié)
本文實踐了一個很常見的功能,起初采用很符合常規(guī)思路的方案來解決,結(jié)果恰好碰到了JDK的Bug,只好變更策略來實現(xiàn)。當(dāng)然,如果業(yè)務(wù)環(huán)境中已經(jīng)存在了一些基礎(chǔ)的中間件還有更多解決方案。
而通過本篇文章我們學(xué)到了JDK Bug導(dǎo)致的連鎖反應(yīng),同時也見證了:實踐見真知。很多技術(shù)方案是否可行,還是需要經(jīng)得起實踐的考驗才行。趕快檢查一下你的代碼實現(xiàn),是否命中該Bug?
文章標(biāo)題:JDK的一個Bug,監(jiān)聽文件變更要小心了
轉(zhuǎn)載注明:http://www.5511xx.com/article/djcoehg.html


咨詢
建站咨詢
