Java geek


Related reading:

JAVA programming ideas (1) Increase extensibility through dependency injection (2) How to program for interface (3) Remove the ugly if, self-registration strategy mode elegant meet the open and close principle (4) JAVA programming ideas (Builder mode) classical paradigm and factory mode how to choose? Java Programming Ideas (5) Event Notification Decoupling Process (6) Event Notification Decoupling Process (7) Scenarios using Composition and Inheritance Java Basics (1) Simple and thorough understanding of inner classes and static inner classes Java basics (2) Memory optimization – Using Java references for caching JAVA foundation (3) ClassLoader implementation hot loading JAVA foundation (4) enumeration (enum) and constant definition, factory class use contrast JAVA foundation (5) functional interface – reuse, The sword of decoupling HikariPool source code (2) Design idea borrowed from JetCache source code (1) beginning JetCache source code (2) Top view people in the workplace (1) IT big factory survival rules


1. The demand

Sometimes, you need to monitor file changes for processing. For example, configuration file changes need to be automatically reloaded into memory.

1.1. Requirements analysis

In terms of functionality and scalability, the requirements are as follows:

  1. The file change events to monitor are file deletion and file content change.
  2. File content changes are identified by summary changes rather than timestamp changes. Algorithms such as MD5 and SHA can be used.
  3. Multiple subscribers are allowed to subscribe to file change events. After file change, subscribers will be notified, and subscribers will handle the change events by themselves.

1.2. Coding design

  1. Define a Listener interface for file changes that trigger listener execution
  2. Define a file change event registration class that is used to subscribe to file change events
  3. The time of file changes is uncertain, and the file needs to be scanned for changes through thread polling
  4. The listener may take a long time to execute. To avoid blocking a listener synchronously, you need to invoke the listener asynchronously
  5. Asynchronous invocation uses thread pools to improve efficiency. At the same time, system-level thread pools should be defined for unified management to avoid resource waste caused by self-defining private thread pools

Thus, the following class structure diagram is obtained:

The responsibilities of the classes themselves are pretty clear here, but one wonders if the FileWatcher and FileListenerRegistry classes could be merged into one, for example:

Color {#FF0000}{#FF0000}{color{#FF0000}{#FF0000}}{color{#FF0000}{#FF0000}} \color{#FF0000}{will pay attention to the content that you should not pay attention to, will not comply with the separation of concerns principle. As shown in figure:

Start file change monitoring is usually pulled in the process initialization task, and register the listener is usually done by the business module, neither of which needs to pay attention to the other thing \color{#FF0000}{neither of which needs to pay attention to the other thing}. Therefore, there is no need to merge the two classes.

2. Show me code

Let’s look at the actual code.

2.1. SignatureTool. Java

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/ * * *@ClassName SignatureTool
 * @Description
 * @AuthorSonorous leaf *@DateHe 2021/1/31 *@VersionThe nuggets: 1.0 * https://juejin.cn/user/3544481219739870 * * /
public final class SignatureTool {
    private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();

    /** * Get the file signature *@paramFilePath filePath *@paramAlgorithm Signature algorithm *@returnDocument signature *@throws IOException
     * @throws NoSuchAlgorithmException
     */
    public static String sign(String filePath, Algorithm algorithm) throws IOException, NoSuchAlgorithmException {
        File file = new File(filePath);
        if(! file.exists() || file.isDirectory()) {return "";
        }
        Path path = Paths.get(filePath);
        MessageDigest md = MessageDigest.getInstance(algorithm.name());
        md.update(Files.readAllBytes(path));
        byte[] hash = md.digest();
        return bytes2string(hash);
    }

    private static String bytes2string(byte[] src) {
        char[] hexChars = new char[src.length * 2];
        for(int j = 0; j < src.length; ++j) {
            int v = src[j] & 255;
            hexChars[j * 2] = HEX_ARRAY[v >>> 4];
            hexChars[j * 2 + 1] = HEX_ARRAY[v & 15];
        }
        return newString(hexChars); }}Copy the code

2.2. The Algorithm. The Java

/** * The algorithm is defined through enumeration classes. The purpose is to determine the range of parameters and reduce input errors */
public enum Algorithm {
    MD5("MD5"),
    SHA1("SHA-1"),
    SHA2("SHA-2"),
    SHA3("SHA-3"),
    SHA256("SHA-256"),
    SHA512("SHA-512"),;

    private String value;

    Algorithm(String value) {
        this.value = value; }}Copy the code

2.3. FileListener. Java

/** * file change monitor interface, this defines a general interface, does not break down whether to listen for file deletion or file content change. * The method signatures that listen for file deletions and content changes are the same, both have no arguments and require no return value, and many * places can be reused as an interface (the more abstract the more reusable). * /
public interface FileListener {
    void apply(a);
}
Copy the code

2.4. FileListenerRegistry. Java

import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/ * * *@ClassName FileListenerRegistry
 * @Description
 * @AuthorSonorous leaf *@Date 2021/1/31 13:42
 * @VersionThe nuggets: 1.0 * https://juejin.cn/user/3544481219739870 * * /
public class FileListenerRegistry {
    private static volatile FileListenerRegistry instance;

    /** * singleton, private constructor */
    private FileListenerRegistry(a) {}

    public static FileListenerRegistry getInstance(a) {
        if (instance == null) {
            synchronized (FileListenerRegistry.class) {
                if (instance == null) {
                    instance = newFileListenerRegistry(); }}}return instance;
    }

    private Map<String, String> fileHashValueMap = new ConcurrentHashMap<>();
    private Map<String, Set<FileListener>> changeListenerMap = new ConcurrentHashMap<>();
    private Map<String, Set<FileListener>> deleteListenerMap = new ConcurrentHashMap<>();

    /** * Register file change listener **@paramFilePath filePath *@paramChangeListener file changeListener */
    public void registerChangeListener(String filePath, FileListener changeListener) {
        if(isValidFile(filePath)) { fillFilepathMap(filePath); addListener(filePath, changeListener, changeListenerMap); }}/** * Register file deletion listener *@paramFilePath filePath *@paramDeleteListener file deletes listener */
    public void registerDeleteListener(String filePath, FileListener deleteListener) {
        if(isValidFile(filePath)) { fillFilepathMap(filePath); addListener(filePath, deleteListener, deleteListenerMap); }}public void unRegister(List<String> filePaths) {
        if(filePaths ! =null) {
            for (String filePath: filePaths) {
                System.out.println("[FileWatchService]----------- unRegister, filePath="+ filePath ); fileHashValueMap.remove(filePath); changeListenerMap.remove(filePath); deleteListenerMap.remove(filePath); }}}// Note that the following methods are defined as package-only. Do not extend the methods' accessibility if necessary
    void updateSignature(String filePath, String newSignature) {
        fileHashValueMap.put(filePath, newSignature);
    }

    Set<FileListener> getFileDeleteListeners(List<String> files) {
        return getListeners(files, deleteListenerMap);
    }

    Set<FileListener> getFileChangeListeners(List<String> files) {
        return getListeners(files, changeListenerMap);
    }

    List<String> getWatchFiles(a) {
        List<String> watchFiles = new ArrayList<>();
        if (fileHashValueMap.size() > 0) {
            watchFiles.addAll(fileHashValueMap.keySet());
        }
        return watchFiles;
    }

    String getOriginalSignature(String filePath) {
        return fileHashValueMap.get(filePath);
    }

    private Set<FileListener> getListeners(List<String> files, Map<String, Set<FileListener>> listenerMap) {
        Set<FileListener> fileListeners = null;
        if(files ! =null && files.size() > 0) {
            fileListeners = new HashSet<>();
            for (String file: files) {
                if (listenerMap.containsKey(file)) {
                    // Note: In this case, no locking mechanism is provided because there is no remove method, otherwise the read and write operations would be lockedfileListeners.addAll(listenerMap.get(file)); }}}return fileListeners;
    }

    /** * This method is called twice, reflecting the generality of two change events abstracting only one listener, otherwise two methods ** need to be written@paramFilePath filePath *@paramThe listener file changes the listener *@paramMap Listener map */
    private void addListener(String filePath, FileListener listener, Map<String, Set<FileListener>> map) {
        Set set = null;
        if (map.containsKey(filePath)) {
            set = map.get(filePath);
        } else {
            set = new HashSet();
        }
        set.add(listener);
        map.put(filePath, set);
        System.out.println("[FileWatchService]----------- register listener, filePath=" + filePath + ", className=" + listener.getClass().getSimpleName());
    }

    private String buildSignature(String filePath) {
        String signature = "";
        try {
            signature = SignatureTool.sign(filePath, Algorithm.MD5);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return signature;
    }

    private void fillFilepathMap(String filePath) {
        if (!fileHashValueMap.containsKey(filePath)) {
            String signature = buildSignature(filePath);
            fileHashValueMap.put(filePath, signature);
        }
    }

    private boolean isValidFile(String filePath) {
        File file = new File(filePath);
        if (file.exists() && file.isFile()) {
            return true;
        }
        return false; }}Copy the code

2.5. FileWatcher. Java

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/** * The file change monitor is responsible for monitoring file changes and notifying listeners *@ClassName FileWatcher
 * @Description
 * @AuthorSonorous leaf *@Date 2021/1/31 13:49
 * @VersionThe nuggets: 1.0 * https://juejin.cn/user/3544481219739870 * * /
public class FileWatcher {
    private static final String LOG_PREFIX = FileWatcher.class.getSimpleName();
    private static volatile FileWatcher instance;
    private static AtomicBoolean isStart = new AtomicBoolean(false);

    private FileWatcher(a) {}

    public static FileWatcher getInstance(a) {
        if (instance == null) {
            synchronized (FileWatcher.class) {
                if (instance == null) {
                    instance = newFileWatcher(); }}}return instance;
    }

    public void start(a) {
        if(! isStart.getAndSet(true)) {
            System.out.println(LOG_PREFIX + "-------- start......");
            // Execute periodically
            ThreadFramework.scheduleAtFixedRate("", ()->{
                startMonitor();
            }, 2.5, TimeUnit.SECONDS);

        } else {
            System.out.println(LOG_PREFIX + "-------- The program has been started and will not be started repeatedly."); }}private void startMonitor(a) {
        System.out.println(LOG_PREFIX + "-------- startMonitor be called.");
        List<String> watchFiles = FileListenerRegistry.getInstance().getWatchFiles();
        List<String> deleteFiles = getDeleteFiles(watchFiles);
        if(deleteFiles ! =null && deleteFiles.size() > 0) {
            Set<FileListener> listeners = FileListenerRegistry.getInstance().getFileDeleteListeners(deleteFiles);
            callListener(listeners);
            FileListenerRegistry.getInstance().unRegister(deleteFiles);
        }
        List<String> changeFiles = getChangeFiles(watchFiles);
        if(changeFiles ! =null && changeFiles.size() > 0) { Set<FileListener> listeners = FileListenerRegistry.getInstance().getFileChangeListeners(changeFiles); callListener(listeners); }}private void callListener(Set<FileListener> listeners) {
        if(listeners ! =null && listeners.size() > 0) {
            for (FileListener listener: listeners) {
                System.out.println(LOG_PREFIX + "-------- call listener... className="+ listener.getClass().getSimpleName()); ThreadFramework.submit(listener.getClass().getSimpleName(), ()->{ listener.apply(); }); }}}private List<String> getDeleteFiles(List<String> watchFiles) {
        List<String> deleteFiles = new ArrayList<>();
        int size = watchFiles.size();
        for (int i = 0; i < size; i++) {
            String file = watchFiles.get(i);
            if (!new File(file).exists()) {
                System.out.println(LOG_PREFIX + "-------- file be deleted. file=" + file);
                deleteFiles.add(file);
                // Delete the file from the list, no need to determine whether to changewatchFiles.remove(i); i--; size--; }}return deleteFiles;
    }

    private List<String> getChangeFiles(List<String> watchFiles) {
        List<String> changeFiles = new ArrayList<>();
        for (String file: watchFiles) {
            String originalSignature = FileListenerRegistry.getInstance().getOriginalSignature(file);
            try {
                String currSignature = SignatureTool.sign(file, Algorithm.MD5);
                if(! originalSignature.equals(currSignature)) { System.out.println(LOG_PREFIX +"-------- file be modifyed, old signature=" + originalSignature + ", new signature="+ currSignature); FileListenerRegistry.getInstance().updateSignature(file, currSignature); changeFiles.add(file); }}catch (Exception e) {
                System.out.println(LOG_PREFIX + "-------- get signature error."); e.printStackTrace(); }}returnchangeFiles; }}Copy the code

2.6. ThreadFramework. Java

import java.util.concurrent.*;

/** * Thread framework class. This is just an example. When multiple threads need to be used in a process, a unified thread framework should be used to manage and schedule **@Description
 * @AuthorSonorous leaf *@Date2021/1/31 sets *@VersionThe nuggets: 1.0 * https://juejin.cn/user/3544481219739870 * * /
public final class ThreadFramework {
    private static final int NORMAL_CORE_POOL_SIZE = 10;
    private static ScheduledExecutorService normalExecService = Executors.newScheduledThreadPool(NORMAL_CORE_POOL_SIZE);

    /** * Submit a normal thread task **@paramName Task name *@paramTask *@return* /
    public staticFuture<? > submit(String name, Runnable task) { Thread thread =new Thread(task);
        thread.setName(name);

        return normalExecService.submit(thread);
    }

    /** * Periodic tasks **@paramName Task name *@paramTask *@paramInitialDelay initialDelay *@paramPeriod Execution interval *@paramTimeUnit timeUnit */
    public static void scheduleAtFixedRate(String name, Runnable task, long initialDelay, long period, TimeUnit timeUnit) {
        Thread thread = newThread(task); thread.setName(name); normalExecService.scheduleAtFixedRate(thread, initialDelay, period, timeUnit); }}Copy the code

2.7. TestDemo. Java

public class TestDemo {
    public static void main(String[] args) {
        String file = "d:\\tmp\\a.txt";
        FileListenerRegistry.getInstance().registerChangeListener(file, ()->{
            System.out.println("changeListener 111 be called");
        });

        FileListenerRegistry.getInstance().registerChangeListener(file, ()->{
            System.out.println("changeListener 222 be called");
        });

        FileListenerRegistry.getInstance().registerDeleteListener(file, ()->{
            System.out.println("deleteListener 111 be called");
        });

        FileWatcher.getInstance().start();

        // Verify repeated startupFileWatcher.getInstance().start(); }}Copy the code

2.8. Run logs

Simulate file changes and deletions in sequence.

FileListenerRegistry----------- register listener, filePath=d:\tmp\a.txt, className=TestDemo$$Lambda$1/424058530
FileListenerRegistry----------- register listener, filePath=d:\tmp\a.txt, className=TestDemo$$Lambda$2/471910020
FileListenerRegistry----------- register listener, filePath=d:\tmp\a.txt, className=TestDemo$$Lambda$3/1418481495FileWatcher-------- start...... FileWatcher-------- The program has been started and will not be started repeatedly. FileWatcher-------- startMonitor be  called. FileWatcher-------- startMonitor be called. FileWatcher-------- startMonitor be called. FileWatcher-------- file be modifyed, old signature=83F20A7CFD9F813E9B1C82F10D12AB0B,new signature=2A76DFFA7FD3D39C9649CCAD2EB73363
FileWatcher-------- call listener... className=TestDemo$$Lambda$1/424058530
changeListener 111 be called
FileWatcher-------- call listener... className=TestDemo$$Lambda$2/471910020
changeListener 222 be called
FileWatcher-------- startMonitor be called.
FileWatcher-------- startMonitor be called.
FileWatcher-------- file be deleted. file=d:\tmp\a.txt
FileWatcher-------- call listener... className=TestDemo$$Lambda$3/1418481495
deleteListener 111 be called
FileListenerRegistry----------- unRegister, filePath=d:\tmp\a.txt
Copy the code

3. Good code summary

The advantages and principles of the above code are as follows:

  1. Class clear responsibility division, to follow the single responsibility and the principle of separation of concerns \ color # FF0000} {{class clear responsibility division, to follow the single responsibility principle and separation of concerns such clear responsibility division, to follow the single responsibility and the principle of separation of concerns, according to the method caller role of different classification, don’t let the caller know shouldn’t know.
  2. Abstract FileListener interface, does not distinguish between different events, more general, easy to expand
  3. Strictly control method visible scope \color{#FF0000}{strictly control method visible scope} strictly control method visible scope, do not use public modification at will, public means more external commitment, at the same time lead to the caller focus more, resulting in knowledge explosion
  4. Color {#FF0000}{use enumeration class to find value definition easily, and avoid input error more than constant definition (in this example, if through constant definition, only String constants can be defined, the input parameter is also String, This is valid as long as the input is String. There is no guarantee that the input is in the correct range. Enumerations ensure that the input is in the correct range.
  5. Method abstraction, no duplication of code
  6. Each method, as far as possible control within 20 lines \ color # FF0000 {} {} within each method as far as possible control in 20 lines each method as far as possible control within 20 lines, it is more concise, 2 it is easy to identify and change point in public area, it is often most people ignore, even the people that is known to also can be ignored, Color {#FF0000}{very important} is very important, in addition to the benefits mentioned above, if the methods can be separated, it will also improve the ability to divide the responsibilities of the class.

From 4.

For example purposes, there are still parts that need to be improved, which can be improved according to actual needs.

  1. Exception handling
  2. Log processing
  3. Thread framework classes are designed and implemented according to the actual situation, planned in advance in the project

<– Read the mark, left like!