• Java实现文件变化监听


    一、前言

    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();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    同时该方案存在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();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    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);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    创建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());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    创建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();
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    3、Apache Commons-IO

    3.1 介绍与环境准备

    commons-io对实现文件监听的实现位于org.apache.commons.io.monitor包下,基本使用流程如下:

    • 自定义文件监听类并继承 FileAlterationListenerAdaptor 实现对文件与目录的创建、修改、删除事件的处理;

    • 自定义文件监控类,通过指定目录创建一个观察者 FileAlterationObserver

    • 向监视器添加文件系统观察器,并添加文件监听器;

    • 调用并执行。

    
    <dependency>
        <groupId>commons-iogroupId>
        <artifactId>commons-ioartifactId>
        <version>2.11.0version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.2 原理讲解

    该方案中监听器本身会启动一个线程定时处理。在每次运行时,都会先调用事件监听处理类的onStart方法,然后检查是否有变动,并调用对应事件的方法;比如,onChange文件内容改变,检查完后,再调用onStop方法,释放当前线程占用的CPU资源,等待下次间隔时间到了被再次唤醒运行。

    监听器是基于文件目录为根源的,也可以可以设置过滤器,来实现对应文件变动的监听。过滤器的设置可查看FileAlterationObserver的构造方法:

    public FileAlterationObserver(String directoryName, FileFilter fileFilter, IOCase caseSensitivity) {
        this(new File(directoryName), fileFilter, caseSensitivity);
    }
    
    • 1
    • 2
    • 3

    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());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    封装一个文件监控的工具类,核心就是创建一个观察者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();
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    调用执行

    public class FileRunner {
    
        public static void main(String[] args) throws Exception {
            // 监控间隔
            FileMonitor fileMonitor = new FileMonitor(10_000L);
            fileMonitor.monitor("static", new FileListener());
            fileMonitor.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    参考文章

    https://segmentfault.com/a/1190000041913336

    https://blog.csdn.net/claram/article/details/97919664

    https://mp.weixin.qq.com/s/McM52HwV2e-uTWJt8qSdCw

  • 相关阅读:
    OpenFeign服务接口调用
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING) @JsonValue解释
    Error: impossible constraint in ‘asm‘
    Stream + 并行流 +Optional +接口的方法 +日期组件+重复注解及类型注解
    Java 线程的优先级
    Linux - 基本背景
    PyQt6 GUI界面设计和Nuitka包生成exe程序(全笔记)
    劳动节快乐!手写个核心价值观编码工具 - Python实现
    如何在idea中使用maven搭建tomcat环境
    kerberos认证相关概念和流程
  • 原文地址:https://blog.csdn.net/lemon_TT/article/details/126063377