首頁 > 軟體

JDK的一個Bug監聽檔案變更的初步實現思路

2022-05-30 14:01:58

背景

在某些業務場景下,我們需要自己實現檔案內容變更監聽的功能,比如:監聽某個檔案是否發生變更,當變更時重新載入檔案的內容。

看似比較簡單的一個功能,但如果在某些JDK版本下,可能會出現意想不到的Bug。

本篇文章就帶大家簡單實現一個對應的功能,並分析一下對應的Bug和優缺點。

初步實現思路

監聽檔案變動並讀取檔案,簡單的思路如下:

  • 單起一個執行緒,定時獲取檔案最後更新的時間戳(單位:毫秒);

  • 對比上一次的時間戳,如果不一致,則說明檔案被改動,則重新進行載入;

這裡寫一個簡單功能實現(不包含定時任務部分)的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";
  // 建立檔案,僅為範例,實踐中由其他程式觸發檔案的變更
  createFile(fileName);

  // 執行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("建立檔案:" + result);
  }
 }

 public static long readLastModified(String fileName) {
  File file = new File(fileName);
  return file.lastModified();
 }
}

在上述程式碼中,先建立一個檔案(方便測試),然後兩次讀取檔案的修改時間,並用LAST_TIME記錄上次修改時間。如果檔案的最新更改時間與上一次不一致,則更新修改時間,並進行業務處理。

範例程式碼中for迴圈兩次,便是為了演示變更與不變更的兩種情況。執行程式,列印紀錄檔如下:

檔案已被更新:1653557504000
檔案未更新

執行結果符合預期。

這種解決方案很明顯有兩個缺點:

  • 無法實時感知檔案的變動,程式輪訓畢竟有一個時間差;

  • lastModified返回的時間單位是毫秒,如果同一毫秒內容出現兩次改動,而定時任務查詢時恰好落在兩次變動之間,則後一次變動則無法被感知到。

第一個缺點,對業務的影響不大;第二個缺點的概率比較小,可以忽略不計;

JDK的Bug登場

上面的程式碼實現,正常情況下是沒什麼問題的,但如果你使用的Java版本為8或9時,則可能出現意想不到的Bug,這是由JDK本身的Bug導致的。

編號為JDK-8177809的Bug是這樣描述的:

JDK-8177809

Bug地址為:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8177809

這個Bug的基本描述就是:在Java8和9的某些版本下,lastModified方法返回時間戳並不是毫秒,而是秒,也就是說返回結果的後三位始終為0。

我們來寫一個程式驗證一下:

public class FileReadDemo {

 public static void main(String[] args) throws IOException, InterruptedException {

  String fileName = "/Users/zzs/temp/1.txt";
  // 建立檔案
  createFile(fileName);

  for (int i = 0; i < 10; i++) {
   // 向檔案內寫入資料
   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("建立檔案:" + result);
  }
 }

 public static void writeToFile(String fileName) throws IOException {
  FileWriter fileWriter = new FileWriter(fileName);
  // 寫入亂數字
  fileWriter.write(new Random(1000).nextInt());
  fileWriter.close();
 }

 public static long readLastModified(String fileName) {
  File file = new File(fileName);
  return file.lastModified();
 }
}

在上述程式碼中,先建立一個檔案,然後在for迴圈中不停的向檔案寫入內容,並讀取修改時間。每次操作睡眠100ms。這樣,同一秒就可以多次寫檔案和讀修改時間。

執行結果如下:

檔案修改時間:1653558619000
檔案修改時間:1653558619000
檔案修改時間:1653558619000
檔案修改時間:1653558619000
檔案修改時間:1653558619000
檔案修改時間:1653558619000
檔案修改時間:1653558620000
檔案修改時間:1653558620000
檔案修改時間:1653558620000
檔案修改時間:1653558620000

修改了10次檔案的內容,只感知到了2次。JDK的這個bug讓這種實現方式的第2個缺點無限放大了,同一秒發生變更的概率可比同一毫秒發生的概率要大太多了。

PS:在官方Bug描述中提到可以通過Files.getLastModifiedTime來實現獲取時間戳,但筆者驗證的結果是依舊無效,可能不同版本有不同的表現吧。

更新解決方案

Java 8目前是主流版本,不可能因為JDK的該bug就換JDK吧。所以,我們要通過其他方式來實現這個業務功能,那就是新增一個用來記錄檔案版本(version)的檔案(或其他儲存方式)。這個version的值,可在寫檔案時按照遞增生成版本號,也可以通過對檔案的內容做MD5計算獲得。

如果能保證版本順序生成,使用時只需讀取版本檔案中的值進行比對即可,如果變更則重新載入,如果未變更則不做處理。

如果使用MD5的形式,則需考慮MD5演演算法的效能,以及MD5結果的碰撞(概率很小,可以忽略)。

下面以版本的形式來展示一下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";
  // 建立檔案
  createFile(fileName);
  createFile(versionName);

  for (int i = 1; i < 10; i++) {
   // 向檔案內寫入資料
   writeToFile(fileName);
   // 同時寫入版本
   writeToFile(versionName, i);
   // 監聽器讀取檔案版本
   int fileVersion = Integer.parseInt(readOneLineFromFile(versionName));
   if (version == fileVersion) {
    System.out.println("版本未變更");
   } else {
    System.out.println("版本已變化,進行業務處理");
   }
   // 睡眠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("建立檔案:" + 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時檔案結束
   tempString = reader.readLine();
  } catch (IOException e) {
   e.printStackTrace();
  }
  return tempString;
 }
}

執行上述程式碼,列印紀錄檔如下:

版本已變化,進行業務處理
版本已變化,進行業務處理
版本已變化,進行業務處理
版本已變化,進行業務處理
版本已變化,進行業務處理
版本已變化,進行業務處理
版本已變化,進行業務處理
版本已變化,進行業務處理
版本已變化,進行業務處理

可以看到,每次檔案變更都能夠感知到。當然,上述程式碼只是範例,在使用的過程中還是需要更多地完善邏輯。

小結

本文實踐了一個很常見的功能,起初採用很符合常規思路的方案來解決,結果恰好碰到了JDK的Bug,只好變更策略來實現。當然,如果業務環境中已經存在了一些基礎的中介軟體還有更多解決方案。

而通過本篇文章我們學到了JDK Bug導致的連鎖反應,同時也見證了:實踐見真知。很多技術方案是否可行,還是需要經得起實踐的考驗才行。趕快檢查一下你的程式碼實現,是否命中該Bug?

到此這篇關於JDK的一個Bug監聽檔案變更要小心了的文章就介紹到這了,更多相關JDK監聽檔案內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com