Услугата за наблюдение на 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
Е, ако искате да откриете всяка промяна (може би в началото или някъде по средата на файла), тя определено няма да се мащабира, но мога да хакна доказателство за концепцията за нея (имам дори започна, защото ми се стори забавен проблем за решаване). Ако вашите файлове се променят само чрез добавяне на съдържание в края (лог файлове), това е напълно различен проблем от първоначално описания от вас. За последното имам предвид друго решение. И така, какво трябва да бъде?   -  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
Е, Саймън, ти не си авторът на въпроса и вероятно твоят a diff щеше да е по-лесен, беше печатна грешка и искаше да кажеш опашка вместо diff. Има и решение за това и предполагам, че искаме да останем независими от платформата (напр. няма предварително инсталиран diff/tail в Windows): 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, ако имате нужда от чиста Java tail реализация. Вижте другия ми отговор. - person kriegaex; 30.05.2014

Добре, ето още един отговор като вариант на предишния ми за промени във всяка файлова позиция (разл.). Сега малко по-простият случай е само добавяне на файлове (опашка).

Как да изградите:

<?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