java学习-io-自定义文件变化监听(custom file change monitor)
简单设计思路:
- 文件变化实际就是 基于 java.io.File#lastModified (最新更新时间); PS:对于不同的操作系统底层对于文件变更可能会有不同的更新策略, 例如 mac下对于子目录下的变更并不会更新所有父级目录更新时间,对于目录的变更时间只针对于直接子级,对于间接子级的变更是不会有感知的
- 首先基于 java原生的 EventObject/Listener/Publisher 机制
- 对于Event(java.util.EventObject) 中的source实际就是java.io.File
- 对于EventListener(java.util.EventListener) 主要是利用 EventListener(必须)和java.util.Observer (观察者模式,但当前工具类从jdk9开始就被标记为废弃状态)
- 对于EventPublisher(自定义类)通过继承 java.util.Observable (主要是为了搭配Observer),通过监听event来触发事件执行
- 对于真正的FileMonitor(自定义类) 通过本地缓存文件相关变更时间数据,并利用定时异步线程主动拉取文件数据,通过历史缓存数据和最新数据进行对比来判断是否变更; 当发生变更时发布事件
- 通过注册提交listener以及file(event)来创建一个文件变化监控
对于上述执行逻辑为:
- 创建Monitor对象,并添加自定义 EventListener,对于添加自定义listener实际就是利用java.util.Observable#addObserver,来添加observer
- 提交事件源(要监听的文件/文件夹)
- 对于提交事件源首先是为了本地缓存文件(File),并记录初始变更时间
- 利用执行中的定时异步线程来主动调用File#lastModified获取最新更新时间, 对于File#lastModified实际会最终调用native 方法来获取物理机器中实际存储的文件数据
- 当对于历史数据和最新的数据 当发现文件发生变更后,利用EventPublisher来发布事件(file)
- 对于EventPublisher实际是利用java.util.Observable#update 发布事件(PS:需要先调用 java.util.Observable#setChange,否则当调用java.util.Observable#notifyObservers不会触发java.util.Observe#update),利用 java.util.Observe#update来触发EventListener(observer)对于event的处理逻辑
对于以上操作中的监听实际是利用的 主动pull 的操作,而非事件源主动push; 因此对于事件源的变更实际存在一定的延迟性以及资源浪费
"talk is cheap , show me the code"
/* * Copyright (c) 2020, guoxing, Co,. Ltd. All Rights Reserved */ package com.xingguo.io.filechange; import java.io.File; import java.util.EventObject; /** * FileChangeEvent * 文件变化事件监听 * * @author guoxing * @date 2020/12/6 3:25 PM * @since */ public class FileChangeEvent extends EventObject { public FileChangeEvent(File file) { super(file); } @Override public File getSource() { return (File) super.getSource(); } } /* * Copyright (c) 2020, guoxing, Co,. Ltd. All Rights Reserved */ package com.xingguo.io.filechange; import java.util.EventListener; import java.util.Observable; import java.util.Observer; import java.util.function.Consumer; /** * FileChangeListener * 文件变化监听器 * * @author guoxing * @date 2020/12/6 3:27 PM * @since */ @FunctionalInterface public interface FileChangeListener extends EventListener, Consumer<FileChangeEvent>, Observer // jdk9后就被标记为过期了 { void onFileChange(FileChangeEvent fileChangeEvent); @Override default void update(Observable o, Object arg) { if (arg instanceof FileChangeEvent) { onFileChange((FileChangeEvent) arg); } } @Override default void accept(FileChangeEvent fileChangeEvent) { onFileChange(fileChangeEvent); } } /* * Copyright (c) 2020, guoxing, Co,. Ltd. All Rights Reserved */ package com.xingguo.io.filechange; import java.io.File; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Observable; import java.util.stream.Stream; /** * FileChangePublisher * * @author guoxing * @date 2020/12/6 3:33 PM * @since */ public class FileChangePublisher extends Observable { /** * 这里利用Observe观察者模式进行相关的监听操作 */ /** * 添加文件更新监听器 * * @param fileChangeListener */ public void addFileChangeListener(FileChangeListener fileChangeListener) { super.addObserver(fileChangeListener); } /** * 发布事件 * * @param fileChangeEvent */ public void publish(FileChangeEvent fileChangeEvent) { super.setChanged(); super.notifyObservers(fileChangeEvent); } /** * 直接通过文件来创建一个{@link FileChangeEvent} * * @param file */ public void publish(File file) { publish(new FileChangeEvent(file)); } }
/* * Copyright (c) 2020, guoxing, Co,. Ltd. All Rights Reserved */ package com.xingguo.io.filechange; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; /** * FileMonitorDemo * 对于当前文件监听操作实际就是通过判断指定文件的`{@link File#lastModified()}来判断指定文件是否发生变更 * * @author guoxing * @date 2020/12/6 3:42 PM * @since */ public class FileMonitorDemo { /** * 缓存文件上一次变更时间 * 对于{@link File#compareTo(File)}中其通过 路径进行比较文件是否相同 * 对于{@link File#equals(Object)}也是利用了 {@link File#compareTo(File)} 来进行判断 * 因此可以将{@link File}作为key来保证文件的唯一性 */ private Map<File, Long> fileLastModifiedCache = new LinkedHashMap<>(); // 利用 定时线程实现定时监听 private static final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); // 创建文件变化事件发布 private FileChangePublisher fileChangePublisher = new FileChangePublisher(); /** * 添加文件变化监听器,要求至少有一个监听器 * * @param fileChangeListener * @param fileChangeListeners */ public void addFileChangeListener(FileChangeListener fileChangeListener, FileChangeListener... fileChangeListeners) { fileChangePublisher.addFileChangeListener(Objects.requireNonNull(fileChangeListener)); if (Objects.nonNull(fileChangeListeners)) { Stream.of(fileChangeListeners) .forEach(fileChangePublisher::addFileChangeListener); } } public void execute(File file) { Objects.requireNonNull(file); // 通过定时线程去探测文件变化,并不能保证及时性 executorService.scheduleAtFixedRate(() -> { // 在本地系统文件可能会不存在 Long elderLastModified = fileLastModifiedCache.get(file); long lastModified = file.lastModified(); // 如果文件最终修改时间发生变化,说明文件发生修改,则发布文件变更通知 if (Objects.isNull(elderLastModified) || elderLastModified < lastModified) { System.out.printf("elder:%s;last:%s\n", elderLastModified, lastModified); fileChangePublisher.publish(file); } // 新增/修改最新文件变更时间 fileLastModifiedCache.put(file, lastModified); }, 0, 5, TimeUnit.SECONDS); } public static void main(String[] args) { FileMonitorDemo fileMonitorDemo = new FileMonitorDemo(); fileMonitorDemo.addFileChangeListener(fileChangeEvent -> { System.out.println("处理文件变更事件:" + fileChangeEvent); }); String targetPath = fileMonitorDemo.getClass().getClassLoader().getResource("").getPath(); fileMonitorDemo.execute(new File(targetPath)); ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); String randomName = UUID.randomUUID().toString(); executorService.scheduleAtFixedRate(() -> { File file = new File(targetPath + "/" + randomName + ".text"); if (file.exists()) { file.delete(); System.out.println("删除文件"); } else { try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { fileOutputStream.flush(); System.out.println("生成文件"); } catch (IOException e) { e.printStackTrace(); } } }, 0, 10, TimeUnit.SECONDS); } }