Un error en JDK, tenga cuidado al monitorear los cambios de archivos

antecedentes

En algunos escenarios comerciales, necesitamos implementar la función de monitorear los cambios en el contenido del archivo por nosotros mismos, como monitorear si un archivo ha cambiado y volver a cargar el contenido del archivo cuando cambia.

Parece ser una función relativamente simple, pero bajo ciertas versiones de JDK, pueden aparecer errores inesperados.

Este artículo lo llevará a una implementación simple de una función correspondiente y analizará los errores correspondientes y las ventajas y desventajas.

Ideas iniciales de realización

Para monitorear los cambios de archivos y leer archivos, la idea simple es la siguiente:

  • Inicie un solo hilo y obtenga regularmente la marca de tiempo de la última actualización del archivo (unidad: milisegundos);
  • Compare la última marca de tiempo, si es inconsistente, significa que el archivo ha sido modificado y luego vuelva a cargarlo;

Aquí hay una demostración de una implementación de función simple (excluyendo la parte de la tarea cronometrada):

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();
  }
}

En el código anterior, primero cree un archivo (conveniente para la prueba), luego lea la hora de modificación del archivo dos veces y registre la última hora de modificación con LAST_TIME. Si la última hora de cambio del archivo no coincide con la última hora, se actualiza la hora de modificación y se realiza el procesamiento empresarial.

El bucle for dos veces en el código de muestra es para demostrar los dos casos de cambio y sin cambio. Ejecute el programa e imprima el registro de la siguiente manera:

文件已被更新:1653557504000
文件未更新

El resultado de la ejecución es el esperado.

Esta solución obviamente tiene dos desventajas:

  • Es imposible percibir los cambios de archivos en tiempo real, después de todo, hay una diferencia de tiempo en la rotación del programa;
  • La unidad de tiempo devuelta por lastModified es milisegundos.Si se producen dos cambios en el mismo milisegundo y la consulta de la tarea programada se encuentra entre los dos cambios, el último cambio no se puede percibir.

La primera desventaja tiene poco impacto en el negocio, la probabilidad de la segunda desventaja es relativamente pequeña y puede ignorarse;

Debut del error JDK

El código anterior está implementado, no hay problema en circunstancias normales, pero si la versión de Java que está utilizando es 8 o 9, pueden ocurrir errores inesperados, que son causados ​​​​por los errores del propio JDK.

El error con el número JDK-8177809 se describe de la siguiente manera:

JDK-8177809

La dirección del error es: bugs.java.com/bugdatabase…

La descripción básica de este error es: en algunas versiones de Java 8 y 9, la marca de tiempo devuelta por el método lastModified no es milisegundos, sino segundos, lo que significa que los últimos tres dígitos del resultado devuelto siempre son 0.

Escribamos un programa para verificar:

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?

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

Supongo que te gusta

Origin juejin.im/post/7117253921130676232
Recomendado
Clasificación