Java implements three methods for monitoring file changes, and the third is recommended

background

When studying the rule engine, if the rules are stored in the form of files, it is necessary to monitor the specified directory or file to sense whether the rules have changed, and then load them. Of course, in other business scenarios, such as dynamic loading of configuration files, monitoring of log files, and monitoring of FTP file changes, similar scenarios will be encountered.

This article provides you with three solutions, analyzes the pros and cons, and recommends collections in case you need them.

Option 1: Scheduled task + File#lastModified

This solution is the simplest and most straightforward solution that comes to mind. Through timed tasks, the last modification time of the query file is polled and compared with the last time. If there is a change, it means that the file has been modified and needs to be reloaded or processed by the corresponding business logic.

In the previous article " A Bug in JDK, Be Careful When Monitoring File Changes ", a specific example has been written, and its shortcomings have also been proposed.

Paste the example code here:

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

For scenarios with low-frequency file changes, this solution is simple to implement and basically meets the requirements. However, as mentioned in the previous article, you need to pay attention to the bug of File#lastModified in Java 8 and Java 9.

However, if this scheme is used for file directory changes, the shortcomings are somewhat obvious. For example, the operation is frequent, and the efficiency is lost in traversing, saving the state, and comparing the state, and the functions of the OS cannot be fully utilized.

Option 2: WatchService

Added in Java 7 java.nio.file.WatchService, through which you can monitor file changes. WatchService is a file system monitor based on the operating system. It can monitor the changes of all files in the system without traversal or comparison. It is a monitoring based on signal sending and receiving, with high efficiency.

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

The above demo shows the basic usage of WatchService, and the annotation part also explains the specific role of each API.

The types of files monitored by WatchService have also become richer:

  • ENTRY_CREATE target is created
  • ENTRY_DELETE target deleted
  • ENTRY_MODIFY target modified
  • OVERFLOW A special Event that indicates that the Event was abandoned or lost

如果查看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包下,基本使用流程如下:

  • Customize the file monitoring class and inherit FileAlterationListenerAdaptorit to handle the creation, modification and deletion of files and directories;
  • Custom file monitoring class, create an observer by specifying a directory FileAlterationObserver;
  • Add a file system watcher to the monitor, and add a file listener;
  • call and execute.

Step 1: Create a file listener. Implement corresponding business logic processing in different methods as needed.

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

Step 2: Encapsulate a tool class for file monitoring. The core is to create an observer FileAlterationObserver, encapsulate the file path Path and the listener FileAlterationListener, and then hand it over to the 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();
  }
}
复制代码

Step 3: Call and execute:

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

Execute the program and you will find that the log is entered every 1 second. When the file is changed, the corresponding log is also printed:

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

Of course, the corresponding listening time interval can be modified when FileMonitor is created.

In this scheme, the listener itself will start a thread for timing processing. In each run, the onStart method of the event listener processing class will be called first, then check whether there is a change, and call the method corresponding to the event; for example, if the content of the onChange file changes, after checking, call the onStop method to release the current thread occupied CPU resources, wait for the next interval to be woken up and run again.

The listener is based on the file directory as the root, and you can also set up filters to monitor the corresponding file changes. The filter settings can be viewed in the constructor of FileAlterationObserver:

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

summary

So far, the three schemes for monitoring file changes based on Java have been introduced. After the above analysis and examples, you have seen that there is no perfect solution, and you can choose the most suitable solution according to your business situation and the tolerance of the system. Moreover, some other auxiliary measures can be added on this basis to avoid the deficiencies in the specific scheme.

Blogger Profile: Author of the technical book "SpringBoot Technology Insider", who loves to study technology and write technical articles.

Public account: "New Horizons of Programs", the blogger's public account, welcome to pay attention~

Technical exchange: please contact the blogger WeChat ID: zhuan2quan

Guess you like

Origin juejin.im/post/7103318602748526628