相关阅读:
JAVA编程思想(一)通过依赖注入增加扩展性
JAVA编程思想(二)如何面向接口编程
JAVA编程思想(三)去掉别扭的if,自注册策略模式优雅满足开闭原则
JAVA编程思想(四)Builder模式经典范式以及和工厂模式如何选?
Java编程思想(五)事件通知模式解耦过程
Java编程思想(六)事件通知模式解耦过程
Java编程思想(七)使用组合和继承的场景
JAVA基础(一)简单、透彻理解内部类和静态内部类
JAVA基础(二)内存优化-使用Java引用做缓存
JAVA基础(三)ClassLoader实现热加载
JAVA基础(四)枚举(enum)和常量定义,工厂类使用对比
JAVA基础(五)函数式接口-复用,解耦之利刃
HikariPool源码(二)设计思想借鉴
【极客源码】JetCache源码(一)开篇
【极客源码】JetCache源码(二)顶层视图
人在职场(一)IT大厂生存法则
1. 需求
有的时候需要监控文件的变更情况做处理,例如配置文件变化需要自动重新加载到内存中。
1.1. 需求分析
从功能和扩展性考虑,需求如下:
- 要监控的文件变更事件有文件删除和文件内容变更。
- 文件内容变更通过文件的摘要信息变化,而不是时间戳变化来识别,算法可以使用MD5, SHA等。
- 允许多个订阅者订阅文件变更事件,文件变更后通知订阅者,由订阅者自行处理变更事件。
1.2. 编码设计
- 给文件变更定义一个listener接口,文件变更后触发listener执行
- 定义一个文件变更事件注册类,用于listener订阅文件变更事件
- 文件变更的时间不确定,需要通过线程轮询扫描文件是否变更
- listener的执行时间可能很长,为了避免某个listener同步阻塞,需要异步调用listener
- 异步调用使用线程池,以提高效率,同时应定义系统级的线程池,统一管理,避免自行定义私有的线程池造成资源浪费
由此得到如下类结构图:
这里类的职责本身挺清晰,但有人会想能否把FileWatcher和FileListenerRegistry两个类合并成一个,例如:
合并之后并不好,因为原来分散在两个类中的方法的,如果合并后不同的调用者。如图:
启动文件变更监控通常是在进程初始化的任务中统一拉起,而注册listener通常是业务模块干的事情,它们,因此者两个类没有必要合并。
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. 好代码总结
以上代码的优点和遵循的原则如下:
- ,根据方法的调用者角色不同来拆分类,不要让调用者知道不该知道的东西。
- 抽象FileListener接口,不区分不同的事件,更通用,易于扩展
- ,不随意使用public修饰,public意味着对外承诺更多,同时导致调用者关注点变多,导致知识爆炸
- ,同时相比定义常量更能避免入参错误(这个例子如果通过常量定义,就只能定义String类型常量,入参也为String类型,这样只要输入String类型就是合法的,并不能保证输入的取值范围正确,而枚举取值一定能保证入参在范围内)
- 方法抽象,没有重复代码
- ,一是更加简洁,二是容易识别出公共部分和变化点,这一点往往多数人都会忽略,即使是明明已经知道的人也会忽略,但这条实际,除了前面说的好处,如果方法都能拆开,还能提高类职责的划分能力。
4. 遗留
出于示例目的,尚有部分需要完善的地方,可自行根据实际需要完善。
- 异常的处理
- 日志处理
- 线程框架类要根据实际情况设计和实现,在项目中提前规划好