当前位置: 首页 > 科技观察

JDK的一个bug,监听文件变化要小心

时间:2023-03-18 02:46:00 科技观察

背景在一些业务场景中,我们需要自己实现监听文件内容变化的功能,比如:监听文件是否发生变化,发生变化时重新加载文件改变内容。看似是一个比较简单的功能,但是如果在某些JDK版本下,可能会出现意想不到的bug。本文将带大家简单实现一个相应的功能,并分析相应的Bug、优缺点。监听文件变化和读取文件的思路初步实现,简单思路如下:启动单线程,定时获取文件最后一次更新的时间戳(单位:毫秒);比较上次的时间戳,如果不一致,说明文件被修改,重新加载;下面是一个简单功能实现的demo(不包括定时任务部分):publicclassFileWatchDemo{/***最后更新时间*/publicstaticlongLAST_TIME=0L;publicstaticvoidmain(String[]args)throwsIOException{StringfileName="/Users/zzs/temp/1.txt";//创建一个文件,只是一个例子,在实践中,其他程序触发文件更改createFile(fileName);//执行两次for(inti=0;i<2;i++){longtimestamp=readLastModified(fileName);if(timestamp!=LAST_TIME){System.out.println("文件已更新:"+timestamp);LAST_TIME=时间戳;//重新加载,文件内容}else{System.out.println("文件未更新");}}}publicstaticvoidcreateFile(StringfileName)抛出IOException{Filefile=newFile(fileName);如果(!file.exists()){布尔结果=file.createNewFile();System.out.println("创建文件:"+result);}}publicstaticlongreadLastModified(StringfileName){Filefile=newFile(fileName);返回文件.lastModified();}}在上面的代码中,先创建一个文件(方便测试),然后两次读取文件的修改时间,用LAST_TIME记录最后一次修改时间。如果文件的最新修改时间与上次不一致,则更新修改时间并进行业务处理。示例代码中的for循环使用了两次,分别演示了变化和不变化的两种情况。执行程序,打印日志如下:文件已更新:1653557504000文件未更新,执行结果符合预期。这种方案显然有两个缺点:不能实时感知文件的变化,毕竟程序轮训有时间差;lastModified返回的时间单位是毫秒,如果同一毫秒的内容有两次变化,而定时任务查询恰好落在两次变化之间,无法感知到后面的变化。第一个缺点对业务影响不大;第二个缺点出现的概率比较小,可以忽略;JDKBug登场以上代码实现,正常情况下是没有问题的,但是如果你使用的Java版本是8或者9,可能会出现意想不到的bug,这是JDK本身的bug导致的。编号为JDK-8177809的bug描述如下:bug地址为:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8177809该bug的基本描述为:在Java8和9的某些版本中其次,lastModified方法返回的时间戳不是毫秒,而是秒,也就是说返回结果的后三位永远为0。让我们写一个程序来验证一下://创建一个文件createFile(fileName);for(inti=0;i<10;i++){//将数据写入文件writeToFile(fileName);//读取文件修改时间longtimestamp=readLastModified(fileName);System.out.println("文件修改时间:"+timestamp);//休眠100毫秒Thread.sleep(100);}}publicstaticvoidcreateFile(StringfileName)抛出IOException{Filefile=newFile(fileName);if(!file.exists()){布尔结果=file.createNewFile();System.out.println("创建文件:"+result);}}publicstaticvoidwriteToFile(StringfileName)抛出IOException{FileWriterfileWriter=newFileWriter(fileName);//写入随机数fileWriter.write(newRandom(1000).nextInt());fileWriter.close();}publicstaticlongreadLastModified(StringfileName){Filefile=newFile(fileName);返回file.lastModified();}}在上面的代码中,首先创建一个file,andthenwritecontenttothefilecontinuouslyintheforloop,andreadthemodificationtimeandsleepfor100msforeachoperation.Inthisway,thefilecanbewrittenandthemodificationtimereadmultipletimesinthesamesecond.执行结果如下:文件修改时间:1653558619000文件修改时间:1653558619000文件修改时间:1653558619000文件修改时间:1653558619000文件修改时间:1653558619000文件修改时间:1653558619000文件修改时间:1653558620000文件修改时间:1653558620000文件修改时间:1653558620000文件Modificationtime:1653558620000Modifiedthecontentofthefile10times,onlyperceived2times.ThisbugofJDKmagnifiesthesecondshortcomingofthisimplementationinfinitely.Theprobabilityofachangeinthesamesecondismuchgreaterthantheprobabilityofachangeinthesamemillisecond.PS:Intheofficialbugdescription,itismentionedthatthetimestampcanbeobtainedthroughFiles.getLastModifiedTime,buttheresultoftheauthor'sverificationisstillinvalid,anddifferentversionsmayhavedifferentperformances.UpdatesolutionJava8iscurrentlythemainstreamversion,itisimpossibletochangetheJDKbecauseofthebugoftheJDK.Therefore,weneedtoimplementthisbusinessfunctioninotherways,thatis,addafile(orotherstoragemethod)torecordthefileversion(version).Thevalueofthisversioncanbeincrementedtogenerateaversionnumberwhenwritingafile,oritcanbeobtainedbyperformingMD5calculationonthecontentofthefile.Iftheorderofversiongenerationcanbeguaranteed,youonlyneedtoreadthevalueintheversionfileforcomparisonwhenusingit.Ifitischanged,itwillbereloaded,andifithasnotbeenchanged,itwillnotbeprocessed.IftheformofMD5isused,theperformanceoftheMD5algorithmandthecollisionoftheMD5resultshouldbeconsidered(theprobabilityisverysmallandcanbeignored).下面以版本形式展示demo:publicclassFileReadVersionDemo{publicstaticintversion=0;publicstaticvoidmain(String[]args)throwsIOException,InterruptedException{StringfileName="/Users/zzs/temp/1.txt";StringversionName="/Users/zzs/temp/version.txt";//创建文件createFile(fileName);创建文件(版本名称);for(inti=1;i<10;i++){//添加到文件writeToFile(fileName)里面的数据;//同时写入版本writeToFile(versionName,i);//监听器读取文件版本intfileVersion=Integer.parseInt(readOneLineFromFile(versionName));if(version==fileVersion){System.out.println("版本没有改变");}else{System.out.println("版本已更改,业务处理中");}//睡眠100msThread.sleep(100);}}publicstaticvoidcreateFile(StringfileName)抛出IOException{Filefile=newFile(fileName);如果(!file.exists()){布尔结果=file.createNewFile();System.out.println("创建文件:"+result);}}publicstaticvoidwriteToFile(StringfileName)抛出IOException{writeToFile(fileName,newRandom(1000).nextInt());}publicstaticvoidwriteToFile(StringfileName,intversion)throwsIOException{FileWriterfileWriter=newFileWriter(fileName);fileWriter.write(version+"");文件写入器.close();}publicstaticStringreadOneLineFromFile(StringfileName){Filefile=newFile(fileName);字符串tempString=null;try(BufferedReaderreader=newBufferedReader(newFileReader(file))){//一次读一行,当读到null时,文件结束tempString=reader.readLine();}catch(IOExceptione){e.printStackTrace();}返回临时字符串;}}执行以上代码,打印日志如下:业务处理的版本变了,业务处理的版本变了,业务处理的版本变了,业务处理的版本变了,业务处理,你看每一个文件的变化都能感知。当然,上面的代码只是一个例子,更多的逻辑需要在使用过程中完善。小结本文实践了一个很常用的功能。起初,它采用了一种符合常规思维的解决方案。结果碰巧遇到了JDK的bug,只好换个策略来实现。当然,如果业务环境中已经存在一些基础的中间件,那么解决方案就更多了。通过这篇文章,我们了解到了JDKBug引发的连锁反应,也见证了:实践见真情。很多技术方案是否可行,还需要经得起实践的检验。快速检查您的代码实现,它是否遇到了错误?