监听文件修改的四种方法
遇到了监听配置文件是否被修改的需求,因功能规模小,没有加入 Apollo、config 等组件,所以得自己实现
1. 自行实现
第一想法是用定时任务去实现,下面是笔者的实现思路:FileModifyManager 来监听管理全部文件,要实现监听接口 FileListener 并传入给 FileModifyManager ,每当文件发生变化就调用监听接口的方法 doListener
1.1 FileListener
@FunctionalInterface
public interface FileListener {
void doListener();
}
看了 Hutool 文档才知道这种设计叫钩子函数,那笔者和 Hutool 作者思路也有相似之处
1.2 FileModifyManager
/**
* @author Howl
* @date 2022/01/15
*/
public class FileModifyManager {
// 存放监听的文件及 FileNodeRunnable 节点
private static ConcurrentHashMap<File, FileNodeRunnable> data = new ConcurrentHashMap<>(16);
// 线程池执行定时监听任务
private static ScheduledExecutorService service = Executors.newScheduledThreadPool(20);
// 单例模式--双重校验锁
private volatile static FileModifyManager instance = null;
private FileModifyManager() {
}
public static FileModifyManager getInstance() {
if (instance == null) {
synchronized (FileModifyManager.class) {
if (instance == null) {
instance = new FileModifyManager();
}
}
}
return instance;
}
// 开始监听,默认 10 秒监听一次
public FileModifyManager startWatch(File file, FileListener fileListener) throws Exception {
return startWatch(file, fileListener, 0, 1000 * 10, TimeUnit.MILLISECONDS);
}
public FileModifyManager startWatch(File file, FileListener fileListener, long delay, long period, TimeUnit timeUnit) throws Exception {
FileNodeRunnable fileNodeRunnable = addFile(file, fileListener);
ScheduledFuture<?> scheduledFuture = service.scheduleAtFixedRate(fileNodeRunnable, delay, period, timeUnit);
fileNodeRunnable.setFuture(scheduledFuture);
return instance;
}
// 停止监听
public FileModifyManager stopWatch(File file) {
return stopWatch(file, true);
}
public FileModifyManager stopWatch(File file, boolean mayInterruptIfRunning) {
FileNodeRunnable fileNodeRunnable = data.get(file);
fileNodeRunnable.getFuture().cancel(mayInterruptIfRunning);
removeFile(file);
return instance;
}
// 是否监听
public boolean isWatching(File file) {
return containsFile(file);
}
// 监听列表
public Set listWatching() {
return getFileList();
}
// 管理文件
private FileNodeRunnable addFile(File file, FileListener fileListener) throws Exception {
isFileExists(file);
FileNodeRunnable fileNodeRunnable = new FileNodeRunnable(file, fileListener, file.lastModified());
data.put(file, fileNodeRunnable);
return fileNodeRunnable;
}
private void removeFile(File file) {
data.remove(file);
}
private boolean containsFile(File file) {
return data.containsKey(file);
}
private Set getFileList() {
return data.keySet();
}
// 判断文件存在与否
private void isFileExists(File file) throws Exception {
if (!file.exists()) {
throw new Exception("文件或路径不存在");
}
}
// 文件节点及其定时任务
private class FileNodeRunnable implements Runnable {
private File file;
private long lastModifyTime;
private FileListener listener;
private ScheduledFuture future;
FileNodeRunnable(File file, FileListener listener, long lastModifyTime) {
this.file = file;
this.listener = listener;
this.lastModifyTime = lastModifyTime;
}
@Override
public void run() {
if (this.lastModifyTime != file.lastModified()) {
System.out.println(file.toString() + " lastModifyTime is " + this.lastModifyTime);
this.lastModifyTime = file.lastModified();
listener.doListener();
}
}
public ScheduledFuture getFuture() {
return future;
}
public void setFuture(ScheduledFuture future) {
this.future = future;
}
}
}
对外只暴露 startWatch、stopWatch、listWatching 三个方法,入参为 File 和 FileListener
Hutool 也是用了 HashMap 来存放对应的关系表,那笔者思路还是挺清晰的
1.3 使用案例
public class FileTest {
public static void main(String[] args) throws Exception {
File file1 = new File("C:\\Users\\Howl\\Desktop\\123.txt");
File file2 = new File("C:\\Users\\Howl\\Desktop\\1234.txt");
FileModifyManager manager = FileModifyManager.getInstance();
manager.startWatch(file1,() -> System.out.println("123.txt 文件改变了"))
.startWatch(file2,() -> System.out.println("1234.txr 文件改变了"));
}
}
2. WatchService
WatchService 是利用本机操作系统的文件系统来实现监控文件目录(监控目录),于 JDK1.7 引入的位于 NIO 包下的新机制,所以使用方式和 NIO 也很相似
JDK 自带的 watchService 的缺点是修改文件会触发两次事件,因操作系统有不同情况:
- 修改了文件的 meta 信息和日期
- 写时复制效果,即旧文件改名,并将内容复制到新建的文件里
watchService 只能监控本目录的内容,不能检测子目录里的内容,如需监控则遍历添加子目录
public class WatchServiceTest {
public static void main(String[] args) throws IOException, InterruptedException {
// 目录路径,不能输入文件否则报错
Path path = Paths.get("C:\\Users\\Howl\\Desktop");
// 获取监听服务
WatchService watchService = FileSystems.getDefault().newWatchService();
// 只注册修改事件(还有创建和删除)
path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
// 监听
while (true) {
// 获取监听到的事件 key
WatchKey watchKey = watchService.poll(3 * 1000, TimeUnit.MILLISECONDS);
// poll 的返回有可能为 null
if (watchKey == null) {
continue;
}
// 遍历这些事件
for (WatchEvent<?> watchEvent : watchKey.pollEvents()) {
if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
Path watchPath = (Path) watchEvent.context();
File watchFile = watchPath.toFile();
System.out.println(watchFile.toString() + "文件修改了");
}
}
// watchKey 复原,用于下次监听
watchKey.reset();
}
}
}
3. Hutool(推荐)
Hutool 是国人维护的工具集,使用别人的轮子,总比自己重复造轮子高效(但也要了解轮子的设计思路),hutool 底层还是使用 WatchService ,其解决了修改文件会触发两次事件,思路是在某个毫秒数范围内的修改视为同一个修改。还可以监控子目录,思路是递归遍历
3.1 添加依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.19</version>
</dependency>
参考文档 Hutool
3.2 示例
public class HutoolTest {
public static void main(String[] args) throws Exception {
File file = new File("C:\\Users\\Howl\\Desktop\\123.txt");
// 监听文件修改
WatchMonitor watchMonitor = WatchMonitor.create(file, WatchMonitor.ENTRY_MODIFY);
// 设置钩子函数
watchMonitor.setWatcher(new SimpleWatcher() {
@Override
public void onModify(WatchEvent<?> event, Path currentPath) {
System.out.println(((Path) event.context()).toFile().toString() + "修改了");
}
});
// 设置监听目录的最大深入,目录层级大于制定层级的变更将不被监听,默认只监听当前层级目录
watchMonitor.setMaxDepth(1);
// 启动监听
watchMonitor.start();
}
}
思路是继承 Thread 类,然后 run 方法一直循环监听 watchService 事件
4. commons-io
commons-io 是 Apache 提供的实现 I/O 操作的工具集
4.1 添加依赖
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
4.2 示例
稍微看了一下使用的是观察者模式
public class CommonsTest {
public static void main(String[] args) throws Exception {
// 也是只能写目录
String filePath = "C:\\Users\\Howl\\Desktop";
// 文件观察者
FileAlterationObserver observer = new FileAlterationObserver(filePath);
// 添加监听
observer.addListener(new FileAlterationListenerAdaptor() {
@Override
public void onFileChange(File file) {
System.out.println(file.toString() + "文件修改了");
}
});
// 监视器
FileAlterationMonitor monitor = new FileAlterationMonitor(10);
// 添加观察者
monitor.addObserver(observer);
// 启动线程
monitor.start();
}
}