<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          JDK的一個Bug,監(jiān)聽文件變更要小心了

          共 534字,需瀏覽 2分鐘

           ·

          2022-05-29 09:31

          背景

          在某些業(yè)務(wù)場景下,我們需要自己實現(xiàn)文件內(nèi)容變更監(jiān)聽的功能,比如:監(jiān)聽某個文件是否發(fā)生變更,當(dāng)變更時重新加載文件的內(nèi)容。

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

          本篇文章就帶大家簡單實現(xiàn)一個對應(yīng)的功能,并分析一下對應(yīng)的Bug和優(yōu)缺點。

          初步實現(xiàn)思路

          監(jiān)聽文件變動并讀取文件,簡單的思路如下:

          • 單起一個線程,定時獲取文件最后更新的時間戳(單位:毫秒);
          • 對比上一次的時間戳,如果不一致,則說明文件被改動,則重新進(jì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????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記錄上次修改時間。如果文件的最新更改時間與上一次不一致,則更新修改時間,并進(jìn)行業(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是這樣描述的:

          JDK-8177809

          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????//?向文件內(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);
          ??//?寫入隨機(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)建一個文件,然后在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計算獲得。

          如果能保證版本順序生成,使用時只需讀取版本文件中的值進(jìn)行比對即可,如果變更則重新加載,如果未變更則不做處理。

          如果使用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????//?向文件內(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("版本已變化,進(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時文件結(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é)

          本文實踐了一個很常見的功能,起初采用很符合常規(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?


          往期推薦

          最簡單的6種防止數(shù)據(jù)重復(fù)提交的方法!(干貨)


          Spring Cloud OpenFeign 的 5 個優(yōu)化小技巧!


          IDEA 版 Postman 面世了,功能真心強(qiáng)大!


          瀏覽 34
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费黄色A片 | 亚洲综合在线婷婷 | 亚洲天堂一区 | 天堂网在线最新版www中文网 | 喷水视频在线观看 |