【好代码进阶】(一)文件变更监控框架

940 阅读3分钟

banner窄.png

铿然架构  |  作者  /  铿然一叶 这是铿然架构的第 83 篇原创文章

相关阅读:

JAVA编程思想(一)通过依赖注入增加扩展性
JAVA编程思想(二)如何面向接口编程
JAVA编程思想(三)去掉别扭的if,自注册策略模式优雅满足开闭原则
JAVA编程思想(四)Builder模式经典范式以及和工厂模式如何选?
Java编程思想(五)事件通知模式解耦过程
Java编程思想(六)事件通知模式解耦过程
Java编程思想(七)使用组合和继承的场景
JAVA基础(一)简单、透彻理解内部类和静态内部类
JAVA基础(二)内存优化-使用Java引用做缓存
JAVA基础(三)ClassLoader实现热加载
JAVA基础(四)枚举(enum)和常量定义,工厂类使用对比
JAVA基础(五)函数式接口-复用,解耦之利刃
HikariPool源码(二)设计思想借鉴
【极客源码】JetCache源码(一)开篇
【极客源码】JetCache源码(二)顶层视图
人在职场(一)IT大厂生存法则


1. 需求

有的时候需要监控文件的变更情况做处理,例如配置文件变化需要自动重新加载到内存中。

1.1. 需求分析

从功能和扩展性考虑,需求如下:

  1. 要监控的文件变更事件有文件删除和文件内容变更。
  2. 文件内容变更通过文件的摘要信息变化,而不是时间戳变化来识别,算法可以使用MD5, SHA等。
  3. 允许多个订阅者订阅文件变更事件,文件变更后通知订阅者,由订阅者自行处理变更事件。

1.2. 编码设计

  1. 给文件变更定义一个listener接口,文件变更后触发listener执行
  2. 定义一个文件变更事件注册类,用于listener订阅文件变更事件
  3. 文件变更的时间不确定,需要通过线程轮询扫描文件是否变更
  4. listener的执行时间可能很长,为了避免某个listener同步阻塞,需要异步调用listener
  5. 异步调用使用线程池,以提高效率,同时应定义系统级的线程池,统一管理,避免自行定义私有的线程池造成资源浪费

由此得到如下类结构图:

这里类的职责本身挺清晰,但有人会想能否把FileWatcher和FileListenerRegistry两个类合并成一个,例如:

合并之后并不好,因为原来分散在两个类中的方法的调用者不同\color{#FF0000}{调用者不同},如果合并后不同的调用者会关注到自己不该关注的内容,不符合分离关注点原则\color{#FF0000}{会关注到自己不该关注的内容,不符合分离关注点原则}。如图:

启动文件变更监控通常是在进程初始化的任务中统一拉起,而注册listener通常是业务模块干的事情,它们都不需要关注对方所干的事情\color{#FF0000}{都不需要关注对方所干的事情},因此者两个类没有必要合并。

2. Show me 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
 * @Author 铿然一叶
 * @Date 2021/1/31 14:37
 * @Version 1.0
 * 掘金:https://juejin.im/user/5d48557d6fb9a06ae071e9b0
 **/
public final class SignatureTool {
    private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();

    /**
     * 获取文件签名
     * @param filePath 文件路径
     * @param algorithm 签名算法
     * @return 文件签名
     * @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 new String(hexChars);
    }
}

2.2. Algorithm.java

/**
 * 通过枚举类定义算法,目的是确定参数范围,减少输入错误
 */
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;
    }
}

2.3. FileListener.java

/**
 * 文件变更监听接口,这里定义了一个通用的接口,没有细分是监听文件删除还是监听文件内容变更。
 * 监听文件删除和内容变更的方法签名是相同的,都是无参且不需要返回值,定义为一个接口的话很多
 * 地方都能被复用(越抽象复用度越高)。
 */
public interface FileListener {
    void apply();
}

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
 * @Author 铿然一叶
 * @Date 2021/1/31 13:42
 * @Version 1.0
 * 掘金:https://juejin.im/user/5d48557d6fb9a06ae071e9b0
 **/
public class FileListenerRegistry {
    private static volatile FileListenerRegistry instance;

    /**
     * 单例模式,私有构造器
     */
    private FileListenerRegistry() {}

    public static FileListenerRegistry getInstance() {
        if (instance == null) {
            synchronized (FileListenerRegistry.class) {
                if (instance == null) {
                    instance = new FileListenerRegistry();
                }
            }
        }
        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<>();

    /**
     * 注册文件变更监听者
     *
     * @param filePath 文件路径
     * @param changeListener 文件变更监听者
     */
    public void registerChangeListener(String filePath, FileListener changeListener) {
        if (isValidFile(filePath)) {
            fillFilepathMap(filePath);
            addListener(filePath, changeListener, changeListenerMap);
        }
    }

    /**
     * 注册文件删除监听者
     * @param filePath 文件路径
     * @param deleteListener 文件删除监听者
     */
    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);
            }
        }
    }

    // 注意下面几个方法都定义为仅包内可访问,如果没有必要,不要扩大方法的可访问范围
    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() {
        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)) {
                    // 注:就则个例子而言,没有提供加锁机制,因为没有remove方法,否则读写操作应加锁
                    fileListeners.addAll(listenerMap.get(file));
                }
            }
        }
        return fileListeners;
    }

    /**
     * 这个方法被调用了两次,体现出两个变更事件只抽象出一个listener的通用性,否则需要写两个方法
     *
     * @param filePath 文件路径
     * @param listener 文件变更监听者
     * @param map 监听者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;
    }
}

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;

/**
 * 文件变更监控着,负责监控文件变更,并通知Listener
 * @ClassName FileWatcher
 * @Description
 * @Author 铿然一叶
 * @Date 2021/1/31 13:49
 * @Version 1.0
 * 掘金:https://juejin.im/user/5d48557d6fb9a06ae071e9b0
 **/
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() {}

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

    public void start() {
        if (!isStart.getAndSet(true)) {
            System.out.println(LOG_PREFIX + "-------- start......");
            // 定期循环执行
            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() {
        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);
                // 从文件列表里删除,删除后不用再判断是否变更
                watchFiles.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();
            }
        }
        return changeFiles;
    }
}

2.6. ThreadFramework.java

import java.util.concurrent.*;

/**
 * 线程框架类,这里只是一个示例,当进程中需要使用多个线程时,应通过统一的线程框架来管理和调度
 *
 * @Description
 * @Author 铿然一叶
 * @Date 2021/1/31 15:05
 * @Version 1.0
 * 掘金:https://juejin.im/user/5d48557d6fb9a06ae071e9b0
 **/
public final class ThreadFramework {
    private static final int NORMAL_CORE_POOL_SIZE = 10;
    private static ScheduledExecutorService normalExecService = Executors.newScheduledThreadPool(NORMAL_CORE_POOL_SIZE);

    /**
     * 提交普通线程任务
     *
     * @param name 任务名称
     * @param task 任务
     * @return
     */
    public static Future<?> submit(String name, Runnable task) {
        Thread thread = new Thread(task);
        thread.setName(name);

        return normalExecService.submit(thread);
    }

    /**
     * 周期性任务
     *
     * @param name 任务名称
     * @param task 任务
     * @param initialDelay 首次延迟时间
     * @param period 执行间隔时间
     * @param timeUnit 时间单位
     */
    public static void scheduleAtFixedRate(String name, Runnable task, long initialDelay, long period, TimeUnit timeUnit) {
        Thread thread = new Thread(task);
        thread.setName(name);
        normalExecService.scheduleAtFixedRate(thread, initialDelay, period, timeUnit);
    }
}

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();

        // 验证重复启动
        FileWatcher.getInstance().start();
    }
}

2.8. 运行日志

先后模拟文件变更和删除。

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/1418481495
FileWatcher-------- 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

3. 好代码总结

以上代码的优点和遵循的原则如下:

  1. 类职责划分清晰,遵循单一职责和分离关注点原则\color{#FF0000}{类职责划分清晰,遵循单一职责和分离关注点原则},根据方法的调用者角色不同来拆分类,不要让调用者知道不该知道的东西。
  2. 抽象FileListener接口,不区分不同的事件,更通用,易于扩展
  3. 严格控制方法可见范围\color{#FF0000}{严格控制方法可见范围},不随意使用public修饰,public意味着对外承诺更多,同时导致调用者关注点变多,导致知识爆炸
  4. 使用枚举类方便查找取值定义\color{#FF0000}{使用枚举类方便查找取值定义},同时相比定义常量更能避免入参错误(这个例子如果通过常量定义,就只能定义String类型常量,入参也为String类型,这样只要输入String类型就是合法的,并不能保证输入的取值范围正确,而枚举取值一定能保证入参在范围内)
  5. 方法抽象,没有重复代码
  6. 每个方法尽可能控制在20行以内\color{#FF0000}{每个方法尽可能控制在20行以内},一是更加简洁,二是容易识别出公共部分和变化点,这一点往往多数人都会忽略,即使是明明已经知道的人也会忽略,但这条实际非常重要\color{#FF0000}{非常重要},除了前面说的好处,如果方法都能拆开,还能提高类职责的划分能力。

4. 遗留

出于示例目的,尚有部分需要完善的地方,可自行根据实际需要完善。

  1. 异常的处理
  2. 日志处理
  3. 线程框架类要根据实际情况设计和实现,在项目中提前规划好

<--阅过留痕,左边点赞!