监听日志文件变化的三种方法,推荐第三种!

更新日期: 2022-04-29阅读: 640标签: 文件

背景

在研究规则引擎时,如果规则以文件的形式存储,那么就需要监听指定的目录或文件来感知规则是否变化,进而进行加载。当然,在其他业务场景下,比如想实现配置文件的动态加载、日志文件的监听、FTP文件变动监听等都会遇到类似的场景。

本文给大家提供三种解决方案,并分析其中的利弊,建议收藏,以备不时之需。

方案一:定时任务 + File#lastModified

这个方案是最简单,最能直接想到的解决方案。通过定时任务,轮训查询文件的最后修改时间,与上一次进行对比。如果发生变化,则说明文件已经修改,进行重新加载或对应的业务逻辑处理。

这里再把实例代码贴出来:

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

对于文件低频变动的场景,这种方案实现简单,基本上可以满足需求。不过像上篇文章中提到的那样,需要注意Java 8和Java

9中File#lastModified的Bug问题。

但该方案如果用在文件目录的变化上,缺点就有些明显了,比如:操作频繁,效率都损耗在遍历、保存状态、对比状态上了,无法充分利用OS的功能。

方案二:WatchService

在Java 7中新增了java.nio.file.WatchService,通过它可以实现文件变动的监听。WatchService是基于操作系统的文件系统监控器,可以监控系统所有文件的变化,无需遍历、无需比较,是一种基于信号收发的监控,效率高。

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

上述demo展示了WatchService的基本使用方式,注解部分也说明了每个api的具体作用。

通过WatchService监听文件的类型也变得更加丰富:

  • ENTRY_CREATE 目标被创建
  • ENTRY_DELETE 目标被删除
  • ENTRY_MODIFY 目标被修改
  • OVERFLOW 一个特殊的Event,表示Event被放弃或者丢失

如果查看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系统。

方案三: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包下,基本使用流程如下:

  • 自定义文件监听类并继承 FileAlterationListenerAdaptor 实现对文件与目录的创建、修改、删除事件的处理;
  • 自定义文件监控类,通过指定目录创建一个观察者 FileAlterationObserver;
  • 向监视器添加文件系统观察器,并添加文件监听器;
  • 调用并执行。

第一步:创建文件监听器。根据需要在不同的方法内实现对应的业务逻辑处理。

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

第二步:封装一个文件监控的工具类,核心就是创建一个观察者FileAlterationObserver,将文件路径Path和监听器FileAlterationListener进行封装,然后交给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();

 }
}

第三步:调用并执行:

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

执行程序,会发现每隔1秒输入一次日志。当文件发生变更时,也会打印出对应的日志:

onStart
修改:/Users/zzs/temp/1.txt
onStop
onStart
onStop

当然,对应的监听时间间隔,可以通过在创建FileMonitor时进行修改。

该方案中监听器本身会启动一个线程定时处理。在每次运行时,都会先调用事件监听处理类的onStart方法,然后检查是否有变动,并调用对应事件的方法;比如,onChange文件内容改变,检查完后,再调用onStop方法,释放当前线程占用的CPU资源,等待下次间隔时间到了被再次唤醒运行。

监听器是基于文件目录为根源的,也可以可以设置过滤器,来实现对应文件变动的监听。过滤器的设置可查看FileAlterationObserver的构造方法:

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

小结

至此,基于Java实现监听文件变化的三种方案便介绍完毕。经过上述分析及实例,大家已经看到,并没有完美的解决方案,根据自己的业务情况及系统的容忍度可选择最适合的方案。

链接: https://www.fly63.com/article/detial/11619

h5移动端实现图片文件上传

PC端上传文件多半用插件,引入flash都没关系,但是移动端要是还用各种冗余的插件估计得被喷死,项目里面需要做图片上传的功能,既然H5已经有相关的接口且兼容性良好,当然优先考虑用H5来实现。

前端使用js读取文件操作

首先我们定义一个input标签type=file、然后我们定义一个jsReadFiles的方法将文件作为参数;当上传文件的时候读取这个文件。图片上传成功,只是图片路径变成了base64编码的形式。

HTML5实现文件读取、编辑、保存

HTML5读取文件主要利用的就是FileReader这个API,它的使用需要从一个构造函数开始,保存文件的关键是生成文件对象,可以使用URL.createObjectURL()方法实现,该方法能返回给定对象的URL,用在<a>标签的href属性上就可以创建可下载的文件链接。

血淋淋的事实告诉你:你为什么不应该在JS文件中保存敏感信息

在JavaScript文件中存储敏感数据,不仅是一种错误的实践方式,而且还是一种非常危险的行为,长期以来大家都知道这一点。

在js文件中引入另一个js文件的实现方法总汇

比如我写了一个JS文件,这个文件需要调用另外一个JS文件,该如何实现呢?这篇文章主要介绍:在js文件中引入另一个js文件的实现

使用HTML5来实现本地文件读取和写入

最近有这样一个需求,就是在HTML页面中有个按钮导出,点击它,将构造一个文档并存储到本地文件系统中。另外还有个按钮,点击它,从本地文件系统中读取一个文件并对内容进行分析。

lock文件_我们为什么需要 lock 文件

从 Yarn 横空出世推出 lock 文件以来,已经两年多时间了,npm 也在 5.0 版本加入了类似的功能,lock 文件越来越被开发者们接收和认可。本篇文章想从前端视角探讨一下我们为什么需要 lock 文件,以及它的一些成本与风险,当然其中一些观点对于后端也是适用的

什么是断点续传?前端如何实现文件的断点续传

什么是断点续传?就是下载文件时,不必重头开始下载,而是从指定的位置继续下载,这样的功能就叫做断点续传。前端通过FileList对象获取到相应的文件,按照指定的分割方式将大文件分段,然后一段一段地传给后端,后端再按顺序一段段将文件进行拼接。

form表单文件上传_multipart/form-data文件上传

form表单的enctype属性:规定了form表单数据在发送到服务器时候的编码方式.。application/x-www-form-urlencoded:默认编码方式,multipart/form-data:指定传输数据为二进制数据,例如图片、mp3、文件,text/plain:纯文本的传输。空格转换为“+”,但不支持特殊字符编码。

使用HttpClient发送文件流到服务器端

适用场景: 网络绝对路径的URL文件或图片,不存储到本地,转换成stream,直接使用HTTPClient传送到SpringBoot的服务端,将文件存储下来,并返回一个文件地址。目前分层架构的系统越来越多这种需求,所以记录下来以备不时之需。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!