Java 7 中的WatchService获取文件更改偏移量

9

我刚刚尝试使用Java 7 WatchService监控文件变化。

这是我编写的一小段代码:

WatchService watcher = FileSystems.getDefault().newWatchService();

    Path path = Paths.get("c:\\testing");

    path.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);

    while (true) {
        WatchKey key = watcher.take();

        for (WatchEvent event : key.pollEvents()) {
            System.out.println(event.kind() + ":" + event.context());
        }

        boolean valid = key.reset();
        if (!valid) {
            break;
        }
    }

目前看来,这个工作正常,我可以收到通知,当一个文件 'changethis.txt' 被修改时。

但是,除了能够在文件更改时通知外,是否有任何方法可以通知文件中修改发生的位置?

我已经查看了Java文档,但似乎找不到任何相关信息。

使用WatchService是否可以实现此功能,还是需要自定义实现?

谢谢


2
使用WatchService是不可能做到这样的事情的。 - Sotirios Delimanolis
谢谢。Java 7/NIO 中是否有任何可以做到这一点的东西? - Tony
据我所知没有这样的功能。您需要在类之前/之后实现自己的扫描。在我看来,WatchService 不是最理想的选择。 - Sotirios Delimanolis
你能否在之前和之后直接读取文件? - Anubian Noob
@AnubianNoob 除了这个代码远离优雅之外,它也不具备可扩展性。想象一下一个巨大的日志文件,每次修改都会在文件中新增一行。每次读取整个文件的效率非常低下... - Simon
显示剩余3条评论
2个回答

7

值得一提的是,我已经实现了一个小的概念验证,可以:

  • 检测被监视目录中新增、修改和删除的文件,
  • 为每个更改显示统一的差异(在添加/删除文件时也可以显示完整的差异),
  • 通过保留源目录的影子副本来跟踪连续的更改,
  • 按照用户定义的节奏工作(默认值为5秒),以便在短时间内不打印太多小的差异,而是偶尔打印一些较大的差异。

这里有几个限制因素,可能会妨碍生产环境的使用:

  • 为了不使示例代码过于复杂化,当创建影子目录时,子目录是在开始时复制的(因为我重用了一个现有的方法来创建深层目录副本),但在运行时被忽略。只有位于被监视目录正下方的文件才被监控,以避免递归。
  • 您不使用外部库的要求未被满足,因为我确实想避免重新发明统一的差异创建轮子。
  • 此解决方案的最大优点——它能够检测文本文件中的任何更改,而不仅仅是像tail -f那样在文件末尾——也是它最大的缺点:每当文件更改时,它必须完全进行影子复制,否则程序无法检测到随后的更改。因此,我不建议将此解决方案用于非常大的文件。

如何构建:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>de.scrum-master.tools</groupId>
    <artifactId>SO_WatchServiceChangeLocationInFile</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>com.googlecode.java-diff-utils</groupId>
            <artifactId>diffutils</artifactId>
            <version>1.3.0</version>
        </dependency>
    </dependencies>
</project>

源代码(抱歉,有点长):

package de.scrum_master.app;

import difflib.DiffUtils;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.LinkedList;
import java.util.List;

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

public class FileChangeWatcher {
    public static final String DEFAULT_WATCH_DIR = "watch-dir";
    public static final String DEFAULT_SHADOW_DIR = "shadow-dir";
    public static final int DEFAULT_WATCH_INTERVAL = 5;

    private Path watchDir;
    private Path shadowDir;
    private int watchInterval;
    private WatchService watchService;

    public FileChangeWatcher(Path watchDir, Path shadowDir, int watchInterval) throws IOException {
        this.watchDir = watchDir;
        this.shadowDir = shadowDir;
        this.watchInterval = watchInterval;
        watchService = FileSystems.getDefault().newWatchService();
    }

    public void run() throws InterruptedException, IOException {
        prepareShadowDir();
        watchDir.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
        while (true) {
            WatchKey watchKey = watchService.take();
            for (WatchEvent<?> event : watchKey.pollEvents()) {
                Path oldFile = shadowDir.resolve((Path) event.context());
                Path newFile = watchDir.resolve((Path) event.context());
                List<String> oldContent;
                List<String> newContent;
                WatchEvent.Kind<?> eventType = event.kind();
                if (!(Files.isDirectory(newFile) || Files.isDirectory(oldFile))) {
                    if (eventType == ENTRY_CREATE) {
                        if (!Files.isDirectory(newFile))
                            Files.createFile(oldFile);
                    } else if (eventType == ENTRY_MODIFY) {
                        Thread.sleep(200);
                        oldContent = fileToLines(oldFile);
                        newContent = fileToLines(newFile);
                        printUnifiedDiff(newFile, oldFile, oldContent, newContent);
                        try {
                            Files.copy(newFile, oldFile, StandardCopyOption.REPLACE_EXISTING);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    } else if (eventType == ENTRY_DELETE) {
                        try {
                            oldContent = fileToLines(oldFile);
                            newContent = new LinkedList<>();
                            printUnifiedDiff(newFile, oldFile, oldContent, newContent);
                            Files.deleteIfExists(oldFile);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            watchKey.reset();
            Thread.sleep(1000 * watchInterval);
        }
    }

    private void prepareShadowDir() throws IOException {
        recursiveDeleteDir(shadowDir);
        Runtime.getRuntime().addShutdownHook(
            new Thread() {
                @Override
                public void run() {
                    try {
                        System.out.println("Cleaning up shadow directory " + shadowDir);
                        recursiveDeleteDir(shadowDir);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        );
        recursiveCopyDir(watchDir, shadowDir);
    }

    public static void recursiveDeleteDir(Path directory) throws IOException {
        if (!directory.toFile().exists())
            return;
        Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Files.delete(file);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                Files.delete(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }

    public static void recursiveCopyDir(final Path sourceDir, final Path targetDir) throws IOException {
        Files.walkFileTree(sourceDir, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Files.copy(file, Paths.get(file.toString().replace(sourceDir.toString(), targetDir.toString())));
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                Files.createDirectories(Paths.get(dir.toString().replace(sourceDir.toString(), targetDir.toString())));
                return FileVisitResult.CONTINUE;
            }
        });
    }

    private static List<String> fileToLines(Path path) throws IOException {
        List<String> lines = new LinkedList<>();
        String line;
        try (BufferedReader reader = new BufferedReader(new FileReader(path.toFile()))) {
            while ((line = reader.readLine()) != null)
                lines.add(line);
        }
        catch (Exception e) {}
        return lines;
    }

    private static void printUnifiedDiff(Path oldPath, Path newPath, List<String> oldContent, List<String> newContent) {
        List<String> diffLines = DiffUtils.generateUnifiedDiff(
            newPath.toString(),
            oldPath.toString(),
            oldContent,
            DiffUtils.diff(oldContent, newContent),
            3
        );
        System.out.println();
        for (String diffLine : diffLines)
            System.out.println(diffLine);
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        String watchDirName = args.length > 0 ? args[0] : DEFAULT_WATCH_DIR;
        String shadowDirName = args.length > 1 ? args[1] : DEFAULT_SHADOW_DIR;
        int watchInterval = args.length > 2 ? Integer.getInteger(args[2]) : DEFAULT_WATCH_INTERVAL;
        new FileChangeWatcher(Paths.get(watchDirName), Paths.get(shadowDirName), watchInterval).run();
    }
}

我建议使用默认设置(例如使用名为“watch-dir”的源目录),并在编辑器中创建和编辑一些文本文件时观察控制台输出,玩弄一段时间,以帮助理解软件的内部机制。如果出现问题,例如在五秒钟的节奏内创建了一个文件,但很快又被删除了,没有可复制或比较的文件,所以程序将只打印System.err的堆栈跟踪。


1
+1并感谢您提供了一份好的、全面的答案。我可以理解在这种情况下使用外部差异工具的用途。在我的情况下,内容只会被追加,因此差异将会更容易。无论如何,我并不真的喜欢简单地拥有文件的副本的方法。我仍然希望有一个更好的解决方案,尽管我怀疑是否存在 :-) - Simon
嗯,Simon,你不是这个问题的作者,也许你的“a diff would be easier”是一个打字错误,你想说的是“tail”而不是“diff”。对于这个问题也有解决方案,我猜我们想要保持平台独立性(例如,在Windows上没有预安装diff/tail):https://github.com/dpillay/tail4j(未经测试)。 - kriegaex
是啊,我知道...那又怎样呢? :-) 对于仅附加内容的文件进行差异比较,实际上就像是一个尾巴,我想 :-) 无论如何,我会再等几天看看是否有更多的答案,如果没有,我会把奖励授予你。 - Simon
啊,好的,你不是作者,但仍然设置了赏金。我之前并不知道这是可能的。吸取教训了。 - kriegaex
实际上,如果您需要一个纯Java的tail实现,Commons IO比未记录的tail4j更容易使用。请参阅我的其他答案。 - kriegaex

4

好的,这里是我之前回答的另一种变体,针对任何文件位置的更改(diff)。现在比较简单的情况是只有文件被追加(tail)。

如何构建:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>de.scrum-master.tools</groupId>
    <artifactId>SO_WatchServiceChangeLocationInFile</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <!-- Use snapshot because of the UTF-8 problem in https://issues.apache.org/jira/browse/IO-354 -->
            <version>2.5-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>apache.snapshots</id>
            <url>http://repository.apache.org/snapshots/</url>
        </repository>
    </repositories>
</project>

正如您所看到的,我们在这里使用了Apache Commons IO。(为什么是快照版本?如果您感兴趣,请按XML注释中的链接。)

源代码:

package de.scrum_master.app;

import org.apache.commons.io.input.Tailer;
import org.apache.commons.io.input.TailerListenerAdapter;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.*;

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

public class FileTailWatcher {
    public static final String DEFAULT_WATCH_DIR = "watch-dir";
    public static final int DEFAULT_WATCH_INTERVAL = 5;

    private Path watchDir;
    private int watchInterval;
    private WatchService watchService;

    public FileTailWatcher(Path watchDir, int watchInterval) throws IOException {
        if (!Files.isDirectory(watchDir))
            throw new IllegalArgumentException("Path '" + watchDir + "' is not a directory");
        this.watchDir = watchDir;
        this.watchInterval = watchInterval;
        watchService = FileSystems.getDefault().newWatchService();
    }

    public static class MyTailerListener extends TailerListenerAdapter {
        public void handle(String line) {
            System.out.println(line);
        }
    }

    public void run() throws InterruptedException, IOException {
        try (DirectoryStream<Path> dirEntries = Files.newDirectoryStream(watchDir)) {
            for (Path file : dirEntries)
                createTailer(file);
        }
        watchDir.register(watchService, ENTRY_CREATE);
        while (true) {
            WatchKey watchKey = watchService.take();
            for (WatchEvent<?> event : watchKey.pollEvents())
                createTailer(watchDir.resolve((Path) event.context()));
            watchKey.reset();
            Thread.sleep(1000 * watchInterval);
        }
    }

    private Tailer createTailer(Path path) {
        if (Files.isDirectory(path))
            return null;
        System.out.println("Creating tailer: " + path);
        return Tailer.create(
            path.toFile(),             // File to be monitored
            Charset.defaultCharset(),  // Character set (available since Commons IO 2.5)
            new MyTailerListener(),    // What should happen for new tail events?
            1000,                      // Delay between checks in ms
            true,                      // Tail from end of file, not from beginning
            true,                      // Close & reopen files in between reads,
                                       // otherwise file is locked on Windows and cannot be deleted
            4096                       // Read buffer size
        );
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        String watchDirName = args.length > 0 ? args[0] : DEFAULT_WATCH_DIR;
        int watchInterval = args.length > 2 ? Integer.getInteger(args[2]) : DEFAULT_WATCH_INTERVAL;
        new FileTailWatcher(Paths.get(watchDirName), watchInterval).run();
    }
}

现在尝试追加到已存在的文件或创建新文件。所有内容将打印到标准输出。在生产环境中,您可能会显示多个窗口或选项卡,每个日志文件一个。无论如何...
@Simon:我希望这个比更一般的情况更适合您的情况,并且值得奖励。:-)

1
非常感谢。这两个答案的结合非常好。您可以考虑将此答案合并到已接受的答案中。 - Simon
不好意思,使用情况太不同了,而且每个答案本身已经够啰嗦的了。;-) - kriegaex
使用Tailor.create()方法不如使用Tailer tailer = new Tailer()。因为Tailor.create()方法可能会导致多次调用listener.handle()方法。请参见https://dev59.com/s2Ag5IYBdhLWcg3w9e0y#22987713。 - aekber
感谢提供这些信息。我写下这个答案已经有5年了,只模糊地记得那是我第一次也是唯一一次使用这个库。所以我不是一个专家用户,我的回答只是一个简单的展示如何完成它。我没有遇到任何问题,但是每个使用这个答案中的代码的人都可以自由地按照@aekber建议去做。 :-) - kriegaex

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