Java implementa tres métodos para monitorear los cambios de archivos, y se recomienda el tercero

antecedentes

Al estudiar el motor de reglas, si las reglas se almacenan en forma de archivos, es necesario monitorear el directorio o archivo especificado para detectar si las reglas han cambiado y luego cargarlas. Por supuesto, en otros escenarios comerciales, como la carga dinámica de archivos de configuración, el monitoreo de archivos de registro y el monitoreo de cambios en archivos FTP, se encontrarán escenarios similares.

Este artículo le brinda tres soluciones, analiza los pros y los contras y recomienda colecciones para emergencias.

Opción 1: Tarea programada + Archivo#últimamodificación

Esta solución es la solución más simple y directa que se me ocurre. A través de tareas cronometradas, se sondea la hora de la última modificación del archivo de consulta y se compara con la última hora. Si hay un cambio, significa que el archivo ha sido modificado y necesita ser recargado o procesado con la lógica de negocios correspondiente.

En el artículo anterior " Un error en JDK, tenga cuidado al monitorear los cambios de archivos ", se ha escrito un ejemplo específico y también se han planteado sus deficiencias.

Pegue el código de ejemplo aquí:

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();
 }
}
复制代码

Para escenarios con cambios de archivos de baja frecuencia, esta solución es simple de implementar y básicamente cumple con los requisitos. Sin embargo, como se mencionó en el artículo anterior, debe prestar atención al error de File#lastModified en Java 8 y Java 9.

Sin embargo, si este esquema se usa para cambios de directorio de archivos, las deficiencias son algo obvias. Por ejemplo, la operación es frecuente y la eficiencia se pierde al atravesar, guardar el estado y comparar el estado, y las funciones del sistema operativo no pueden ser completamente utilizado.

Opción 2: WatchService

Agregado en Java 7 java.nio.file.WatchService, a través del cual puede monitorear los cambios de archivos. WatchService es un monitor de sistema de archivos basado en el sistema operativo. Puede monitorear los cambios de todos los archivos en el sistema sin recorrido ni comparación. Es un monitoreo basado en el envío y recepción de señales, con alta eficiencia.

public class WatchServiceDemo {
  public static void main(String[] args) throws IOException {
    // 这里的监听必须是目录
    Path path = Paths.get("/Users/zzs/temp/");
    // 创建WatchService,它是对操作系统的文件监视器的封装,相对之前,不需要遍历文件目录,效率要高很多
    WatchService watcher = FileSystems.getDefault().newWatchService();
    // 注册指定目录使用的监听器,监视目录下文件的变化;
    // PS:Path必须是目录,不能是文件;
    // StandardWatchEventKinds.ENTRY_MODIFY,表示监视文件的修改事件
    path.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
    // 创建一个线程,等待目录下的文件发生变化
    try {
      while (true) {
        // 获取目录的变化:
        // take()是一个阻塞方法,会等待监视器发出的信号才返回。
        // 还可以使用watcher.poll()方法,非阻塞方法,会立即返回当时监视器中是否有信号。
        // 返回结果WatchKey,是一个单例对象,与前面的register方法返回的实例是同一个;
        WatchKey key = watcher.take();
        // 处理文件变化事件:
        // key.pollEvents()用于获取文件变化事件,只能获取一次,不能重复获取,类似队列的形式。
        for (WatchEvent<?> event : key.pollEvents()) {
          // event.kind():事件类型
          if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
            //事件可能lost or discarded
            continue;
          }
          // 返回触发事件的文件或目录的路径(相对路径)
          Path fileName = (Path) event.context();
          System.out.println("文件更新: " + fileName);
        }
        // 每次调用WatchService的take()或poll()方法时需要通过本方法重置
        if (!key.reset()) {
          break;
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
复制代码

La demostración anterior muestra el uso básico de WatchService y la parte de la anotación también explica la función específica de cada API.

Los tipos de archivos monitoreados por WatchService también se han enriquecido:

  • Se crea el objetivo ENTRY_CREATE
  • ENTRY_DELETE destino eliminado
  • ENTRY_MODIFY destino modificado
  • DESBORDAMIENTO Un Evento especial que indica que el Evento fue abandonado o perdido

如果查看WatchService实现类(PollingWatchService)的源码,会发现,本质上就是开启了一个独立的线程来监控文件的变化:

PollingWatchService() {
        // TBD: Make the number of threads configurable
        scheduledExecutor = Executors
            .newSingleThreadScheduledExecutor(new ThreadFactory() {
                 @Override
                 public Thread newThread(Runnable r) {
                     Thread t = new Thread(null, r, "FileSystemWatcher", 0, false);
                     t.setDaemon(true);
                     return t;
                 }});
    }
复制代码

也就是说,本来需要我们手动实现的部分,也由WatchService内部帮我们完成了。

如果你编写一个demo,进行验证时,会很明显的感觉到WatchService监控文件的变化并不是实时的,有时候要等几秒才监听到文件的变化。以实现类PollingWatchService为例,查看源码,可以看到如下代码:

void enable(Set<? extends Kind<?>> var1, long var2) {
            synchronized(this) {
                this.events = var1;
                Runnable var5 = new Runnable() {
                    public void run() {
                        PollingWatchKey.this.poll();
                    }
                };
                this.poller = PollingWatchService.this.scheduledExecutor.scheduleAtFixedRate(var5, var2, var2, TimeUnit.SECONDS);
            }
        }
复制代码

也就是说监听器由按照固定时间间隔的调度器来控制的,而这个时间间隔在SensitivityWatchEventModifier类中定义:

public enum SensitivityWatchEventModifier implements Modifier {
    HIGH(2),
    MEDIUM(10),
    LOW(30);
    // ...
}
复制代码

该类提供了3个级别的时间间隔,分别为2秒、10秒、30秒,默认值为10秒。这个时间间隔可以在path#register时进行传递:

path.register(watcher, new WatchEvent.Kind[]{StandardWatchEventKinds.ENTRY_MODIFY},
        SensitivityWatchEventModifier.HIGH);
复制代码

相对于方案一,实现起来简单,效率高。不足的地方也很明显,只能监听当前目录下的文件和目录,不能监视子目录,而且我们也看到监听只能算是准实时的,而且监听时间只能取API默认提供的三个值。

该API在Stack Overflow上也有人提出Java 7在Mac OS下有延迟的问题,甚至涉及到Windows和Linux系统,笔者没有进行其他操作系统的验证,如果你遇到类似的问题,可参考对应的文章,寻求解决方案:blog.csdn.net/claram/arti…

方案三:Apache Commons-IO

方案一我们自己来实现,方案二借助于JDK的API来实现,方案三便是借助于开源的框架来实现,这就是几乎每个项目都会引入的commons-io类库。

引入相应依赖:

<dependency>
  <groupId>commons-io</groupId>
  <artifactId>commons-io</artifactId>
  <version>2.7</version>
</dependency>
复制代码

注意,不同的版本需要不同的JDK支持,2.7需要Java 8及以上版本。

commons-io对实现文件监听的实现位于org.apache.commons.io.monitor包下,基本使用流程如下:

  • Personaliza la clase de monitoreo de archivos y FileAlterationListenerAdaptorheredala para manejar la creación, modificación y eliminación de archivos y directorios;
  • Clase de monitoreo de archivos personalizada, cree un observador especificando un directorio FileAlterationObserver;
  • Agregue un observador del sistema de archivos al monitor y agregue un detector de archivos;
  • llamar y ejecutar.

Paso 1: Cree un detector de archivos. Implemente el procesamiento de lógica empresarial correspondiente en diferentes métodos según sea necesario.

public class FileListener extends FileAlterationListenerAdaptor {
  @Override
  public void onStart(FileAlterationObserver observer) {
    super.onStart(observer);
    System.out.println("onStart");
  }
  @Override
  public void onDirectoryCreate(File directory) {
    System.out.println("新建:" + directory.getAbsolutePath());
  }
  @Override
  public void onDirectoryChange(File directory) {
    System.out.println("修改:" + directory.getAbsolutePath());
  }
  @Override
  public void onDirectoryDelete(File directory) {
    System.out.println("删除:" + directory.getAbsolutePath());
  }
  @Override
  public void onFileCreate(File file) {
    String compressedPath = file.getAbsolutePath();
    System.out.println("新建:" + compressedPath);
    if (file.canRead()) {
      // TODO 读取或重新加载文件内容
      System.out.println("文件变更,进行处理");
    }
  }
  @Override
  public void onFileChange(File file) {
    String compressedPath = file.getAbsolutePath();
    System.out.println("修改:" + compressedPath);
  }
  @Override
  public void onFileDelete(File file) {
    System.out.println("删除:" + file.getAbsolutePath());
  }
  @Override
  public void onStop(FileAlterationObserver observer) {
    super.onStop(observer);
    System.out.println("onStop");
  }
}
复制代码

Paso 2: encapsule una clase de herramienta para el monitoreo de archivos. El núcleo es crear un observador FileAlterationObserver, encapsule la ruta del archivo Path y el oyente FileAlterationListener, y luego transfiéralo al FileAlterationMonitor.

public class FileMonitor {
  private FileAlterationMonitor monitor;
  public FileMonitor(long interval) {
    monitor = new FileAlterationMonitor(interval);
  }
  /**
   * 给文件添加监听
   *
   * @param path     文件路径
   * @param listener 文件监听器
   */
  public void monitor(String path, FileAlterationListener listener) {
    FileAlterationObserver observer = new FileAlterationObserver(new File(path));
    monitor.addObserver(observer);
    observer.addListener(listener);
  }
  public void stop() throws Exception {
    monitor.stop();
  }
  public void start() throws Exception {
    monitor.start();
  }
}
复制代码

Paso 3: Llame y ejecute:

public class FileRunner {
  public static void main(String[] args) throws Exception {
    FileMonitor fileMonitor = new FileMonitor(1000);
    fileMonitor.monitor("/Users/zzs/temp/", new FileListener());
    fileMonitor.start();
  }
}
复制代码

Ejecute el programa y encontrará que el registro se ingresa cada 1 segundo. Cuando se cambia el archivo, también se imprime el registro correspondiente:

onStart
修改:/Users/zzs/temp/1.txt
onStop
onStart
onStop
复制代码

Por supuesto, el intervalo de tiempo de escucha correspondiente se puede modificar cuando se crea FileMonitor.

En este esquema, el propio oyente iniciará un hilo para el procesamiento de tiempo. En cada ejecución, primero se llamará al método onStart de la clase de procesamiento del detector de eventos, luego se verificará si hay un cambio y se llamará al método correspondiente al evento; por ejemplo, si el contenido del archivo onChange cambia, después de verificar, llame al método onStop para liberar los recursos de CPU ocupados por el subproceso actual, espere a que el siguiente intervalo de tiempo se active nuevamente para ejecutarse.

El oyente se basa en el directorio de archivos como raíz, y también puede configurar filtros para monitorear los cambios de archivos correspondientes. La configuración del filtro se puede ver en el constructor de FileAlterationObserver:

public FileAlterationObserver(String directoryName, FileFilter fileFilter, IOCase caseSensitivity) {
    this(new File(directoryName), fileFilter, caseSensitivity);
}
复制代码

resumen

Hasta ahora, se han introducido los tres esquemas para monitorear cambios de archivos basados ​​en Java. Después del análisis y los ejemplos anteriores, ha visto que no existe una solución perfecta y puede elegir la solución más adecuada de acuerdo con la situación de su negocio y la tolerancia del sistema. Además, sobre esta base se pueden añadir algunas otras medidas auxiliares para evitar las deficiencias del régimen específico.

Perfil de Blogger: Autor del libro técnico "SpringBoot Technology Insider", a quien le encanta estudiar tecnología y escribir artículos técnicos.

Cuenta pública: "Program New Vision", la cuenta pública del blogger, bienvenido a prestar atención ~

Intercambio técnico: comuníquese con el bloguero WeChat ID: zhuan2quan

Supongo que te gusta

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