文章目录
一、前言
1、简介
在平时的开发过程中,会有很多场景需要实时监听文件的变化,如下:
-
通过实时监控 mysql 的 binlog 日志实现数据同步
-
修改配置文件后,希望系统可以实时感知
-
应用系统将日志写入文件中,日志监控系统可以实时抓取日志,分析日志内容并进行报警
-
类似 ide 工具,可以实时感知管理的工程下的文件变更
2、三种方法介绍
-
定时任务 + File#lastModified
-
WatchService
-
Apache Commons-IO
二、三种方法实现
1、定时任务 + File#lastModified
通过定时任务,轮训查询文件的最后修改时间,与上一次进行对比。如果发生变化,则说明文件已经修改,进行重新加载或对应的业务逻辑处理
对于文件低频变动的场景,这种方案实现简单,基本上可以满足需求。但该方案如果用在文件目录的变化上,缺点就有些明显了,比如:操作频繁,效率都损耗在遍历、保存状态、对比状态上了,无法充分利用OS的功能。
public class FileWatchDemo {
/**
* 上次更新时间
*/
public static long LAST_TIME = 0L;
public static void main(String[] args) throws Exception {
// 相对路径代表这个功能相同的目录下
String fileName = "static/test.json";
// 创建文件,仅为实例,实践中由其他程序触发文件的变更
createFile(fileName);
// 循环执行
while (true){
long timestamp = readLastModified(fileName);
if (timestamp != LAST_TIME) {
System.out.println("文件已被更新:" + timestamp);
LAST_TIME = timestamp;
// 重新加载,文件内容
} else {
System.out.println("文件未更新");
}
Thread.sleep(1000);
}
}
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();
}
}
同时该方案存在Bug:在Java8和9的某些版本下,lastModified方法返回时间戳并不是毫秒,而是秒,也就是说返回结果的后三位始终为0
2、WatchService
2.1 介绍
在Java 7中新增了
java.nio.file.WatchService
,通过它可以实现文件变动的监听。WatchService是基于操作系统的文件系统监控器,可以监控系统所有文件的变化,无需遍历、无需比较,是一种基于信号收发的监控,效率高
相对于方案一,实现起来简单,效率高。不足的地方也很明显,只能监听当前目录下的文件和目录,不能监视子目录。另外对于jdk8之后版本来说,该方案已经实现实时监听,不存在准实时的问题
2.2 简单示例
public class WatchServiceDemo {
public static void main(String[] args) throws IOException {
// 这里的监听必须是目录
Path path = Paths.get("static");
// 创建WatchService,它是对操作系统的文件监视器的封装,相对之前,不需要遍历文件目录,效率要高很多
WatchService watcher = FileSystems.getDefault().newWatchService();
// 注册指定目录使用的监听器,监视目录下文件的变化;
// PS:Path必须是目录,不能是文件;
// StandardWatchEventKinds.ENTRY_MODIFY,表示监视文件的修改事件
path.register(watcher, new WatchEvent.Kind[]{StandardWatchEventKinds.ENTRY_MODIFY},
SensitivityWatchEventModifier.LOW);
// 创建一个线程,等待目录下的文件发生变化
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();
}
}
}
2.3 完整示例
创建FileWatchedListener
接口
public interface FileWatchedListener {
void onCreated(WatchEvent<Path> watchEvent);
void onDeleted(WatchEvent<Path> watchEvent);
void onModified(WatchEvent<Path> watchEvent);
void onOverflowed(WatchEvent<Path> watchEvent);
}
创建FileWatchedAdapter
实现类,实现文件监听的方法
public class FileWatchedAdapter implements FileWatchedListener {
@Override
public void onCreated(WatchEvent<Path> watchEvent) {
Path fileName = watchEvent.context();
System.out.println(String.format("文件【%s】被创建,时间:%s", fileName, now()));
}
@Override
public void onDeleted(WatchEvent<Path> watchEvent) {
Path fileName = watchEvent.context();
System.out.println(String.format("文件【%s】被删除,时间:%s", fileName, now()));
}
@Override
public void onModified(WatchEvent<Path> watchEvent) {
Path fileName = watchEvent.context();
System.out.println(String.format("文件【%s】被修改,时间:%s", fileName, now()));
}
@Override
public void onOverflowed(WatchEvent<Path> watchEvent) {
Path fileName = watchEvent.context();
System.out.println(String.format("文件【%s】被丢弃,时间:%s", fileName, now()));
}
private String now(){
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
return dateFormat.format(Calendar.getInstance().getTime());
}
}
创建FileWatchedService
监听类,监听文件
public class FileWatchedService {
private WatchService watchService;
private FileWatchedListener listener;
/**
*
* @param path 要监听的目录,注意该 Path 只能是目录,否则会报错 java.nio.file.NotDirectoryException:
* @param listener 自定义的 listener,用来处理监听到的创建、修改、删除事件
* @throws IOException
*/
public FileWatchedService(Path path, FileWatchedListener listener) throws IOException {
watchService = FileSystems.getDefault().newWatchService();
path.register(watchService,
/// 监听文件创建事件
StandardWatchEventKinds.ENTRY_CREATE,
/// 监听文件删除事件
StandardWatchEventKinds.ENTRY_DELETE,
/// 监听文件修改事件
StandardWatchEventKinds.ENTRY_MODIFY);
this.listener = listener;
}
private void watch() throws InterruptedException {
while (true) {
WatchKey watchKey = watchService.take();
List<WatchEvent<?>> watchEventList = watchKey.pollEvents();
for (WatchEvent<?> watchEvent : watchEventList) {
WatchEvent.Kind<?> kind = watchEvent.kind();
WatchEvent<Path> curEvent = (WatchEvent<Path>) watchEvent;
if (kind == StandardWatchEventKinds.OVERFLOW) {
listener.onOverflowed(curEvent);
continue;
} else if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
listener.onCreated(curEvent);
continue;
} else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
listener.onModified(curEvent);
continue;
} else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
listener.onDeleted(curEvent);
continue;
}
}
/**
* WatchKey 有两个状态:
* {@link sun.nio.fs.AbstractWatchKey.State.READY ready} 就绪状态:表示可以监听事件
* {@link sun.nio.fs.AbstractWatchKey.State.SIGNALLED signalled} 有信息状态:表示已经监听到事件,不可以接续监听事件
* 每次处理完事件后,必须调用 reset 方法重置 watchKey 的状态为 ready,否则 watchKey 无法继续监听事件
*/
if (!watchKey.reset()) {
break;
}
}
}
public static void main(String[] args) {
try {
Path path = Paths.get("static");
FileWatchedService fileWatchedService = new FileWatchedService(path, new FileWatchedAdapter());
fileWatchedService.watch();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
3、Apache Commons-IO
3.1 介绍与环境准备
commons-io
对实现文件监听的实现位于org.apache.commons.io.monitor
包下,基本使用流程如下:
-
自定义文件监听类并继承
FileAlterationListenerAdaptor
实现对文件与目录的创建、修改、删除事件的处理; -
自定义文件监控类,通过指定目录创建一个观察者
FileAlterationObserver
; -
向监视器添加文件系统观察器,并添加文件监听器;
-
调用并执行。
<!--注意,不同的版本需要不同的JDK支持,2.7需要Java 8及以上版本-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
3.2 原理讲解
该方案中监听器本身会启动一个线程定时处理。在每次运行时,都会先调用事件监听处理类的onStart方法,然后检查是否有变动,并调用对应事件的方法;比如,onChange文件内容改变,检查完后,再调用onStop方法,释放当前线程占用的CPU资源,等待下次间隔时间到了被再次唤醒运行。
监听器是基于文件目录为根源的,也可以可以设置过滤器,来实现对应文件变动的监听。过滤器的设置可查看FileAlterationObserver
的构造方法:
public FileAlterationObserver(String directoryName, FileFilter fileFilter, IOCase caseSensitivity) {
this(new File(directoryName), fileFilter, caseSensitivity);
}
3.2 实战演示
创建文件监听器。根据需要在不同的方法内实现对应的业务逻辑处理
public class FileListener extends FileAlterationListenerAdaptor {
@Override
public void onStart(FileAlterationObserver observer) {
super.onStart(observer);
// System.out.println("一轮轮询开始,被监视路径:" + observer.getDirectory());
}
@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("一轮轮询结束,被监视路径:" + fileAlterationObserver.getDirectory());
}
}
封装一个文件监控的工具类,核心就是创建一个观察者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(10_000L);
fileMonitor.monitor("static", new FileListener());
fileMonitor.start();
}
}
参考文章
https://segmentfault.com/a/1190000041913336