Служба наблюдения Java 7 получает смещение изменения файла

Я только что поиграл с 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 или нужно реализовать что-то нестандартное?

Спасибо


person Tony    schedule 20.10.2013    source источник
comment
Такое невозможно с WatchService.   -  person Sotirios Delimanolis    schedule 20.10.2013
comment
Спасибо. Есть ли что-нибудь в Java 7/NIO, что могло бы это сделать?   -  person Tony    schedule 20.10.2013
comment
Не то, чтобы я знаю. Вам нужно будет реализовать собственное сканирование класса до/после. WatchService не был бы идеальным для этого imo.   -  person Sotirios Delimanolis    schedule 20.10.2013
comment
Можете ли вы просто прочитать файл до и после?   -  person Anubian Noob    schedule 27.05.2014
comment
@AnubianNoob, помимо того, что это далеко не элегантно, это не масштабируется. Представьте себе огромный лог-файл, где каждое изменение — это одна новая строка в файле. Чтение всего файла каждый раз вообще неэффективно...   -  person Simon    schedule 28.05.2014
comment
Если файл обновляется последовательными сообщениями, и ваше приложение не должно пропускать ни одно из этих сообщений, почему бы вам не заглянуть в JMS. Это стандарт и, следовательно, лучшая практика для обмена сообщениями.   -  person Wolfgang Kuehn    schedule 29.05.2014
comment
Ну, если вы хотите обнаружить любое изменение (может быть, в начале или где-то в середине файла), оно точно не будет масштабироваться, но я могу взломать для него proof of concept (у меня есть даже начал, потому что я нашел это забавной задачей для решения). Если ваши файлы изменяются только за счет добавления содержимого в конце (файлы журналов), это совершенно другая проблема, чем первоначально описанная вами. Для последнего у меня есть другое решение. Так что же это должно быть?   -  person kriegaex    schedule 29.05.2014
comment
Меня лично интересуют только добавления файлов, но первоначальный вопрос кажется немного более общим, так что любое изменение также может быть интересным.   -  person Simon    schedule 30.05.2014


Ответы (2)


Что бы это ни стоило, я взломал небольшое доказательство концепции, которая способна

  • обнаруживать добавленные, измененные и удаленные файлы в отслеживаемом каталоге,
  • отображение единых различий для каждого изменения (также полные различия при добавлении/удалении файлов),
  • отслеживать последовательные изменения, сохраняя теневую копию исходного каталога,
  • работать в заданном пользователем ритме (по умолчанию 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») и немного поэкспериментировать с ним, наблюдая за выводом консоли при создании и редактировании некоторых текстовых файлов в редакторе. Это помогает понять внутреннюю механику программного обеспечения. Если что-то пойдет не так, т.е. в течение одного 5-секундного ритма файл создается, но также быстро снова удаляется, копировать или сравнивать нечего, поэтому программа просто напечатает трассировку стека в System.err.

person kriegaex    schedule 29.05.2014
comment
+1 и спасибо за хороший и исчерпывающий ответ. Я могу понять использование внешнего инструмента сравнения в этом случае. В моем случае содержимое будет только добавлено, так что сравнение будет намного проще. В любом случае мне не очень нравится подход просто иметь копию файла. Я все еще надеюсь, что есть лучшее решение, хотя я сомневаюсь, что оно есть :-) - person Simon; 30.05.2014
comment
Что ж, Саймон, вы не автор вопроса, и, вероятно, ваш diff был бы проще, если бы он был опечаткой, и вы хотели сказать tail вместо diff. Для этого также есть решение, и я думаю, что мы хотим оставаться независимыми от платформы (например, в Windows нет предустановленных diff/tail): github.com/dpillay/tail4j (не проверено) - person kriegaex; 30.05.2014
comment
Да, я знаю... Так? :-) Я бы сказал, что разница в файлах, где содержимое только добавляется, на самом деле является хвостом :-) В любом случае, я подожду еще несколько дней, чтобы увидеть, есть ли еще ответы, и если нет, я присуждаю вам награду. - person Simon; 30.05.2014
comment
Ах, хорошо, вы не автор, но все равно назначили награду. Я понятия не имел, что это возможно. Урок выучен. - person kriegaex; 30.05.2014
comment
На самом деле Commons IO проще в использовании, чем недокументированный tail4j, если вам нужна чистая реализация tail на Java. Смотрите мой другой ответ. - person kriegaex; 30.05.2014

Хорошо, вот еще один ответ как вариант моего предыдущего для изменений в любой позиции файла (diff). Теперь несколько более простой случай - файлы только добавляются (хвост).

Как построить:

<?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: Я надеюсь, что это лучше подходит для вашей ситуации, чем более общий случай, и стоит вознаграждения. :-)

person kriegaex    schedule 30.05.2014
comment
Большое спасибо. Сочетание двух ответов прекрасно. Вы можете рассмотреть возможность объединения этого с принятым. - person Simon; 01.06.2014
comment
Нет, варианты использования слишком разные, и каждый ответ сам по себе уже слишком многословен. ;-) - person kriegaex; 02.06.2014
comment
Вместо использования метода Tailer.create() было бы лучше использовать Tailer tailer = new Tailer(). Поскольку метод Tailer.create() может привести к многократному вызову метода listener.handle(). См. stackoverflow.com/a/22987713/1348364 - person aekber; 25.06.2019
comment
Спасибо за информацию. Я написал этот ответ 5 лет назад и лишь смутно помню, что это был первый и единственный раз, когда я использовал эту библиотеку. Так что я не опытный пользователь, мой ответ был просто демонстрацией того, как это можно сделать. У меня не было с этим проблем, но все, кто использует код из этого ответа, не стесняются делать то, что предложил @aekber. :-) - person kriegaex; 26.06.2019