这样一个需求:
编写一个缓存池,把groovy文件每次加载到缓存池中,如果发生了变化,就把新的文件加到缓存池中,如果没变,就使用缓存池中的缓存文件。
我最开始使用静态的map作为缓存池来处理的,一方面是因为map便于查找,另一方面做成单例模式一切就ok。但是在判断文件是否变化的时候,老大觉得有点low,而且耽误时间。代码如下:
import restful.CacheElement;
import transfer.ReloadHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class CacheUtil {
private final static Logger logger = LoggerFactory.getLogger(CacheUtil.class);
private static Map<String, CacheElement> cacheElementMap = new ConcurrentHashMap<>();
private Set<String> fileSet = new LinkedHashSet<>();
private static final CacheUtil cacheUtil = new CacheUtil();
public static CacheUtil getCacheUtil() {
return cacheUtil;
}
/**
* 获取缓存对象,如果缓存对象被修改重新加载最新配置
*
* @param fileName
* @param handler
* @return
* @throws Exception
*/
public Object getCache(String fileName, ReloadHandler handler) throws Exception {
fileName = fileName.trim();
if (isModified(fileName)) {
synchronized (this){
reload(fileName, handler);
}
}
return cacheElementMap.get(fileName).getCache();
}
private boolean isModified(String fileName) {
CacheElement cacheElement = cacheElementMap.get(fileName);
//没有被加载过
if (cacheElement == null) {
return true;
}
//被修改过
if (cacheElement.getFile().lastModified() != cacheElement.getLastEditTime()) {
return true;
}
//没变过
return false;
}
private void reload(String fileName, ReloadHandler handler) throws Exception {
CacheElement cacheElement = cacheElementMap.get(fileName);
if (cacheElement == null || cacheElement.getFile().lastModified() != cacheElement.getLastEditTime()) {
cacheElement = new CacheElement();
cacheElement.setFile(new File(fileName));
cacheElementMap.put(fileName, cacheElement);
if (!fileSet.contains(fileName)) {
fileSet.add(fileName);
}
}
cacheElement.setCache(handler.processNewCache());
cacheElement.setLastEditTime(cacheElement.getFile().lastModified());
}
}
判断文件是否修改过的时候,使用了cacheElement.getFile().lastModified()
,这个方法是大多数人都会使用的方法,但是弊端就是尽管不需要读写文件,但是还要读出文件的配置,获取最后修改时间,这样的方法还是有IO,对于要求较高的系统来说,不是很好的方法。
所以在老大的建议下,我使用了一下的方法。WatcherService,有点类似于观察者模式。下面介绍一下。
WatcherService
这个类的对象就是操作系统原生的文件系统监控器。每个系统都有自己的文件监控器,而这种监控器的实现是不需要比较和遍历的,基于的是信号收发的监控,效率一定最高。这个对于代码是很有帮助的。
Q:我要对文件进行一个监控,如果文件发生了变化就将新文件替换缓存中的文件。
A:这个问题可能第一时间的解决办法就是利用lastModified这个方法进行操作,比较和缓存中文件的时间是否一致。但是,这个方法有一个弊端,虽然不用读文件,但是获取文件配置也是要进行IO的,并不能缩短时间提高效率。这个时候就用上我们的WatchService了。
获取当前操作系统下的文件系统监控器:
watchService = FileSystems.getDefault().newWatchService();
OS上可以开启多个监控器,当然这里也可以开启多个监控器。不同的线程,所以互不影响。
获取了监控器以后,就要将你所要监控的文件夹,注册到监控器上,得让监控器知道监控哪个地方。
Path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
Path就是获取的文件夹的路径,这里可以通过
Path path = Paths.get(String path)//path为文件夹路径
后面的看上去为常量的,是文件的状态,如果在本地跑的话,监控一个文件夹,这几个状态都会出现,是因为打开本地文件(word)本身就会创建出一个副本来,修改副本,并且最后将符文内容与主文件合并,删除副本,所以这几个状态在这里都会出现。
下面就是开始准备监控的过程了。
WatchKey
- 即监控键,说的明确一点就是该文件节点所绑定的监控器的监控信息池,即文件节点的监控池,简称监控池;
- 所有监控到的信息都会放到监控池中;
- register方法返回的就是节点的监控池;
监控池是静态的,它获取的是某一个时间节点下的文件变化信息(上面所说的常量),不能动态的保存。当register发生以后,返回的是一个空的监控池,即使后面发生了文件修改,如果不加以操作,这个监控池还是空的。只有当我们主动的去获取监控池的信息的时候,更新的操作才会放进监控池。
获取下一个信息,就是获取新的监控池
WatchKey watchKey = watchService.take();
这个语句,就是在尝试获取下一个信息,也就是获取新的监控池,如果没有变化的话,就会一直等待,这里可能解决了一些朋友分明写的是while循环,但是一直过不去这条一句的困惑。
WatchKey WatchService.poll(); // 尝试获取下一个变化信息的监控池,如果没有变化则返回null
这个与take的方法,异曲同工,只是用的地方不同。
获取监控池的具体信息
WatchEvent<?> watchEvent : watchKey.pollEvents()
当获取监控池的信息,发现有更新以后,就要获取其中信息到底是什么。这里对应的即为StandardWatchEventKinds对应的几个时间。这里为了严谨,一般采取遍历模式,遍历出此次操作变化的所有事件(例如改变word的例子)
下面就要说一下当发现有了这个事件的变化,我要如何处理了。这里主要提供了两个方法:
WatchEvent.Kind<?> kind = watchEvent.kind();
Path fileName = watchEvent.context();
kind返回了目前在监控池中产生了什么事件,就是最开始定义的几种那样。
context返回了产生这个事件的文件名称
reset
最后需要重置监视器,使用poll或take时监控器线程就被阻塞了,因为你处理文件变化的操作可能需要挺长时间的,为了防止在这段时间内又要处理其他类似的事件,因此需要阻塞监控器线程,而调用reset表示重启该线程;
最后上一下代码:
import transfer.ReloadHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class WatcherServiceUtil extends Thread{
private static final Logger logger = LoggerFactory.getLogger(WatcherServiceUtil.class);
private static WatchService watchService;
public WatcherServiceUtil(Path path) throws IOException {
watchService = FileSystems.getDefault().newWatchService();
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
}
public void run() {
logger.info("deal with the Events");
try {
while (true){
logger.info("while while while while");
WatchKey watchKey = watchService.take();
logger.info("watchKey watchKey watchKey");
for (WatchEvent<?> watchEvent : watchKey.pollEvents()) {
logger.info("enter into the watchEvent");
WatchEvent.Kind<?> kind = watchEvent.kind();
if (kind == StandardWatchEventKinds.OVERFLOW) {
continue;
}
Path fileName = (Path) watchEvent.context();
logger.info("fileName: " + fileName.toString());
if (kind.name().equals("ENTRY_MODIFY")) {
synchronized (this){
CachePoolUtil.getCachePoolUtil().reload(fileName.toString(), new ReloadHandler() {
@Override
public Object processNewCache() throws Exception {
return GroovyUtil.getInstance().newInstance("groovy/" + fileName.toString());
}
});
}
logger.info("Ready to return");
}
}
if (!watchKey.reset()){
break;
}
}
}catch (Exception e){
logger.info("error: " + e.getMessage());
}
}
public static void addListener(String path) throws Exception {
new WatcherServiceUtil(Paths.get(path)).run();
}
}
这里继承了Thread类,主要是因为一般采用了take的连续监控,而不是poll的返回值方式,都是需要一个新的线程持续的监控,所以在主函数里面new一个线程,就可以解决问题了。