Four methods of monitoring file modification

Posted by kikidonc on Tue, 18 Jan 2022 21:48:28 +0100

It is required to monitor whether the configuration file has been modified. Due to the small function scale, Apollo, config and other components are not added, so it has to be implemented by itself


1. Self realization

The first idea is to implement it with timed tasks. The following is the author's implementation idea: FileModifyManager monitors and manages all files. To implement the monitoring interface FileListener and pass it to FileModifyManager, call the monitoring interface method doListener whenever the file changes


1.1 FileListener

@FunctionalInterface
public interface FileListener {
    void doListener();
}

After reading the Hutool document, I know that this design is called hook function, and the author and Hutool have similar ideas



1.2 FileModifyManager

/**
 * @author Howl
 * @date 2022/01/15
 */
public class FileModifyManager {

    // Store the monitored file and FileNodeRunnable node
    private static ConcurrentHashMap<File, FileNodeRunnable> data = new ConcurrentHashMap<>(16);

    // Thread pool performs scheduled listening tasks
    private static ScheduledExecutorService service = Executors.newScheduledThreadPool(20);

    // Singleton mode -- double check lock
    private volatile static FileModifyManager instance = null;

    private FileModifyManager() {
    }

    public static FileModifyManager getInstance() {
        if (instance == null) {
            synchronized (FileModifyManager.class) {
                if (instance == null) {
                    instance = new FileModifyManager();
                }
            }
        }
        return instance;
    }

    // Start listening. The default is once every 10 seconds
    public FileModifyManager startWatch(File file, FileListener fileListener) throws Exception {
        return startWatch(file, fileListener, 0, 1000 * 10, TimeUnit.MILLISECONDS);
    }

    public FileModifyManager startWatch(File file, FileListener fileListener, long delay, long period, TimeUnit timeUnit) throws Exception {
        FileNodeRunnable fileNodeRunnable = addFile(file, fileListener);
        ScheduledFuture<?> scheduledFuture = service.scheduleAtFixedRate(fileNodeRunnable, delay, period, timeUnit);
        fileNodeRunnable.setFuture(scheduledFuture);
        return instance;
    }

    // Stop listening
    public FileModifyManager stopWatch(File file) {
        return stopWatch(file, true);
    }

    public FileModifyManager stopWatch(File file, boolean mayInterruptIfRunning) {
        FileNodeRunnable fileNodeRunnable = data.get(file);
        fileNodeRunnable.getFuture().cancel(mayInterruptIfRunning);
        removeFile(file);
        return instance;
    }

    // Monitor or not
    public boolean isWatching(File file) {
        return containsFile(file);
    }

    // Listening list
    public Set listWatching() {
        return getFileList();
    }


    // Management documents
    private FileNodeRunnable addFile(File file, FileListener fileListener) throws Exception {
        isFileExists(file);
        FileNodeRunnable fileNodeRunnable = new FileNodeRunnable(file, fileListener, file.lastModified());
        data.put(file, fileNodeRunnable);
        return fileNodeRunnable;
    }

    private void removeFile(File file) {
        data.remove(file);
    }

    private boolean containsFile(File file) {
        return data.containsKey(file);
    }

    private Set getFileList() {
        return data.keySet();
    }

    // Judge whether the file exists or not
    private void isFileExists(File file) throws Exception {
        if (!file.exists()) {
            throw new Exception("The file or path does not exist");
        }
    }

    // File nodes and their scheduled tasks
    private class FileNodeRunnable implements Runnable {

        private File file;
        private long lastModifyTime;
        private FileListener listener;
        private ScheduledFuture future;

        FileNodeRunnable(File file, FileListener listener, long lastModifyTime) {
            this.file = file;
            this.listener = listener;
            this.lastModifyTime = lastModifyTime;
        }

        @Override
        public void run() {
            if (this.lastModifyTime != file.lastModified()) {
                System.out.println(file.toString() + " lastModifyTime is " + this.lastModifyTime);
                this.lastModifyTime = file.lastModified();
                listener.doListener();
            }
        }

        public ScheduledFuture getFuture() {
            return future;
        }

        public void setFuture(ScheduledFuture future) {
            this.future = future;
        }
    }
}

Only startWatch, stopWatch and listWatching methods are exposed externally, and the input parameters are File and FileListener

Hutool also uses HashMap to store the corresponding relationship table, so the author's idea is quite clear



1.3 use cases

public class FileTest {
    public static void main(String[] args) throws Exception {

        File file1 = new File("C:\\Users\\Howl\\Desktop\\123.txt");
        File file2 = new File("C:\\Users\\Howl\\Desktop\\1234.txt");

        FileModifyManager manager = FileModifyManager.getInstance();

        manager.startWatch(file1,() -> System.out.println("123.txt The file has changed"))
                .startWatch(file2,() -> System.out.println("1234.txr The file has changed"));
        
    }
}






2. WatchService

WatchService uses the file system of the local operating system to realize the monitoring file directory (monitoring directory), which is available in jdk1 7 introduces a new mechanism under the NIO package, so the use method is also very similar to NIO


The disadvantage of the built-in watchService of JDK is that modifying a file will trigger two events, due to different conditions of the operating system:

  • Modified the meta information and date of the file
  • Copy on write effect, that is, rename the old file and copy the content to the new file

watchService can only monitor the contents of this directory and cannot detect the contents in subdirectories. If monitoring is required, it will traverse and add subdirectories


public class WatchServiceTest {
    public static void main(String[] args) throws IOException, InterruptedException {

        // Directory path, cannot enter file, otherwise an error will be reported
        Path path = Paths.get("C:\\Users\\Howl\\Desktop");

        // Get listening service
        WatchService watchService = FileSystems.getDefault().newWatchService();

        // Register only modification events (as well as creation and deletion)
        path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);

        // monitor
        while (true) {

            // Get the monitored event key
            WatchKey watchKey = watchService.poll(3 * 1000, TimeUnit.MILLISECONDS);

            // The return of poll may be null
            if (watchKey == null) {
                continue;
            }

            // Traverse these events
            for (WatchEvent<?> watchEvent : watchKey.pollEvents()) {
                if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
                    Path watchPath = (Path) watchEvent.context();
                    File watchFile = watchPath.toFile();
                    System.out.println(watchFile.toString() + "The file has been modified");
                }
            }

            // watchKey recovery, used for next monitoring
            watchKey.reset();
        }
    }
}






3. Hutool (recommended)

Hutool is a tool set maintained by Chinese people. Using other people's wheels is always more efficient than building wheels repeatedly (but also understand the design idea of wheels). The bottom layer of hutool still uses WatchService, which solves that modifying files will trigger two events. The idea is that modifications within a certain millisecond range are regarded as the same modification. You can also monitor subdirectories. The idea is recursive traversal


3.1 adding dependencies

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.19</version>
</dependency>

Reference documents Hutool



3.2 example

public class HutoolTest {
    public static void main(String[] args) throws Exception {

        File file = new File("C:\\Users\\Howl\\Desktop\\123.txt");

        // Listening file modification
        WatchMonitor watchMonitor = WatchMonitor.create(file, WatchMonitor.ENTRY_MODIFY);

        // Set hook function
        watchMonitor.setWatcher(new SimpleWatcher() {
            @Override
            public void onModify(WatchEvent<?> event, Path currentPath) {
                System.out.println(((Path) event.context()).toFile().toString() + "Modified");
            }
        });

        // Set the maximum depth of the listening directory. Changes with a directory level greater than the specified level will not be monitored. By default, only the current level directory will be monitored
        watchMonitor.setMaxDepth(1);

        // lsnrctl start 
        watchMonitor.start();
    }
}

The idea is to inherit the Thread class, and then the run method keeps listening to the watchService event in a loop







4. commons-io

Commons IO is a tool set provided by Apache to implement I/O operations


4.1 adding dependencies

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>


4.2 example

After a little look, the observer mode is used

public class CommonsTest {
    public static void main(String[] args) throws Exception {

        // It can only write directories
        String filePath = "C:\\Users\\Howl\\Desktop";

        // Document observer
        FileAlterationObserver observer = new FileAlterationObserver(filePath);

        // Add listening
        observer.addListener(new FileAlterationListenerAdaptor() {
            @Override
            public void onFileChange(File file) {
                System.out.println(file.toString() + "The file has been modified");
            }
        });

        // monitor
        FileAlterationMonitor monitor = new FileAlterationMonitor(10);

        // Add observer
        monitor.addObserver(observer);

        // Start thread
        monitor.start();
    }
}