简介
1.1 前言
每当文件发生任何更改时,它们都会自动刷新——这是大多数应用程序中常见的非常普遍的问题。每个应用程序都有一些配置,预期该配置文件中的每次更改都会刷新。解决该问题的过去方法包括使用Thread,根据配置文件的“最后更新时间戳”定期轮询文件更改。Java 从 JDK1.7 开始引入了一项出色的功能:WatchService 类,可用于监控文件系统的变化。WatchService 看作是文件监控器,通过操作系统原生文件系统来运行,可以监控系统是所有文件的变化,这种监控是无需遍历、无需比较的,是一种基于信号收发的监控。
1.2 功能
WatchService是基于本机操作系统实现对文件的监控,动态获取文件变化,无需重启系统。
1.3 应用
- 比如系统中的配置文件,一般都是系统启动的时候只加载一次,利用WatchService可实现动态修改配置文件,无需重启系统。
- 监控磁盘中的文件变化,使用 WatchService 进行文件实时监控。
二、关键接口、类和方法
2.1 关键接口
- java.nio.file.WatchService:监听服务
- 是JDK的内部服务,它监视注册对象的更改,这些注册的对象必须是Watchable接口的实例。
- 扩展了Closeable接口,表示可以在需要时关闭服务。通常,应该使用JVM提供的关闭挂钩来完成。
public interface WatchService extends Closeable {
/**
*
*/
void close() throws IOException;
/**
* 尝试获取下一个监听结果,如果没有变化则返回null
*/
WatchKey poll();
/**
* 尝试获取下一个监听结果,最多等待指定时间,如果没有变化则返回null
*/
WatchKey poll(long timeout, TimeUnit unit) throws InterruptedException;
/**
* 等待下一次的监听结果,如果没有变化则一直等待
*/
WatchKey take() throws InterruptedException;
}
- 监听服务可通过下面方式获取:
FileSystems.getDefault().newWatchService();
- 如果需要长时间一直监控要用take,而如果只是在某个指定的时间监控则用poll
- java.nio.file.WatchEvent:监听事件
public interface WatchEvent<T> {
/**
* 监听事件的类型
*/
Kind<T> kind();
/**
* 事件的个数,大于1表示是一个重复事件
*/
int count();
/**
* 事件的上下文
* @return 返回触发该事件的那个文件或目录的路径(相对路径)
*/
T context();
}
- java.nio.file.WatchKey:监听key
- WatchKey 对象包含了文件变化的事件的属性集合,也就是所谓的监控信息池,当文件发生变化时所有的变化信息都会被导入监控池。
- 监控池是静态的,只有当你主动去获取新的监控池时才会有更新的内容加入监控池。这就造成了系统接收到监控信息事件可能稍长的问题。
public interface WatchKey {
/**
* 监听key是否合法,true:合法,false:不合法
*/
boolean isValid();
/**
* 拉取并删除位于这个监听key的监听事件列表(可能为空)
*/
List<WatchEvent<?>> pollEvents();
/**
* 监听key复位,复位后,新的pending事件才会再次进入监听,返回true:key合法且复位成功
* 每次调用 WatchService 的 take() 或 poll() 方法时需要通过本方法重置
*/
boolean reset();
/**
* 取消key的监听服务
*/
void cancel();
/**
* 返回该key关联的Watchable
*/
Watchable watchable();
}
- java.nio.file.Watchable:监听注册
public interface Watchable {
/**
* 监听注册,包含监听服务、监听事件、监听修改
*/
WatchKey register(WatchService watcher,WatchEvent.Kind<?>[] events,WatchEvent.Modifier... modifiers)
throws IOException;
/**
* 监听注册,包含监听服务和监听事件
*/
WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events) throws IOException;
}
2.2 关键类
- java.nio.file.FileSystems
方法 | 说明 |
---|---|
FileSystem getDefault() | 返回默认的文件系统, |
FileSystem getFileSystem(URI uri) | 返回一个指向已存在的FileSystem的引用。 如果找不到对应的已存在的FileSystem,则会抛出异常ProviderNotFoundException。 |
FileSystem newFileSystem(Path path,ClassLoader loader) | 构建一个文件系统去访问path指定的文件的内容。 |
FileSystem newFileSystem(URI uri, Map<String,?> env) | 根据URI创建一个信息文件系统。 如果找不到对应的已存在的FileSystem,则会抛出异常ProviderNotFoundException。 |
FileSystem newFileSystem(URI uri, Map<String,?> env, ClassLoader loader) | 同上。 |
-
java.nio.file.FileSystem
方法 | 说明 |
---|---|
Iterable getFileStores() | 返回一个Iterable,用来遍历该文件系统的各个FileStore |
Path getPath(String first, String... more) | 将字符串拼接成一个路径,并根据路径生成Path的实例 |
PathMatcher getPathMatcher(String syntaxAndPattern) | 返回一个 PathMatcher |
Iterable getRootDirectories() | 返回一个Iterable,用来遍历根目录的路径 |
String getSeparator() | 返回名称分隔符,字符串形式 |
boolean isOpen() | 判断文件系统是否已经打开了 |
boolean isReadOnly() | 判断这个文件系统的file stores是只读的 |
WatchService newWatchService() | 构建了一种新的 WatchService(可选操作) |
FileSystemProvider provider() | 返回创建此文件系统的提供者 |
void close() | 关闭此文件系统。如果文件系统已经关闭,则调用该方法没有效果 |
- java.nio.file.Path
- JDK1.7中定义的接口,主要用来在文件系统中定位文件,通常表示系统相关的文件路径。
- java中的Path表示文件系统的路径,可以指向文件或文件夹,也有相对路径和绝对路径之分。绝对路径表示从文件系统的根路径到文件或是文件夹的路径,而相对路径表示从特定路径下访问指定文件或文件夹的路径。
- 在很多方面,
java.nio.file.Path
和java.io.File
有相似性,但也有一些细微的差别。在很多情况下,可以用Path来代替File类。
方法 | 说明 |
---|---|
boolean endsWith(Path other) | |
boolean endsWith(String other) | |
Path getFileName() | 获取文件、目录或者其他类型文件的名称 |
FileSystem getFileSystem() | 获取文件系统 |
Path getName(int index) | 循环遍历每个元素的名字 |
int getNameCount() | 获取路径层级的个数 |
Path getParent() | 获取文件的父路径的Path实例 |
Path getRoot() | 获取根路径 |
boolean isAbsolute() | 判断是否是绝对路径,即根据该路径是否能定位到实际的文件。 需要注意的是,如果是绝对路径,即使文件不存在也会返回true |
Iterator iterator() | 返回迭代器,用来访问各级路径 |
Path normalize() | 规范化文件路径,去除路径中多余的部分,指向真正的路径目录地址 |
Path relativize(Path other) | 返回一个相对路径,是基于path的path1的相对路径 |
Path resolve(Path other) | 把当前路径当成父路径,把输入参数的路径当成子路径,得到一个新的路径。 |
Path resolve(String other) | 把当前路径当成父路径,把输入参数的路径当成子路径,得到一个新的路径。 |
Path resolveSibling(Path other) | 会根据给定的路径去替换当前的路径 |
Path resolveSibling(String other) | 会根据给定的路径去替换当前的路径 |
boolean startsWith(Path other) | |
boolean startsWith(String other) | |
Path subpath(int beginIndex, int endIndex) | 获取子路径 |
Path toAbsolutePath() | 由相对路径转换成绝对路径 |
File toFile() | 转换成File对象 |
Path toRealPath(LinkOption... options) | 转换成真实路径 |
URI toUri() |
- java.nio.file.Paths
- 是JDK1.7中定义的静态工具类,用来根据String格式的路径或者URI返回Path的实例
方法 | 说明 |
---|---|
Path get(URI uri) | 创建Path实例 |
Path get(String first, String... more) | 接受一个或多个字符串,字符串之间自动使用默认文件系统的路径分隔符连接起来。 (Unix是 /,Windows是 \ ) |
- 使用相对路径时,可以使用两种符号:
- .:表示当前路径
- ..: 表示父类目录
三、示例
3.1 注意的点
- Paths.get(path).register 方法只会监视 path 文件下的文件变化,其子目录的变化是不会监视的。
- 每次take()\poll()操作都会导致线程监控阻塞,每次操作文件可能需要长时间,如果监听目录下有其他事件发生,将会导致事件丢失。
- WatchKey 每次读完文件变化后,需要调用
reset()
方法才能继续读取变化。
3.2 示例
整个监控目录文件操作的流程大致如下:
- 获取 WatchService
- 注册指定目录的监视器 WatchService
- 等待目录下的文件发生变化
- 对发生变化的文件进行操作
下面示例展示监视目录和其子目录下的文件变化,并打印变化信息:
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.WatchEvent.Kind;
import java.util.*;
import static com.sun.jmx.mbeanserver.Util.cast;
/**
* 把今天最好的表现当作明天最新的起点..~
* <p>
* Today the best performance as tomorrow newest starter!
*
* @类描述: Java 从 JDK1.7 开始增加了 WatchService 类,可用于监控文件系统的变化。
* @author: <a href="mailto:duleilewuhen@sina.com">独泪了无痕</a>
* @创建时间: 2021-07-22 下午9:25
* @版本: V 1.0.1
* @since: JDK 1.8
*/
public final class WatchServiceHelper {
/**
* Java监听文件
*
* @param rootPath 监听的文件目录(只能监听目录)
*/
public static void watcherFile(String rootPath) throws Exception {
File root = new File(rootPath);
if (!root.isDirectory()) {
throw new Exception("只监视目录变化");
}
// WatchService 就类似一个文件监视器,使用如下方法创建一个监听服务
WatchService watcher = FileSystems.getDefault().newWatchService();
// 所有目录集合
Set<String> pathSet = new LinkedHashSet<>();
// 递归找子目录
loopDir(root, pathSet);
// 维护 WatchKey 与目录的映射,用于找到变化文件的目录。
Map<WatchKey, String> watchKeyPathMap = new HashMap<>();
// 创建完监视器后,需要将其绑定到某个目录,并指定监控哪些变化
Kind[] kinds = {
// 新增或目录重命名
StandardWatchEventKinds.ENTRY_CREATE,
// 修改
StandardWatchEventKinds.ENTRY_MODIFY,
// 删除或重命名
StandardWatchEventKinds.ENTRY_DELETE
};
// 遍历添加监视
for (String path : pathSet) {
// 监听注册,监听实体的创建、修改、删除事件
WatchKey key = Paths.get(path).register(watcher, kinds);
watchKeyPathMap.put(key, path);
}
while (true) {
// 获取下一个文件改动事件
WatchKey watchKey = watcher.take();
// 监听key为null,则跳过
if (watchKey == null) {
continue;
}
// 利用 key.pollEvents() 方法获取监听事件列表
List<WatchEvent<?>> watchEventList = watchKey.pollEvents();
// 获取监听事件
watchEventList.forEach(watchEvent -> {
// 获取监听事件类型
Kind kind = watchEvent.kind();
// 若是异常事件跳过
if (StandardWatchEventKinds.OVERFLOW != kind) {
// 获取监听的文件/目录的名称
Path path = cast(watchEvent.context());
// 输出事件类型、文件路径及名称
String msg = String.format("变化类型:%s,监视目录:%s,变化对象:%s",
kind.name(),
watchKeyPathMap.get(watchKey),
path.toString());
System.out.println(msg);
}
});
// 处理监听key后(即处理监听事件后),监听key需要复位,便于下次监听
boolean valid = watchKey.reset();
// 如果重设失败,退出监听
if (!valid) {
break;
}
}
// 增加jvm关闭的钩子来关闭监听
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
watcher.close();
} catch (IOException e) {
e.printStackTrace();
}
}));
}
/**
* 递归寻找子目录
*
* @param parent 父目录
* @param pathSet 路径集合
*/
private static void loopDir(File parent, Set<String> pathSet) {
if (!parent.isDirectory()) {
return;
}
pathSet.add(parent.getPath());
for (File child : Objects.requireNonNull(parent.listFiles())) {
loopDir(child, pathSet);
}
}
}