关于java:Java实现监听文件变化的三种方法推荐第三种

41次阅读

共计 6812 个字符,预计需要花费 18 分钟才能阅读完成。

背景

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

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

计划一:定时工作 + File#lastModified

这个计划是最简略,最能间接想到的解决方案。通过定时工作,轮训查问文件的最初批改工夫,与上一次进行比照。如果发生变化,则阐明文件曾经批改,进行从新加载或对应的业务逻辑解决。

在上篇文章《JDK 的一个 Bug,监听文件变更要小心了》中曾经编写了具体的实例,并且也提出了其中的有余。

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

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 零碎,笔者没有进行其余操作系统的验证,如果你遇到相似的问题,可参考对应的文章,寻求解决方案:https://blog.csdn.net/claram/…。

计划三: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 实现监听文件变动的三种计划便介绍结束。通过上述剖析及实例,大家曾经看到,并没有完满的解决方案,依据本人的业务状况及零碎的容忍度可抉择最适宜的计划。而且,在此基础上能够新增一些其余的辅助措施,来防止具体计划中的不足之处。

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

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

技术交换:请分割博主微信号:zhuan2quan

正文完
 0