当前位置: 首页 > 工具软件 > JNotify > 使用案例 >

使用jnotify、commons-io监控文件变化

刘德义
2023-12-01

概要:
使用JNotify和commons-io监控文件变化。
其中:
1.JNotify通过系统API获取文件变更事件,优点是性能好,响应速度快,缺点是需要依赖JNotify库;
2.commons-io通过轮询获取文件变更,优点是不需要额外库,缺点是性能较差,且随着文件数量增多,响应时间也会增加。

说明:
1.以下测试均在Windows 10系统下进行;
2.当报错找不到JNotify库("no jnotify_64bit in java.library.path")时,下载JNotify包(下载地址:https://sourceforge.net/projects/jnotify/files/jnotify/jnotify-0.94/),并将JNotify复制到以下任一目录:

  • System.getProperty("java.library.path") 下任一目录
  • 在Windows下,复制 jnotify_64bit.dll 到 C:\Windows

代码:

pom.xml:

        <!-- commons-io -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.11.0</version>
        </dependency>
        <!-- jnotify -->
        <dependency>
            <groupId>net.contentobjects.jnotify</groupId>
            <artifactId>jnotify</artifactId>
            <version>0.94</version>
        </dependency>

JNotify:

package com.example.study.monitor;

import net.contentobjects.jnotify.JNotify;
import net.contentobjects.jnotify.JNotifyListener;

import java.io.IOException;

/**
 * 监视系统文件变化
 * JNotify通过系统API获取文件变更事件
 */
public class JNotifyFileMonitor implements JNotifyListener {
    public static void main(String[] args) throws IOException {
        String path1 = "e:/test/001";
        String path2 = "e:/test/002";
        // 是否监视子目录
        boolean watchSubtree = true;
        // 需要监视的文件变更类型,此处用4个bit位表示,类似linux的文件访问权限
        int mask = JNotify.FILE_CREATED | JNotify.FILE_DELETED | JNotify.FILE_MODIFIED | JNotify.FILE_RENAMED;

        int watchId1 = JNotify.addWatch(path1, mask, watchSubtree, new JNotifyFileMonitor());
        System.out.println("watchId1 = " + watchId1 + ", 开始监视路径:" + path1);
        int watchId2 = JNotify.addWatch(path2, JNotify.FILE_ANY, false, new JNotifyFileMonitor());
        System.out.println("watchId2 = " + watchId2 + ", 开始监视路径:" + path2);
        while (true) {
        }
    }

    /**
     * 文件创建后调用
     *
     * @param watchId      监视Id
     * @param watchPath    被监视的最上层路径
     * @param relativePath 创建的文件相对watchPath的相对路径
     */
    @Override
    public void fileCreated(int watchId, String watchPath, String relativePath) {
        System.out.println(String.format("watchId = [%s], 被监视路径 = [%s], 创建文件 = [%s]",
                watchId, watchPath, relativePath));
    }

    /**
     * 文件删除后调用
     * 删除一个文件夹时:
     * 1.若放入回收站,则只有该文件夹的delete通知(从回收站删除时不再有delete通知);
     * 2.若不放入回收站直接删除,则该文件夹下所有文件和文件夹(包括子目录)都会有delete通知。
     *
     * @param watchId      监视Id
     * @param watchPath    被监视的最上层路径
     * @param relativePath 删除的文件相对watchPath的相对路径
     */
    @Override
    public void fileDeleted(int watchId, String watchPath, String relativePath) {
        System.out.println(String.format("watchId = [%s], 被监视路径 = [%s], 删除文件 = [%s]",
                watchId, watchPath, relativePath));
    }

    /**
     * 文件修改后调用
     * 当文件夹下的直接文件或文件夹发生【创建、重命名、删除】动作时,该文件夹也会发生变化;【修改】文件时文件夹不发生变化
     * 如:被监视的文件夹为/001, 其下有文件夹 ./123/456
     * 其中:
     * 1.创建文件夹  ./123/456/tmp
     * 2.创建文件  ./123/456/1.txt,
     * 3.重命名文件夹 ./123/456/tmp -> ./123/456/789
     * 4.重命名文件 ./123/456/1.txt -> ./123/456/2.txt
     * 5.删除文件夹 ./123/456/789
     * 6.删除文件 ./123/456/2.txt
     * 除了会触发对应路径的【创建、重命名、删除】事件外,还会触发文件夹 ./123/456 的【修改】事件
     * 但:
     * 1.修改文件 ./123/456/1.txt
     * 2.修改文件夹 ./123/456/789 (比如用touch 789对文件夹进行修改)
     * 只会触发对应路径的【修改】事件,不会触发 ./123/456 的【修改】事件
     *
     * @param watchId      监视Id
     * @param watchPath    被监视的最上层路径
     * @param relativePath 修改的文件相对watchPath的相对路径
     */
    @Override
    public void fileModified(int watchId, String watchPath, String relativePath) {
        System.out.println(String.format("watchId = [%s], 被监视路径 = [%s], 修改文件 = [%s]",
                watchId, watchPath, relativePath));
    }

    /**
     * 文件重命名后调用
     *
     * @param watchId         监视Id
     * @param watchPath       被监视的最上层路径
     * @param oldRelativePath 修改前文件名(相对watchPath的相对路径)
     * @param newRelativePath 修改后文件名(相对watchPath的相对路径)
     */
    @Override
    public void fileRenamed(int watchId, String watchPath, String oldRelativePath, String newRelativePath) {
        System.out.println(String.format("watchId = [%s], 被监视路径 = [%s], 文件重命名: [%s] -> [%s]",
                watchId, watchPath, oldRelativePath, newRelativePath));
    }
}

commons-io:

package com.example.study.monitor;

import org.apache.commons.io.monitor.FileAlterationListener;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;

import java.io.File;

public class CommonsIOFileMonitor implements FileAlterationListener {
    public static void main(String[] args) throws Exception {
        String path1 = "e:/test/001";
        String path2 = "e:/test/002";
        FileAlterationListener listener = new CommonsIOFileMonitor();
        FileAlterationObserver observer1 = new FileAlterationObserver(path1);
        FileAlterationObserver observer2 = new FileAlterationObserver(path2);
        observer1.addListener(listener);
        observer2.addListener(listener);
        // 构造函数的入参为轮询间隔(单位:ms)
        FileAlterationMonitor monitor = new FileAlterationMonitor(10000l);
        monitor.addObserver(observer1);
        monitor.addObserver(observer2);
        monitor.start();
    }

    @Override
    public void onStart(FileAlterationObserver fileAlterationObserver) {
        System.out.println("一轮轮询开始,被监视路径:" + fileAlterationObserver.getDirectory());
    }

    @Override
    public void onDirectoryCreate(File file) {
        System.out.println("创建文件夹:" + file.getPath());
    }

    @Override
    public void onDirectoryChange(File file) {
        System.out.println("修改文件夹:" + file.getPath());
    }

    @Override
    public void onDirectoryDelete(File file) {
        System.out.println("删除文件夹:" + file.getPath());
    }

    @Override
    public void onFileCreate(File file) {
        System.out.println("创建文件:" + file.getPath());
    }

    @Override
    public void onFileChange(File file) {
        System.out.println("修改文件:" + file.getPath());
    }

    @Override
    public void onFileDelete(File file) {
        System.out.println("删除文件:" + file.getPath());
    }

    @Override
    public void onStop(FileAlterationObserver fileAlterationObserver) {
        System.out.println("一轮轮询结束,被监视路径:" + fileAlterationObserver.getDirectory());
    }
}

commons-io轮询源码解读:

FileAlterationMonitor(仅展示关键代码):

    // 构造函数,默认轮询间隔为10s
    public FileAlterationMonitor() {
        this(10000L);
    }
    // 构造函数,设置轮询间隔,初始化观察者数组
    public FileAlterationMonitor(long interval) {
        this.observers = new CopyOnWriteArrayList();
        this.interval = interval;
    }
    // 启动线程,数据初始化
    public synchronized void start() throws Exception {
        if (this.running) {
            throw new IllegalStateException("Monitor is already running");
        } else {
            // 遍历观察者
            Iterator var1 = this.observers.iterator();
            while(var1.hasNext()) {
                FileAlterationObserver observer = (FileAlterationObserver)var1.next();
                // 观察者初始化,此处遍历所有文件
                observer.initialize();
            }
            this.running = true;
            if (this.threadFactory != null) {
                this.thread = this.threadFactory.newThread(this);
            } else {
                this.thread = new Thread(this);
            }
            // 启动监视线程
            this.thread.start();
        }
    }
    // 监视系统文件变化
    public void run() {
        while(true) {
            if (this.running) {
                // 遍历观察者
                Iterator var1 = this.observers.iterator();

                while(var1.hasNext()) {
                    FileAlterationObserver observer = (FileAlterationObserver)var1.next();
                    // 检测文件变化,并调用相关方法
                    observer.checkAndNotify();
                }

                if (this.running) {
                    try {
                        // 线程休眠设定的间隔时间
                        Thread.sleep(this.interval);
                    } catch (InterruptedException var3) {
                    }
                    continue;
                }
            }
            return;
        }
    }

FileAlterationObserver(仅展示关键代码):

    // 构造函数,文件名大小写敏感默认使用系统规则
    public FileAlterationObserver(String directoryName) {
        this(new File(directoryName));
    }
    public FileAlterationObserver(File directory) {
        this((File)directory, (FileFilter)null);
    }
    public FileAlterationObserver(File directory, FileFilter fileFilter) {
        this((File)directory, fileFilter, (IOCase)null);
    }
    public FileAlterationObserver(File directory, FileFilter fileFilter, IOCase caseSensitivity) {
        this(new FileEntry(directory), fileFilter, caseSensitivity);
    }
    protected FileAlterationObserver(FileEntry rootEntry, FileFilter fileFilter, IOCase caseSensitivity) {
        this.listeners = new CopyOnWriteArrayList();
        if (rootEntry == null) {
            throw new IllegalArgumentException("Root entry is missing");
        } else if (rootEntry.getFile() == null) {
            throw new IllegalArgumentException("Root directory is missing");
        } else {
            this.rootEntry = rootEntry;
            this.fileFilter = fileFilter;
            if (caseSensitivity != null && !caseSensitivity.equals(IOCase.SYSTEM)) {
                if (caseSensitivity.equals(IOCase.INSENSITIVE)) {
                    this.comparator = NameFileComparator.NAME_INSENSITIVE_COMPARATOR;
                } else {
                    this.comparator = NameFileComparator.NAME_COMPARATOR;
                }
            } else {
                this.comparator = NameFileComparator.NAME_SYSTEM_COMPARATOR;
            }

        }
    }
    // 观察者初始化
    public void initialize() throws Exception {
        // 获取当前文件(夹)的文件属性
        this.rootEntry.refresh(this.rootEntry.getFile());
        FileEntry[] children = this.doListFiles(this.rootEntry.getFile(), this.rootEntry);
        this.rootEntry.setChildren(children);
    }
    // 更新文件entity,并返回文件是否发生变化的结果(检测指标为:文件存在情况,文件最后修改时间,文件类型变更(文件夹或文件),文件大小)
    public boolean refresh(File file) {
        boolean origExists = this.exists;
        long origLastModified = this.lastModified;
        boolean origDirectory = this.directory;
        long origLength = this.length;
        this.name = file.getName();
        this.exists = Files.exists(file.toPath(), new LinkOption[0]);
        this.directory = this.exists && file.isDirectory();
        try {
            this.lastModified = this.exists ? FileUtils.lastModified(file) : 0L;
        } catch (IOException var9) {
            this.lastModified = 0L;
        }
        this.length = this.exists && !this.directory ? file.length() : 0L;
        return this.exists != origExists || this.lastModified != origLastModified || this.directory != origDirectory || this.length != origLength;
    }
    // 获取所有子文件(包括子目录)
    private FileEntry[] doListFiles(File file, FileEntry entry) {
        File[] files = this.listFiles(file);
        FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_FILE_ENTRY_ARRAY;
        for(int i = 0; i < files.length; ++i) {
            children[i] = this.createFileEntry(entry, files[i]);
        }
        return children;
    }
    // 获取子文件
    private File[] listFiles(File file) {
        File[] children = null;
        if (file.isDirectory()) {
            children = this.fileFilter == null ? file.listFiles() : file.listFiles(this.fileFilter);
        }
        if (children == null) {
            children = FileUtils.EMPTY_FILE_ARRAY;
        }
        if (this.comparator != null && children.length > 1) {
            Arrays.sort(children, this.comparator);
        }
        return children;
    }
    // 创建文件信息(包括获取子目录信息)
    private FileEntry createFileEntry(FileEntry parent, File file) {
        FileEntry entry = parent.newChildInstance(file);
        // 获取子目录下文件信息
        entry.refresh(file);
        FileEntry[] children = this.doListFiles(file, entry);
        entry.setChildren(children);
        return entry;
    }
    // 检测文件变化,并调用相关方法
    public void checkAndNotify() {
        Iterator var1 = this.listeners.iterator();
        while(var1.hasNext()) {
            FileAlterationListener listener = (FileAlterationListener)var1.next();
            listener.onStart(this);
        }
        // 检测文件变化
        File rootFile = this.rootEntry.getFile();
        if (rootFile.exists()) {
            this.checkAndNotify(this.rootEntry, this.rootEntry.getChildren(), this.listFiles(rootFile));
        } else if (this.rootEntry.isExists()) {
            this.checkAndNotify(this.rootEntry, this.rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
        }

        Iterator var5 = this.listeners.iterator();
        while(var5.hasNext()) {
            FileAlterationListener listener = (FileAlterationListener)var5.next();
            listener.onStop(this);
        }
    }
    // 检测文件变化
    private void checkAndNotify(FileEntry parent, FileEntry[] previous, File[] files) {
        int c = 0;
        FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_FILE_ENTRY_ARRAY;
        FileEntry[] var6 = previous;
        int var7 = previous.length;

        for(int var8 = 0; var8 < var7; ++var8) {
            FileEntry entry;
            for(entry = var6[var8]; c < files.length && this.comparator.compare(entry.getFile(), files[c]) > 0; ++c) {
                // 出现新文件(夹)时,执行文件(夹)创建后方法,doCreate根据文件类型决定调用onDirectoryCreate()或onFileCreate()
                current[c] = this.createFileEntry(parent, files[c]);
                this.doCreate(current[c]);
            }

            if (c < files.length && this.comparator.compare(entry.getFile(), files[c]) == 0) {
                this.doMatch(entry, files[c]);
                this.checkAndNotify(entry, entry.getChildren(), this.listFiles(files[c]));
                current[c] = entry;
                ++c;
            } else {
                // 删除文件(夹)时,执行文件(夹)删除后方法,doDelete根据文件类型决定调用onDirectoryDelete()或onFileDelete()
                this.checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
                this.doDelete(entry);
            }
        }
        while(c < files.length) {
            current[c] = this.createFileEntry(parent, files[c]);
            this.doCreate(current[c]);
            ++c;
        }
        parent.setChildren(current);
    }
    // 判断文件是否发生修改,并执行文件(夹)修改后方法
    private void doMatch(FileEntry entry, File file) {
        // 通过 refresh() 方法判断文件是否发生变化
        if (entry.refresh(file)) {
            Iterator var3 = this.listeners.iterator();
            while(var3.hasNext()) {
                FileAlterationListener listener = (FileAlterationListener)var3.next();
                if (entry.isDirectory()) {
                    listener.onDirectoryChange(file);
                } else {
                    listener.onFileChange(file);
                }
            }
        }
    }

参考:
1.https://www.cnblogs.com/songxingzhu/p/8963591.html

 类似资料: