背景

在某些业务场景下,我们需要自己实现文件内容变更监听的功能,比如:监听某个文件是否发生变更,当变更时重新加载文件的内容。

看似比较简单的一个功能,但如果在某些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监听文件内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

JDK的一个Bug监听文件变更的初步实现思路的更多相关文章

  1. Butterknife 8.1.0在Android Studio 2.1.2中不能与JDK 1.8一起使用

    如果是,我需要做些什么才能使其正常工作?

  2. android-studio – 安卓工作室更新后的问题

    解决方法我在AndroidStudio中花了很多时间来处理这个问题.看来这个问题是由用于编译项目的java版本的差异引起的.最后,在“项目结构”设置窗口中,我在SDK位置选项卡中启用了“使用嵌入式JDK(推荐)”.并快乐编译:)

  3. Android Studio在启动时修改./idea/vcs.xml

    因为不建议忽略AndroidStudio中的整个.idea文件夹,所以大多数文件都由git跟踪.然而奇怪的是,每次启动后,即使已经存在数十个,也会向vcs.xml添加相同的行.这很快变老了.这种行为是有目的还是仅仅是一个错误?AndroidStudio还可以在启动时阻止它进行此类修改吗?

  4. Java中JDK动态代理的超详细讲解

    JDK 的动态代理是基于拦截器和反射来实现的,JDK代理是不需要第三方库支持的,只需要JDK环境就可以进行代理,下面这篇文章主要给大家介绍了关于Java中JDK动态代理的超详细讲解,需要的朋友可以参考下

  5. JDK与Dubbo中的SPI详细介绍

    这篇文章主要介绍了JDK中的SPI与Dubbo中的SPI,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  6. JDK下载与安装超详细步骤大全

    学习JAVA必须得安装一下JDK(java development kit java开发工具包),配置一下环境就可以学习JAVA了,下面这篇文章主要给大家介绍了关于JDK下载与安装步骤的相关资料,需要的朋友可以参考下

  7. JDK19新特性使用实例详解

    这篇文章主要为大家介绍了JDK19新特性使用实例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  8. 详解JDK自带javap命令反编译class文件和Jad反编译class文件(推荐使用jad)

    这篇文章主要介绍了JDK自带javap命令反编译class文件和Jad反编译class文件(推荐使用jad),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  9. 理解JDK动态代理为什么必须要基于接口

    这篇文章主要介绍了理解JDK动态代理为什么必须要基于接口,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  10. JDK的一个Bug监听文件变更的初步实现思路

    这篇文章主要介绍了JDK的一个Bug监听文件变更要小心了,本篇文章就带大家简单实现一个对应的功能,并分析一下对应的Bug和优缺点,需要的朋友可以参考下

随机推荐

  1. 基于EJB技术的商务预订系统的开发

    用EJB结构开发的应用程序是可伸缩的、事务型的、多用户安全的。总的来说,EJB是一个组件事务监控的标准服务器端的组件模型。基于EJB技术的系统结构模型EJB结构是一个服务端组件结构,是一个层次性结构,其结构模型如图1所示。图2:商务预订系统的构架EntityBean是为了现实世界的对象建造的模型,这些对象通常是数据库的一些持久记录。

  2. Java利用POI实现导入导出Excel表格

    这篇文章主要为大家详细介绍了Java利用POI实现导入导出Excel表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. Mybatis分页插件PageHelper手写实现示例

    这篇文章主要为大家介绍了Mybatis分页插件PageHelper手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  4. (jsp/html)网页上嵌入播放器(常用播放器代码整理)

    网页上嵌入播放器,只要在HTML上添加以上代码就OK了,下面整理了一些常用的播放器代码,总有一款适合你,感兴趣的朋友可以参考下哈,希望对你有所帮助

  5. Java 阻塞队列BlockingQueue详解

    本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景,通过实例代码介绍了Java 阻塞队列BlockingQueue的相关知识,需要的朋友可以参考下

  6. Java异常Exception详细讲解

    异常就是不正常,比如当我们身体出现了异常我们会根据身体情况选择喝开水、吃药、看病、等 异常处理方法。 java异常处理机制是我们java语言使用异常处理机制为程序提供了错误处理的能力,程序出现的错误,程序可以安全的退出,以保证程序正常的运行等

  7. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  8. 面试突击之跨域问题的解决方案详解

    跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据。那怎么解决这个问题呢?接下来我们一起来看

  9. Mybatis-Plus接口BaseMapper与Services使用详解

    这篇文章主要为大家介绍了Mybatis-Plus接口BaseMapper与Services使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  10. mybatis-plus雪花算法增强idworker的实现

    今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部