我可以使用WatchService观察单个文件的更改吗?(而不是整个目录)

87

当我尝试注册文件而不是目录时,会抛出java.nio.file.NotDirectoryException。我能监听单个文件的更改,而不是整个目录吗?


7
Javadoc:“在此版本中,此路径定位于存在的目录。”==> 因此答案是“不,您不能注册文件”。然后:“该目录已使用监视服务注册,以便可以监视目录中的条目。” ==> 因此,注册目录实际上是在监视目录条目上的事件,而不是在目录本身上。事件的名称提醒了它们所涉及的内容,它们以ENTRY_开头,如“ENTRY_MODIFY-修改目录中的条目”。所选答案提供了使用事件的详细信息。 - mins
8个回答

114

只需为您想要的目录中的文件过滤事件:

final Path path = FileSystems.getDefault().getPath(System.getProperty("user.home"), "Desktop");
System.out.println(path);
try (final WatchService watchService = FileSystems.getDefault().newWatchService()) {
    final WatchKey watchKey = path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
    while (true) {
        final WatchKey wk = watchService.take();
        for (WatchEvent<?> event : wk.pollEvents()) {
            //we only register "ENTRY_MODIFY" so the context is always a Path.
            final Path changed = (Path) event.context();
            System.out.println(changed);
            if (changed.endsWith("myFile.txt")) {
                System.out.println("My file has changed");
            }
        }
        // reset the key
        boolean valid = wk.reset();
        if (!valid) {
            System.out.println("Key has been unregisterede");
        }
    }
}

在这里我们检查修改的文件是否为“myFile.txt”,如果是,则执行相应的操作。


3
不要忘记测试事件是否为“OVERFLOW”。您不需要注册此事件。示例在此处 - Venkata Raju
3
这里存在一个未明说的陷阱,即某些平台实现的观察程序可能会锁定您放置观察程序的目录。 - Hakanai
11
final WatchKey watchKey - 这是用来干什么的?这个变量后面似乎没有被使用。 - mvmn
1
确保不要尝试在资源目录中查看文件。虽然它似乎在工作,但实际上并非如此,因为文件变化时,资源不会自动重新加载。 - Nick
关于@mvmn的问题,我也想知道那个变量是干什么用的,所以对我来说唯一合理的答案是,上面的代码是从更大的东西中提取出来并进行了一些修改,只是为了这个例子而已,并且那部分 final WatchKey watchKey = 没有被删除。最重要的是该路径已经使用watchservice进行了注册 path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY); - Karol K
显示剩余3条评论

28

其他答案都是正确的,你必须监视一个目录并过滤你需要的文件。然而,你可能希望在后台运行一个线程。接受的答案可以在watchService.take();上无限期阻塞,并且没有关闭watchService。适用于独立线程的解决方案可能如下:

public class FileWatcher extends Thread {
    private final File file;
    private AtomicBoolean stop = new AtomicBoolean(false);

    public FileWatcher(File file) {
        this.file = file;
    }

    public boolean isStopped() { return stop.get(); }
    public void stopThread() { stop.set(true); }

    public void doOnChange() {
        // Do whatever action you want here
    }

    @Override
    public void run() {
        try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
            Path path = file.toPath().getParent();
            path.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
            while (!isStopped()) {
                WatchKey key;
                try { key = watcher.poll(25, TimeUnit.MILLISECONDS); }
                catch (InterruptedException e) { return; }
                if (key == null) { Thread.yield(); continue; }

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

                    @SuppressWarnings("unchecked")
                    WatchEvent<Path> ev = (WatchEvent<Path>) event;
                    Path filename = ev.context();

                    if (kind == StandardWatchEventKinds.OVERFLOW) {
                        Thread.yield();
                        continue;
                    } else if (kind == java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY
                            && filename.toString().equals(file.getName())) {
                        doOnChange();
                    }
                    boolean valid = key.reset();
                    if (!valid) { break; }
                }
                Thread.yield();
            }
        } catch (Throwable e) {
            // Log or rethrow the error
        }
    }
}

我尝试了这个被接受的答案和这篇文章。你应该能够使用这个线程,使用new FileWatcher(new File("/home/me/myfile")).start()启动它,并通过在线程上调用stopThread()来停止它。


2
请记住,非守护线程会阻止JVM退出,因此根据您希望应用程序如何运行,在调用run()之前调用setDaemon(boolean) - Chris H.
如果有人收到修改事件两次,这里有一些解决方法:https://dev59.com/nmQn5IYBdhLWcg3wg3WR - Jehad Nasser
您可能还想降低线程的优先级。 - dan1st

21

不,不可能注册一个文件,监视服务不是这样工作的。但是,注册一个目录实际上会监视目录子项(文件和子目录)的更改,而不是目录本身的更改。

如果要监视一个文件,则需要使用监视服务注册包含该文件的目录。Path.register() documentation中写道:

WatchKey java.nio.file.Path.register(WatchService watcher, Kind[] events, Modifier... modifiers) throws IOException

注册此路径指向的文件与监视服务。

在此版本中,此路径定位到一个已存在的目录。 该目录被注册到监视服务中,以便可以监视目录中的条目的更改

然后,您需要处理条目上的事件,并通过检查事件的上下文值来检测与您感兴趣的文件相关的事件。上下文值代表条目的名称(实际上是相对于其父级路径的路径,即子项名称)。这里有一个 示例

11

Apache提供了一个FileWatchdog类,其中包含一个doOnChange方法。

private class SomeWatchFile extends FileWatchdog {

    protected SomeWatchFile(String filename) {
        super(filename);
    }

    @Override
    protected void doOnChange() {
        fileChanged= true;
    }

}

无论你想要在哪里开始这个主题:

SomeWatchFile someWatchFile = new SomeWatchFile (path);
someWatchFile.start();

FileWatchDog类轮询文件的lastModified()时间戳。Java NIO的本机WatchService更高效,因为通知是立即的。


9
想知道FileWatchdog类来自哪个库? - szydan
1
从log4j - org.apache.log4j.helpers - idog

9

您无法直接观看单个文件,但您可以过滤掉不需要的内容。

这是我的FileWatcher类实现:

import java.io.File;
import java.nio.file.*;
import java.nio.file.WatchEvent.Kind;

import static java.nio.file.StandardWatchEventKinds.*;

public abstract class FileWatcher
{
    private Path folderPath;
    private String watchFile;

    public FileWatcher(String watchFile)
    {
        Path filePath = Paths.get(watchFile);

        boolean isRegularFile = Files.isRegularFile(filePath);

        if (!isRegularFile)
        {
            // Do not allow this to be a folder since we want to watch files
            throw new IllegalArgumentException(watchFile + " is not a regular file");
        }

        // This is always a folder
        folderPath = filePath.getParent();

        // Keep this relative to the watched folder
        this.watchFile = watchFile.replace(folderPath.toString() + File.separator, "");
    }

    public void watchFile() throws Exception
    {
        // We obtain the file system of the Path
        FileSystem fileSystem = folderPath.getFileSystem();

        // We create the new WatchService using the try-with-resources block
        try (WatchService service = fileSystem.newWatchService())
        {
            // We watch for modification events
            folderPath.register(service, ENTRY_MODIFY);

            // Start the infinite polling loop
            while (true)
            {
                // Wait for the next event
                WatchKey watchKey = service.take();

                for (WatchEvent<?> watchEvent : watchKey.pollEvents())
                {
                    // Get the type of the event
                    Kind<?> kind = watchEvent.kind();

                    if (kind == ENTRY_MODIFY)
                    {
                        Path watchEventPath = (Path) watchEvent.context();

                        // Call this if the right file is involved
                        if (watchEventPath.toString().equals(watchFile))
                        {
                            onModified();
                        }
                    }
                }

                if (!watchKey.reset())
                {
                    // Exit if no longer valid
                    break;
                }
            }
        }
    }

    public abstract void onModified();
}

要使用它,您只需扩展并实现onModified()方法,如下所示:

import java.io.File;

public class MyFileWatcher extends FileWatcher
{
    public MyFileWatcher(String watchFile)
    {
        super(watchFile);
    }

    @Override
    public void onModified()
    {
        System.out.println("Modified!");
    }
}

最后,开始查看文件:

String watchFile = System.getProperty("user.home") + File.separator + "Desktop" + File.separator + "Test.txt";
FileWatcher fileWatcher = new MyFileWatcher(watchFile);
fileWatcher.watchFile();

干净的工作代码!解释清晰且书写良好的答案! - Minhas Kamal
实际上,它缺少了“溢出”情况,因此还不是一个完整的解决方案。 - Hakanai

7

第二个链接依然涉及到Java Watcher API的大量代码。 - bhantol

5
我已经创建了一个围绕Java 1.7的WatchService的封装,允许注册目录以及任意数量的glob模式。这个类将处理过滤并只发出您感兴趣的事件。
try {
    DirectoryWatchService watchService = new SimpleDirectoryWatchService(); // May throw
    watchService.register( // May throw
            new DirectoryWatchService.OnFileChangeListener() {
                @Override
                public void onFileCreate(String filePath) {
                    // File created
                }

                @Override
                public void onFileModify(String filePath) {
                    // File modified
                }

                @Override
                public void onFileDelete(String filePath) {
                    // File deleted
                }
            },
            <directory>, // Directory to watch
            <file-glob-pattern-1>, // E.g. "*.log"
            <file-glob-pattern-2>, // E.g. "input-?.txt"
            <file-glob-pattern-3>, // E.g. "config.ini"
            ... // As many patterns as you like
    );

    watchService.start(); // The actual watcher runs on a new thread
} catch (IOException e) {
    LOGGER.error("Unable to register file change listener for " + fileName);
}

完整代码位于此存储库中。


0

我稍微扩展了BullyWiiPlaza的解决方案,以便与javafx.concurrent集成,例如javafx.concurrent.Taskjavafx.concurrent.Service。 此外,我还添加了跟踪多个文件的可能性。 任务:

import javafx.concurrent.Task;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.nio.file.*;
import java.util.*;

import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;

@Slf4j
public abstract class FileWatcherTask extends Task<Void> {

    static class Entry {
        private final Path folderPath;
        private final String watchFile;

        Entry(Path folderPath, String watchFile) {
            this.folderPath = folderPath;
            this.watchFile = watchFile;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Entry entry = (Entry) o;
            return Objects.equals(folderPath, entry.folderPath) && Objects.equals(watchFile, entry.watchFile);
        }

        @Override
        public int hashCode() {
            return Objects.hash(folderPath, watchFile);
        }
    }

    private final List<Entry> entryList;

    private final Map<WatchKey, Entry> watchKeyEntryMap;

    public FileWatcherTask(Iterable<String> watchFiles) {
        this.entryList = new ArrayList<>();
        this.watchKeyEntryMap = new LinkedHashMap<>();
        for (String watchFile : watchFiles) {
            Path filePath = Paths.get(watchFile);
            boolean isRegularFile = Files.isRegularFile(filePath);
            if (!isRegularFile) {
                // Do not allow this to be a folder since we want to watch files
                throw new IllegalArgumentException(watchFile + " is not a regular file");
            }
            // This is always a folder
            Path folderPath = filePath.getParent();
            // Keep this relative to the watched folder
            watchFile = watchFile.replace(folderPath.toString() + File.separator, "");
            Entry entry = new Entry(folderPath, watchFile);
            entryList.add(entry);
            log.debug("Watcher initialized for {} entries. ({})", entryList.size(), entryList.stream().map(e -> e.watchFile + "-" + e.folderPath).findFirst().orElse("<>"));
        }
    }

    public FileWatcherTask(String... watchFiles) {
        this(Arrays.asList(watchFiles));
    }

    public void watchFile() throws Exception {
        // We obtain the file system of the Path
        // FileSystem fileSystem = folderPath.getFileSystem();
        // TODO: use the actual file system instead of default
        FileSystem fileSystem = FileSystems.getDefault();

        // We create the new WatchService using the try-with-resources block
        try (WatchService service = fileSystem.newWatchService()) {
            log.debug("Watching filesystem {}", fileSystem);
            for (Entry e : entryList) {
                // We watch for modification events
                WatchKey key = e.folderPath.register(service, ENTRY_MODIFY);
                watchKeyEntryMap.put(key, e);
            }

            // Start the infinite polling loop
            while (true) {
                // Wait for the next event
                WatchKey watchKey = service.take();
                for (Entry e : entryList) {
                    // Call this if the right file is involved
                    var hans = watchKeyEntryMap.get(watchKey);
                    if (hans != null) {
                        for (WatchEvent<?> watchEvent : watchKey.pollEvents()) {
                            // Get the type of the event
                            WatchEvent.Kind<?> kind = watchEvent.kind();

                            if (kind == ENTRY_MODIFY) {
                                Path watchEventPath = (Path) watchEvent.context();
                                onModified(e.watchFile);
                            }
                            if (!watchKey.reset()) {
                                // Exit if no longer valid
                                log.debug("Watch key {} was reset", watchKey);
                                break;
                            }
                        }
                    }
                }
            }
        }
    }

    @Override
    protected Void call() throws Exception {
        watchFile();
        return null;
    }

    public abstract void onModified(String watchFile);
}

服务:

public abstract class FileWatcherService extends Service<Void> {
    
    private final Iterable<String> files;

    public FileWatcherService(Iterable<String> files) {
        this.files = files;
    }

    @Override
    protected Task<Void> createTask() {
        return new FileWatcherTask(files) {
            @Override
            public void onModified(String watchFile) {
                FileWatcherService.this.onModified(watchFile);
            }
        };
    }
    
    abstract void onModified(String watchFile);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接