Java 7 WatchService – игнорирование нескольких вхождений одного и того же события

В javadoc для StandardWatchEventKinds.ENTRY_MODIFY говорится:

Запись каталога изменена. Когда каталог регистрируется для этого события, WatchKey ставится в очередь, когда обнаруживается, что запись в каталоге была изменена. Количество событий для этого события равно 1 или больше.

Когда вы редактируете содержимое файла с помощью редактора, он изменит как дату (или другие метаданные), так и содержимое. Таким образом, вы получаете два события ENTRY_MODIFY, но каждое из них будет иметь count из 1 (по крайней мере, это то, что я вижу).

Я пытаюсь отслеживать файл конфигурации (servers.cfg, ранее зарегистрированный в WatchService), который обновляется вручную (т. е. через командную строку vi) с помощью следующего кода:

while(true) {
    watchKey = watchService.take(); // blocks

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

        System.out.println(watchEvent.context() + ", count: "+ watchEvent.count() + ", event: "+ watchEvent.kind());
        // prints (loop on the while twice)
        // servers.cfg, count: 1, event: ENTRY_MODIFY
        // servers.cfg, count: 1, event: ENTRY_MODIFY

        switch(kind.name()) {
            case "ENTRY_MODIFY":
                handleModify(watchEvent.context()); // reload configuration class
                break;
            case "ENTRY_DELETE":
                handleDelete(watchEvent.context()); // do something else
                break;              
        }
    }   

    watchKey.reset();       
}

Поскольку вы получаете два события ENTRY_MODIFY, описанное выше будет перезагружать конфигурацию дважды, когда требуется только один раз. Есть ли способ игнорировать все, кроме одного, если предположить, что таких событий может быть более одного?

Если в WatchService API есть такая утилита, тем лучше. (Я как бы не хочу проверять время между каждым событием. Все методы обработчика в моем коде синхронны.

То же самое происходит, если вы создаете (копируете/вставляете) файл из одного каталога в отслеживаемый каталог. Как вы можете объединить их в одно событие?


person Sotirios Delimanolis    schedule 27.05.2013    source источник


Ответы (13)


У меня была аналогичная проблема: я использую WatchService API для синхронизации каталогов, но заметил, что во многих случаях обновления выполнялись дважды. Кажется, я решил проблему, проверив метку времени в файлах - это, кажется, отсеивает вторую операцию копирования. (По крайней мере, в Windows 7 - я не уверен, что это будет работать правильно в других операционных системах)

Может быть, вы могли бы использовать что-то подобное? Сохранять метку времени из файла и перезагружать только при обновлении метки времени?

person tofarr    schedule 04.06.2013
comment
Что, если файл был изменен два или более раз в этой группе событий? Гипотетически у вас будет 4, 6, 8... и т. д. изменения событий, но значение .lastModified() будет представлять только последнее событие. - person 2rs2ts; 25.02.2014

WatcherServices сообщает о событиях дважды, потому что базовый файл обновляется дважды. Один раз для содержимого и один раз для времени изменения файла. Эти события происходят в течение короткого промежутка времени. Чтобы решить эту проблему, спите между вызовами poll() или take() и вызовом key.pollEvents(). Например:

@Override
@SuppressWarnings( "SleepWhileInLoop" )
public void run() {
  setListening( true );

  while( isListening() ) {
    try {
      final WatchKey key = getWatchService().take();
      final Path path = get( key );

      // Prevent receiving two separate ENTRY_MODIFY events: file modified
      // and timestamp updated. Instead, receive one ENTRY_MODIFY event
      // with two counts.
      Thread.sleep( 50 );

      for( final WatchEvent<?> event : key.pollEvents() ) {
        final Path changed = path.resolve( (Path)event.context() );

        if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) {
          System.out.println( "Changed: " + changed );
        }
      }

      if( !key.reset() ) {
        ignore( path );
      }
    } catch( IOException | InterruptedException ex ) {
      // Stop eavesdropping.
      setListening( false );
    }
  }
}

Звонок sleep() помогает устранить двойные звонки. Задержка может достигать трех секунд.

person Community    schedule 09.08.2014
comment
Разве клавиша WatchService не активирует эти события в фоновом режиме? К чему приведет отсрочка их получения? - person Sotirios Delimanolis; 09.08.2014
comment
При этом вы получите событие со значением счетчика 2 вместо двух отдельных событий со значением счетчика 1. Таким образом, вам не нужно удалять повторяющиеся события вручную. Я иду в этом wa. Счастливый :) - person mmdemirbas; 06.09.2014
comment
Это лучший ответ, спасибо за описание того, почему есть два события. - person nverbeek; 05.03.2018
comment
Не работает. Мне нужно написать это в отдельной теме? В настоящее время я пишу метод main только для тестирования. - person Faizan Mubasher; 23.08.2019
comment
Вам нужно написать это в отдельной теме. Работает отлично. Я тестировал это в Windows и имел 3 события изменения записи при перезаписи файла. Добавлен сон потоков, и проблема была решена. Спасибо за ответ. - person Player1; 08.04.2020
comment
Ты мой герой! Спасибо, Thread.sleep(100) работал отлично. - person wilmerlpr; 16.11.2020

Одно из моих решений goto для таких проблем — просто поставить в очередь уникальные ресурсы событий и отложить обработку на приемлемое время. В этом случае я поддерживаю Set<String>, который содержит каждое имя файла, полученное из каждого поступающего события. Использование Set<> гарантирует, что дубликаты не будут добавлены и, следовательно, будут обработаны только один раз (за период задержки).

Каждый раз, когда происходит интересное событие, я добавляю имя файла в Set<> и перезапускаю таймер задержки. Когда все уляжется и время задержки истечет, я приступаю к обработке файлов.

Методы addFileToProcess() и processFiles() «синхронизированы», чтобы гарантировать отсутствие исключений ConcurrentModificationException.

Этот упрощенный/автономный пример является производным от Oracle WatchDir.java:

import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;

public class DirectoryWatcherService implements Runnable {
    @SuppressWarnings("unchecked")
    static <T> WatchEvent<T> cast(WatchEvent<?> event) {
        return (WatchEvent<T>)event;
    }

    /*
     * Wait this long after an event before processing the files.
     */
    private final int DELAY = 500;

    /*
     * Use a SET to prevent duplicates from being added when multiple events on the 
     * same file arrive in quick succession.
     */
    HashSet<String> filesToReload = new HashSet<String>();

    /*
     * Keep a map that will be used to resolve WatchKeys to the parent directory
     * so that we can resolve the full path to an event file. 
     */
    private final Map<WatchKey,Path> keys;

    Timer processDelayTimer = null;

    private volatile Thread server;

    private boolean trace = false;

    private WatchService watcher = null;

    public DirectoryWatcherService(Path dir, boolean recursive) 
        throws IOException {
        this.watcher = FileSystems.getDefault().newWatchService();
        this.keys = new HashMap<WatchKey,Path>();

        if (recursive) {
            registerAll(dir);
        } else {
            register(dir);
        }

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

    private synchronized void addFileToProcess(String filename) {
        boolean alreadyAdded = filesToReload.add(filename) == false;
        System.out.println("Queuing file for processing: " 
            + filename + (alreadyAdded?"(already queued)":""));
        if (processDelayTimer != null) {
            processDelayTimer.cancel();
        }
        processDelayTimer = new Timer();
        processDelayTimer.schedule(new TimerTask() {

            @Override
            public void run() {
                processFiles();
            }
        }, DELAY);
    }

    private synchronized void processFiles() {
        /*
         * Iterate over the set of file to be processed
         */
        for (Iterator<String> it = filesToReload.iterator(); it.hasNext();) {
            String filename = it.next();

            /*
             * Sometimes you just have to do what you have to do...
             */
            System.out.println("Processing file: " + filename);

            /*
             * Remove this file from the set.
             */
            it.remove();
        }
    }

    /**
     * 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
            {
                if (dir.getFileName().toString().startsWith(".")) {
                    return FileVisitResult.SKIP_SUBTREE;
                }

                register(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }

    @SuppressWarnings("unchecked")
    @Override
    public void run() {
        Thread thisThread = Thread.currentThread();

        while (server == thisThread) {
            try {
                // wait for key to be signaled
                WatchKey key;
                try {
                    key = watcher.take();
                } catch (InterruptedException x) {
                    return;
                }

                Path dir = keys.get(key);
                if (dir == null) {
                    continue;
                }

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

                    if (kind == OVERFLOW) {
                        continue;
                    }

                    if (kind == ENTRY_MODIFY) {

                        WatchEvent<Path> ev = (WatchEvent<Path>)event;
                        Path name = ev.context();
                        Path child = dir.resolve(name);

                        String filename = child.toAbsolutePath().toString();

                        addFileToProcess(filename);
                    }
                }

                key.reset();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void start() {
        server = new Thread(this);
        server.setName("Directory Watcher Service");
        server.start();
    }


    public void stop() {
        Thread moribund = server;
        server = null;
        if (moribund != null) {
            moribund.interrupt();
        }
    }

    public static void main(String[] args) {
        if (args==null || args.length == 0) {
            System.err.println("You need to provide a path to watch!");
            System.exit(-1);
        }

        Path p = Paths.get(args[0]);
        if (!Files.isDirectory(p)) {
            System.err.println(p + " is not a directory!");
            System.exit(-1);
        }

        DirectoryWatcherService watcherService;
        try {
            watcherService = new DirectoryWatcherService(p, true);
            watcherService.start();
        } catch (IOException e) {
            System.err.println(e.getMessage());
        }
    }

}
person Kevin    schedule 15.01.2016

Я изменил WatchDir.java, чтобы получать только изменения, сделанные человеком. Сравнение .lastModified() файла.

long lastModi=0; //above for loop
if(kind==ENTRY_CREATE){
    System.out.format("%s: %s\n", event.kind().name(), child);
}else if(kind==ENTRY_MODIFY){
    if(child.toFile().lastModified() - lastModi > 1000){
        System.out.format("%s: %s\n", event.kind().name(), child);
    }
}else if(kind==ENTRY_DELETE){
    System.out.format("%s: %s\n", event.kind().name(), child);
}
    lastModi=child.toFile().lastModified();
person Nilesh    schedule 04.04.2015

Вот полная реализация с использованием timestamps, чтобы избежать запуска нескольких событий:

import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Map;

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

public abstract class DirectoryWatcher
{
    private WatchService watcher;
    private Map<WatchKey, Path> keys;
    private Map<Path, Long> fileTimeStamps;
    private boolean recursive;
    private boolean trace = true;

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

    /**
     * Register the given directory with the WatchService
     */
    private void register(Path directory) throws IOException
    {
        WatchKey watchKey = directory.register(watcher, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE);

        addFileTimeStamps(directory);

        if (trace)
        {
            Path existingFilePath = keys.get(watchKey);
            if (existingFilePath == null)
            {
                System.out.format("register: %s\n", directory);
            } else
            {
                if (!directory.equals(existingFilePath))
                {
                    System.out.format("update: %s -> %s\n", existingFilePath, directory);
                }
            }
        }

        keys.put(watchKey, directory);
    }

    private void addFileTimeStamps(Path directory)
    {
        File[] files = directory.toFile().listFiles();
        if (files != null)
        {
            for (File file : files)
            {
                if (file.isFile())
                {
                    fileTimeStamps.put(file.toPath(), file.lastModified());
                }
            }
        }
    }

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

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

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

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

    /**
     * Process all events for keys queued to the watcher
     */
    void processEvents() throws InterruptedException, IOException
    {
        while (true)
        {
            WatchKey key = watcher.take();

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

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

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

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

                long oldFileModifiedTimeStamp = fileTimeStamps.get(filePath);
                long newFileModifiedTimeStamp = filePath.toFile().lastModified();
                if (newFileModifiedTimeStamp > oldFileModifiedTimeStamp)
                {
                    fileTimeStamps.remove(filePath);
                    onEventOccurred();
                    fileTimeStamps.put(filePath, filePath.toFile().lastModified());
                }

                if (recursive && watchEventKind == ENTRY_CREATE)
                {
                    if (Files.isDirectory(filePath, NOFOLLOW_LINKS))
                    {
                        registerAll(filePath);
                    }
                }

                break;
            }

            boolean valid = key.reset();

            if (!valid)
            {
                keys.remove(key);

                if (keys.isEmpty())
                {
                    break;
                }
            }
        }
    }

    public abstract void onEventOccurred();
}

Расширьте класс и реализуйте метод onEventOccurred().

person BullyWiiPlaza    schedule 16.11.2016

Вы уверены, что проблема с jdk7? У меня это дает правильный результат (jdk7u15, windows)

Код

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

public class WatchTest {

    public void watchMyFiles() throws IOException, InterruptedException {
        Path path = Paths.get("c:/temp");
        WatchService watchService = path.getFileSystem().newWatchService();
        path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);

        while (true) {
            WatchKey watchKey = watchService.take(); // blocks

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

                System.out.println(watchEvent.context() + ", count: " +
                        watchEvent.count() + ", event: " + watchEvent.kind());
                // prints (loop on the while twice)
                // servers.cfg, count: 1, event: ENTRY_MODIFY
                // servers.cfg, count: 1, event: ENTRY_MODIFY

                switch (kind.name()) {
                    case "ENTRY_MODIFY":
                        handleModify(watchEvent.context()); // reload configuration class
                        break;
                    case "ENTRY_DELETE":
                        handleDelete(watchEvent.context()); // do something else
                        break;
                    default:
                        System.out.println("Event not expected " + event.kind().name());
                }
            }

            watchKey.reset();
        }
    }

    private void handleDelete(Path context) {
        System.out.println("handleDelete  " + context.getFileName());
    }

    private void handleModify(Path context) {
        System.out.println("handleModify " + context.getFileName());
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        new WatchTest().watchMyFiles();
    }
}

Вывод показан ниже — когда файл копируется или редактируется с помощью блокнота.

config.xml, count: 1, event: ENTRY_MODIFY
handleModify config.xml

Vi использует множество дополнительных файлов и несколько раз обновляет атрибут файла. notepad++ делает ровно два раза.

person Jayan    schedule 11.06.2013
comment
WatchService — это функция Java 7, так что да, в этом смысле. Если вы скопировали файл, вы должны увидеть как ENTRY_CREATED, так и ENTRY_MODIFY. - person Sotirios Delimanolis; 11.06.2013
comment
@ Сотириос Делиманолис: Вы имеете в виду, что получаете уведомление о создании при просмотре StandardWatchEventKinds.ENTRY_MODIFY? Решение от tofarr кажется хорошим обходным путем. - person Jayan; 11.06.2013
comment
Да, nvm, вы не за этим следите. Я удивлен, что ты получил только один ENTRY_MODIFY. Решение Tofarr - это то, что я сделал на данный момент, но я все еще даю ему время для других возможных решений. - person Sotirios Delimanolis; 11.06.2013
comment
У меня была такая же проблема, затем я прочитал комментарий Джаяна о том, что блокнот ++ дважды обновляет атрибуты файла. Проблема не в наблюдателе, в моем случае проблема в редакторе файлов. - person Volceri; 14.09.2016

Если вы используете RxJava, вы можете использовать операторtrolLast. В приведенном ниже примере для каждого файла в отслеживаемом каталоге генерируется только последнее событие за 1000 миллисекунд.

public class FileUtils {
    private static final long EVENT_DELAY = 1000L;

    public static Observable<FileWatchEvent> watch(Path directory, String glob) {
        return Observable.<FileWatchEvent>create(subscriber -> {
            final PathMatcher matcher = directory.getFileSystem().getPathMatcher("glob:" + glob);

            WatchService watcher = FileSystems.getDefault().newWatchService();
            subscriber.setCancellable(watcher::close);

            try {
                directory.register(watcher,
                        ENTRY_CREATE,
                        ENTRY_DELETE,
                        ENTRY_MODIFY);
            } catch (IOException e) {
                subscriber.onError(e);
                return;
            }

            while (!subscriber.isDisposed()) {
                WatchKey key;
                try {
                    key = watcher.take();
                } catch (InterruptedException e) {
                    if (subscriber.isDisposed())
                        subscriber.onComplete();
                    else
                        subscriber.onError(e);
                    return;
                }

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

                    if (kind != OVERFLOW) {
                        WatchEvent<Path> ev = (WatchEvent<Path>) event;
                        Path child = directory.resolve(ev.context());

                        if (matcher.matches(child.getFileName()))
                            subscriber.onNext(new FileWatchEvent(kindToType(kind), child));
                    }
                }

                if (!key.reset()) {
                    subscriber.onError(new IOException("Invalid key"));
                    return;
                }
            }
        }).groupBy(FileWatchEvent::getPath).flatMap(o -> o.throttleLast(EVENT_DELAY, TimeUnit.MILLISECONDS));
    }

    private static FileWatchEvent.Type kindToType(WatchEvent.Kind kind) {
        if (StandardWatchEventKinds.ENTRY_CREATE.equals(kind))
            return FileWatchEvent.Type.ADDED;
        else if (StandardWatchEventKinds.ENTRY_MODIFY.equals(kind))
            return FileWatchEvent.Type.MODIFIED;
        else if (StandardWatchEventKinds.ENTRY_DELETE.equals(kind))
            return FileWatchEvent.Type.DELETED;
        throw new RuntimeException("Invalid kind: " + kind);
    }

    public static class FileWatchEvent {
        public enum Type {
            ADDED, DELETED, MODIFIED
        }

        private Type type;
        private Path path;

        public FileWatchEvent(Type type, Path path) {
            this.type = type;
            this.path = path;
        }

        public Type getType() {
            return type;
        }

        public Path getPath() {
            return path;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            FileWatchEvent that = (FileWatchEvent) o;

            if (type != that.type) return false;
            return path != null ? path.equals(that.path) : that.path == null;
        }

        @Override
        public int hashCode() {
            int result = type != null ? type.hashCode() : 0;
            result = 31 * result + (path != null ? path.hashCode() : 0);
            return result;
        }
    }
}
person user2252051    schedule 22.07.2017

Я решил эту проблему, определив глобальную логическую переменную с именем «modifySolver», которая по умолчанию имеет значение false. Вы можете справиться с этой проблемой, как показано ниже:

else if (eventKind.equals (ENTRY_MODIFY))
        {
            if (event.count() == 2)
            {
                getListener(getDirPath(key)).onChange (FileChangeType.MODIFY, file.toString ());
            }
            /*capture first modify event*/
            else if ((event.count() == 1) && (!modifySolver))
            {
                getListener(getDirPath(key)).onChange (FileChangeType.MODIFY, file.toString ());
                modifySolver = true;
            }
            /*discard the second modify event*/
            else if ((event.count() == 1) && (modifySolver))
            {
                modifySolver = false;
            }
        }
person khesam109    schedule 10.05.2016

Я скомпилировал Oracle WatchDir.java и предложение @nilesh. в класс Observable, который будет уведомлять своих наблюдателей один раз, когда отслеживаемый файл будет изменен.

Я пытался сделать его как можно более читабельным и коротким, но все же получил более 100 строк. Улучшения приветствуются, конечно.

Применение:

FileChangeNotifier fileReloader = new FileChangeNotifier(File file);
fileReloader.addObserver((Observable obj, Object arg) -> {
    System.out.println("File changed for the " + arg + " time.");
});

См. мое решение на GitHub: FileChangeNotifier.java.

person Florian Sesser    schedule 31.07.2015
comment
+1 за Observable. Единственная проблема с этим решением заключается в том, что оно не очень масштабируемо, поскольку вы создаете один поток для каждого наблюдателя. Это очень много, если вы собираетесь просматривать несколько сотен файлов! - person igracia; 20.04.2017
comment
@igracia вы очень правы, спасибо! Мой вариант использования - просмотр одного файла, я не думал о масштабируемости. Решение FaNaJ ниже, вероятно, работает намного лучше при просмотре большого количества файлов. - person Florian Sesser; 21.04.2017
comment
Я думаю, вы могли бы как-то комбинировать и то, и другое, и зарегистрировать FileChangeNotifier Observable представление изменения файла. Просто добавьте цикл while в указанное вами решение, уведомляющее наблюдателей, и вы получите лучшее из обоих миров ;-) Выдача случайного, зависящего от платформы sleep кажется очень случайным. - person igracia; 23.04.2017
comment
Я уже загромождал свою реализацию флагом ignoreNext, когда сам модифицировал файл (немного быстро и грязно...). Должно быть относительно легко отказаться от этого и добавить карту путей в список наблюдателей. Я бы предпочел не делать это сам без варианта использования, и вспоминая, какие усилия стоило протестировать это на разных платформах... - person Florian Sesser; 24.04.2017

Я пробовал это, и он отлично работает:

import java.io.IOException;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

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

public class FileWatcher implements Runnable, AutoCloseable {

    private final WatchService service;
    private final Map<Path, WatchTarget> watchTargets = new HashMap<>();
    private final List<FileListener> fileListeners = new CopyOnWriteArrayList<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock r = lock.readLock();
    private final Lock w = lock.writeLock();
    private final AtomicBoolean running = new AtomicBoolean(false);

    public FileWatcher() throws IOException {
        service = FileSystems.getDefault().newWatchService();
    }

    @Override
    public void run() {
        if (running.compareAndSet(false, true)) {
            while (running.get()) {
                WatchKey key;
                try {
                    key = service.take();
                } catch (Throwable e) {
                    break;
                }
                if (key.isValid()) {
                    r.lock();
                    try {
                        key.pollEvents().stream()
                                .filter(e -> e.kind() != OVERFLOW)
                                .forEach(e -> watchTargets.values().stream()
                                        .filter(t -> t.isInterested(e))
                                        .forEach(t -> fireOnEvent(t.path, e.kind())));
                    } finally {
                        r.unlock();
                    }
                    if (!key.reset()) {
                        break;
                    }
                }
            }
            running.set(false);
        }
    }

    public boolean registerPath(Path path, boolean updateIfExists, WatchEvent.Kind... eventKinds) {
        w.lock();
        try {
            WatchTarget target = watchTargets.get(path);
            if (!updateIfExists && target != null) {
                return false;
            }
            Path parent = path.getParent();
            if (parent != null) {
                if (target == null) {
                    watchTargets.put(path, new WatchTarget(path, eventKinds));
                    parent.register(service, eventKinds);
                } else {
                    target.setEventKinds(eventKinds);
                }
                return true;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            w.unlock();
        }
        return false;
    }

    public void addFileListener(FileListener fileListener) {
        fileListeners.add(fileListener);
    }

    public void removeFileListener(FileListener fileListener) {
        fileListeners.remove(fileListener);
    }

    private void fireOnEvent(Path path, WatchEvent.Kind eventKind) {
        for (FileListener fileListener : fileListeners) {
            fileListener.onEvent(path, eventKind);
        }
    }

    public boolean isRunning() {
        return running.get();
    }

    @Override
    public void close() throws IOException {
        running.set(false);
        w.lock();
        try {
            service.close();
        } finally {
            w.unlock();
        }
    }

    private final class WatchTarget {

        private final Path path;
        private final Path fileName;
        private final Set<String> eventNames = new HashSet<>();
        private final Event lastEvent = new Event();

        private WatchTarget(Path path, WatchEvent.Kind[] eventKinds) {
            this.path = path;
            this.fileName = path.getFileName();
            setEventKinds(eventKinds);
        }

        private void setEventKinds(WatchEvent.Kind[] eventKinds) {
            eventNames.clear();
            for (WatchEvent.Kind k : eventKinds) {
                eventNames.add(k.name());
            }
        }

        private boolean isInterested(WatchEvent e) {
            long now = System.currentTimeMillis();
            String name = e.kind().name();
            if (e.context().equals(fileName) && eventNames.contains(name)) {
                if (lastEvent.name == null || !lastEvent.name.equals(name) || now - lastEvent.when > 100) {
                    lastEvent.name = name;
                    lastEvent.when = now;
                    return true;
                }
            }
            return false;
        }

        @Override
        public int hashCode() {
            return path.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            return obj == this || obj != null && obj instanceof WatchTarget && Objects.equals(path, ((WatchTarget) obj).path);
        }

    }

    private final class Event {

        private String name;
        private long when;

    }

    public static void main(String[] args) throws IOException, InterruptedException {
        FileWatcher watcher = new FileWatcher();
        if (watcher.registerPath(Paths.get("filename"), false, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE)) {
            watcher.addFileListener((path, eventKind) -> System.out.println(path + " -> " + eventKind.name()));
            new Thread(watcher).start();
            System.in.read();
        }
        watcher.close();
        System.exit(0);
    }

}

ФайлСлушатель:

import java.nio.file.Path;
import java.nio.file.WatchEvent;

public interface FileListener {

    void onEvent(Path path, WatchEvent.Kind eventKind);

}
person FaNaJ    schedule 31.05.2015
comment
Можете ли вы объяснить, что делает код? Объединяет ли это оба события в одно событие? Использует ли оно решение, при котором запускается только одно событие? - person Sotirios Delimanolis; 31.05.2015
comment
@SotiriosDelimanolis в методе isInterested мы игнорируем появление тех же событий, которые произошли в 100 ms. но в некоторых случаях это может быть небезопасно. например, если два приложения изменяют один и тот же файл, а приложение B завершает свою работу 50 ms после того, как это сделало приложение A... - person FaNaJ; 01.06.2015
comment
ОП: I kind of don't want to check times between each event - person Florian Sesser; 31.07.2015

У меня была аналогичная проблема. Я знаю, что это поздно, но это может помочь кому-то. Мне просто нужно было удалить дубликаты ENTRY_MODIFY. Всякий раз, когда запускается ENTRY_MODIFY, count() возвращает либо 2, либо 1. Если это 1, то будет другое событие с count() 1. Так что просто поставьте глобальный счетчик, который ведет подсчет возвращаемых значений, и выполняйте операции только тогда, когда счетчик становится 2. Что-то вроде этого можно сделать:

WatchEvent event; 
int count = 0;

if(event.count() == 2)
     count = 2;

if(event.count() == 1)
     count++;

if(count == 2){
     //your operations here
     count = 0;
}
person user2729516    schedule 05.11.2015
comment
Это кажется нормальным, если 2 гарантировано. В моих тестах разные приложения ведут себя по-разному. cmd и java вызывают одно событие при записи, тогда как notepad.exe и notepad++ вызывают два. - person tresf; 09.12.2017
comment
Ты прав. В некоторых случаях это было не очень полезно, поэтому в итоге я использовал это решение от Дейва Джарвиса. Может быть, вы можете использовать оба решения вместе, если это вам поможет. - person user2729516; 09.12.2017
comment
Я провел сравнительный анализ. На моей машине сохранение файла с 1 строкой приводит к примерно 6 мс между повторяющимися событиями. Сохранение файла с 10 000 000 строк приводит к примерно 500 мс между повторяющимися событиями. - person tresf; 09.12.2017
comment
Повторяющиеся вызовы на самом деле зависят от базовой файловой системы и приложения, которое изменяет содержимое файла. Поведение меняется от приложения к приложению, поэтому ваш пробег может варьироваться. - person user2729516; 09.12.2017

Если вы пытаетесь сделать то же самое в Scala, используя библиотеку better-files-akka, я придумал этот обходной путь на основе решения, предложенного в принятом ответе.

https://github.com/pathikrit/better-files/issues/313

trait ConfWatcher {

  implicit def actorSystem: ActorSystem

  private val confPath = "/home/codingkapoor/application.conf"
  private val appConfFile = File(confPath)
  private var appConfLastModified = appConfFile.lastModifiedTime

  val watcher: ActorRef = appConfFile.newWatcher(recursive = false)

  watcher ! on(EventType.ENTRY_MODIFY) { file =>
    if (appConfLastModified.compareTo(file.lastModifiedTime) < 0) {
      // TODO
      appConfLastModified = file.lastModifiedTime
    }
  }

}
person iamsmkr    schedule 09.05.2019

Не проверено, но, возможно, это сработает:

AtomicBoolean modifyEventFired = new AtomicBoolean();
modifyEventFired.set(false);

while(true) {
    watchKey = watchService.take(); // blocks

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

        System.out.println(watchEvent.context() + ", count: "+ watchEvent.count() + ", event: "+ watchEvent.kind());
        // prints (loop on the while twice)
        // servers.cfg, count: 1, event: ENTRY_MODIFY
        // servers.cfg, count: 1, event: ENTRY_MODIFY

        switch(kind.name()) {
            case "ENTRY_MODIFY":
                if(!modifyEventFired.get()){
                   handleModify(watchEvent.context()); // reload configuration class
                   modifyEventFired.set(true);                           
                }
                break;
            case "ENTRY_DELETE":
                handleDelete(watchEvent.context()); // do something else
                break;              
        }
    }   
    modifyEventFired.set(false);
    watchKey.reset();       
}
person Robert H    schedule 27.05.2013
comment
Цикл for на самом деле повторяется только один раз. Цикл while повторяется дважды с одним событием в watchKey.pollEvents(), поэтому я не думаю, что это сработает. - person Sotirios Delimanolis; 28.05.2013
comment
Если вы расширите свой цикл for each, вы сможете получить доступ к методу size() списка pollEvents: List<WatchEvent<?>> events = watchKey.pollEvents(); System.out.println(events.size()); Это также возвращает 1 или точно показывает 2 события? Количество watchEvent должно быть 2, если это дубликат, но в этом случае я думаю, что ваши 2 могут лучше отражаться в списке. - person Robert H; 28.05.2013
comment
pollEvents().size() — один, а take() происходит дважды (затем снова блокируется, ждет). Смешно говорить об этом. Существует два события ОС (например, изменение содержимого и изменение метаданных), но только одно событие, связанное с человеком. Я предполагаю, что WatchService API видит это как два WatchKeys с одним WatchEvent в каждом. - person Sotirios Delimanolis; 28.05.2013