Java中的文件更改侦听器

Posted

技术标签:

【中文标题】Java中的文件更改侦听器【英文标题】:File changed listener in Java 【发布时间】:2010-10-04 10:30:40 【问题描述】:

我希望在文件系统中的文件发生更改时收到通知。我只发现了一个轮询 lastModified File 属性的线程,显然这个解决方案不是最佳的。

【问题讨论】:

只是一个注释。当我们解决这个问题之前,我们发现大文件往往进来很慢,而轮询机制往往在它完全写入之前就发现了一个新的或更改的文件。为了解决这个问题,采用了“两口”解决方案。轮询器注意到文件已更改,但直到两次轮询看起来相同时才通知系统新/更改的文件:即已稳定。修复了很多badfile错误。 顺便说一句,Tomcat 在将大型 WAR 文件从远程源拖放到 webapps 文件夹时会遇到这个问题,并且已经这样做了很长时间。 我相信还有更优雅的方法可以做到这一点,但您可以保存文件的校验和,然后继续比较它。 【参考方案1】:

之前写过一个日志文件监控器,发现每秒轮询几次单个文件的属性对系统性能的影响其实很小。

Java 7 作为 NIO.2 的一部分添加了 WatchService API

WatchService API 专为需要通知文件更改事件的应用程序而设计。

【讨论】:

我将示例视为监视目录,但是单个文件呢? @ArchimedesTrajano API 会在目录中的文件发生更改时通知您。触发的事件包括已更改文件的名称。因此,您可以处理某个或多个文件的事件并忽略其他文件。 @ArchimedesTrajano ***.com/questions/16251273/…【参考方案2】:

我使用来自 Apache Commons 的 VFS API,下面是一个如何在不影响性能的情况下监控文件的示例:

DefaultFileMonitor

【讨论】:

【参考方案3】:

有一个名为jnotify 的库在Linux 上包装inotify,并且还支持Windows。没用过,不知道好不好用,不过值得一试。

【讨论】:

它运行完美,使用超级简单。我已经使用 inotify 很多年了。快速、稳定、可靠【参考方案4】:

自 JDK 1.7 起,通知应用程序文件更改的规范方法是使用 WatchService API。 WatchService 是事件驱动的。 official tutorial 提供了一个例子:

/*
 * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   - Neither the name of Oracle nor the names of its
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
import static java.nio.file.LinkOption.*;
import java.nio.file.attribute.*;
import java.io.*;
import java.util.*;

/**
 * Example to watch a directory (or tree) for changes to files.
 */

public class WatchDir 

    private final WatchService watcher;
    private final Map<WatchKey,Path> keys;
    private final boolean recursive;
    private boolean trace = false;

    @SuppressWarnings("unchecked")
    static <T> WatchEvent<T> cast(WatchEvent<?> event) 
        return (WatchEvent<T>)event;
    

    /**
     * Register the given directory with the WatchService
     */
    private void register(Path dir) throws IOException 
        WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
        if (trace) 
            Path prev = keys.get(key);
            if (prev == null) 
                System.out.format("register: %s\n", dir);
             else 
                if (!dir.equals(prev)) 
                    System.out.format("update: %s -> %s\n", prev, dir);
                
            
        
        keys.put(key, dir);
    

    /**
     * Register the given directory, and all its sub-directories, with the
     * WatchService.
     */
    private void registerAll(final Path start) throws IOException 
        // register directory and sub-directories
        Files.walkFileTree(start, new SimpleFileVisitor<Path>() 
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                throws IOException
            
                register(dir);
                return FileVisitResult.CONTINUE;
            
        );
    

    /**
     * Creates a WatchService and registers the given directory
     */
    WatchDir(Path dir, boolean recursive) throws IOException 
        this.watcher = FileSystems.getDefault().newWatchService();
        this.keys = new HashMap<WatchKey,Path>();
        this.recursive = recursive;

        if (recursive) 
            System.out.format("Scanning %s ...\n", dir);
            registerAll(dir);
            System.out.println("Done.");
         else 
            register(dir);
        

        // enable trace after initial registration
        this.trace = true;
    

    /**
     * Process all events for keys queued to the watcher
     */
    void processEvents() 
        for (;;) 

            // wait for key to be signalled
            WatchKey key;
            try 
                key = watcher.take();
             catch (InterruptedException x) 
                return;
            

            Path dir = keys.get(key);
            if (dir == null) 
                System.err.println("WatchKey not recognized!!");
                continue;
            

            for (WatchEvent<?> event: key.pollEvents()) 
                WatchEvent.Kind kind = event.kind();

                // TBD - provide example of how OVERFLOW event is handled
                if (kind == OVERFLOW) 
                    continue;
                

                // Context for directory entry event is the file name of entry
                WatchEvent<Path> ev = cast(event);
                Path name = ev.context();
                Path child = dir.resolve(name);

                // print out event
                System.out.format("%s: %s\n", event.kind().name(), child);

                // if directory is created, and watching recursively, then
                // register it and its sub-directories
                if (recursive && (kind == ENTRY_CREATE)) 
                    try 
                        if (Files.isDirectory(child, NOFOLLOW_LINKS)) 
                            registerAll(child);
                        
                     catch (IOException x) 
                        // ignore to keep sample readbale
                    
                
            

            // reset key and remove from set if directory no longer accessible
            boolean valid = key.reset();
            if (!valid) 
                keys.remove(key);

                // all directories are inaccessible
                if (keys.isEmpty()) 
                    break;
                
            
        
    

    static void usage() 
        System.err.println("usage: java WatchDir [-r] dir");
        System.exit(-1);
    

    public static void main(String[] args) throws IOException 
        // parse arguments
        if (args.length == 0 || args.length > 2)
            usage();
        boolean recursive = false;
        int dirArg = 0;
        if (args[0].equals("-r")) 
            if (args.length < 2)
                usage();
            recursive = true;
            dirArg++;
        

        // register directory and process its events
        Path dir = Paths.get(args[dirArg]);
        new WatchDir(dir, recursive).processEvents();
    

对于单个文件,存在各种解决方案,例如:

https://dzone.com/articles/listening-to-fileevents-with-java-nio

请注意,Apache VFS 使用轮询算法,尽管它可能提供更强大的功能。另请注意,API 不提供确定文件是否已关闭的方法。

【讨论】:

那个 API 过于复杂。如果强制您使用“无限”循环来实现/管理线程只是为了使用它。然后你必须提防所谓的“溢出”事件,这意味着你错过了一些东西,但谁知道呢。它实际上增加了程序员的工作,因为本机目录更改通知通常会带来过度的好处(如果文件系统甚至支持,否则无论如何它默认为内部轮询)。如果它遵循一个简单的侦听器模式,那就太好了。例如:WatchService.watch(File f, FileListener listener),其中 f 可以是目录或文件。太糟糕了。【参考方案5】:

Java commons-io 有一个FileAlterationObserver。它结合 FileAlterationMonitor 进行轮询。类似于commons VFS。优点是依赖少得多。

edit: 较少的依赖是不正确的,它们对于 VFS 是可选的。但它使用 java File 而不是 VFS 抽象层。

【讨论】:

【参考方案6】:

我每次去读取属性文件时都会运行这个sn-p代码,只有在我上次读取文件后被修改过时才会真正读取文件。希望这对某人有所帮助。

private long timeStamp;
private File file;

private boolean isFileUpdated( File file ) 
  this.file = file;
  this.timeStamp = file.lastModified();

  if( this.timeStamp != timeStamp ) 
    this.timeStamp = timeStamp;
    //Yes, file is updated
    return true;
  
  //No, file is not updated
  return false;

在 Log4J FileWatchdog 中使用了类似的方法。

【讨论】:

【参考方案7】:

“更多 NIO 功能”具有文件监视功能,其实现取决于底层操作系统。应该在JDK7中。

更新:已添加到 Java SE 7。Chris Janicki 提供了link to the relevant Java tutorial。

【讨论】:

您能否更具体一些或发布文档链接?似乎在这上面找不到任何东西... 好像是包java.nio.file。见tutorial。 docs.oracle.com/javase/tutorial/essential/io/notification.html【参考方案8】:

您可以使用 FileReader 监听文件更改。请看下面的例子

// File content change listener 
private String fname;
private Object lck = new Object();
... 
public void run()

    try
    
        BufferedReader br = new BufferedReader( new FileReader( fname ) );
        String s;
        StringBuilder buf = new StringBuilder();
        while( true )
        
            s = br.readLine();
            if( s == null )
            
                synchronized( lck )
                
                    lck.wait( 500 );
                
            
            else
            
               System.out.println( "s = " + s );
            

        
    
    catch( Exception e )
    
        e.printStackTrace();
    

【讨论】:

【参考方案9】:

如果您愿意花一些钱,JNIWrapper 是一个带有 Winpack 的有用库,您将能够获取某些文件的文件系统事件。不幸的是,只有窗户。

见https://www.teamdev.com/jniwrapper。

否则,诉诸本机代码并不总是一件坏事,尤其是当提供的最佳方案是针对本机事件的轮询机制时。

我注意到 Java 文件系统操作在某些计算机上可能会很慢,如果处理不当很容易影响应用程序的性能。

【讨论】:

【参考方案10】:

您也可以考虑使用 Apache Commons JCI(Java 编译器接口)。尽管此 API 似乎专注于类的动态编译,但它还在其 API 中包含用于监视文件更改的类。

示例: http://commons.apache.org/jci/usage.html

【讨论】:

【参考方案11】:

Spring Integration 提供了一个很好的机制来查看目录和文件:http://static.springsource.org/spring-integration/reference/htmlsingle/#files。很确定它是跨平台的(我在 mac、linux 和 windows 上使用过)。

【讨论】:

【参考方案12】:

有一个名为 JxFileWatcher 的用于文件和文件夹监视的商业跨桌面库。 它可以从这里下载: http://www.teamdev.com/jxfilewatcher/

您还可以在线查看它的实际效果: http://www.teamdev.com/jxfilewatcher/onlinedemo/

【讨论】:

【参考方案13】:

与其他答案类似,这就是我如何使用 File、Timer 和 TimerTask 让它作为后台线程以设定的间隔轮询运行。

import java.io.File;
import java.util.Timer;
import java.util.TimerTask;

public class FileModifiedWatcher

  private static File file;
  private static int pollingInterval;
  private static Timer fileWatcher;
  private static long lastReadTimeStamp = 0L;

  public static boolean init(String _file, int _pollingInterval)
  
    file =  new File(_file);
    pollingInterval = _pollingInterval; // In seconds

    watchFile();

    return true;
  

  private static void watchFile()
  
    if ( null == fileWatcher )
    
      System.out.println("START");

      fileWatcher = new Timer();

      fileWatcher.scheduleAtFixedRate(new TimerTask()
      
        @Override
        public void run()
        

          if ( file.lastModified() > lastReadTimeStamp )
          
            System.out.println("File Modified");
          

          lastReadTimeStamp = System.currentTimeMillis();
        
      , 0, 1000 * pollingInterval);
    

  

【讨论】:

【参考方案14】:

轮询上次修改的文件属性是一个简单而有效的解决方案。只需定义一个扩展我的FileChangedWatcher 的类并实现onModified() 方法:

import java.io.File;

public abstract class FileChangedWatcher

    private File file;

    public FileChangedWatcher(String filePath)
    
        file = new File(filePath);
    

    public void watch() throws InterruptedException
    
        long currentModifiedDate = file.lastModified();

        while (true)
        
            long newModifiedDate = file.lastModified();

            if (newModifiedDate != currentModifiedDate)
            
                currentModifiedDate = newModifiedDate;
                onModified();
            

            Thread.sleep(100);
        
    

    public String getFilePath()
    
        return file.getAbsolutePath();
    

    protected abstract void onModified();

【讨论】:

以上是关于Java中的文件更改侦听器的主要内容,如果未能解决你的问题,请参考以下文章

添加事件侦听器,在播放完声音后更改/删除类

Java Swing - 从鼠标侦听器更改 JComponent 不透明度

本地存储中的更改未触发事件侦听器

Java SWT 创建侦听器以更改标签的文本并返回数据

区分 Firestore 中的第一次查询快照和更改侦听器

Flutter - 使用提供程序包侦听 Firebase 用户的用户配置文件(Firestore)中的数据更改