缓存文件变化 && WatchService监控文件变化分析

来源:互联网 发布:淘宝权女朋友辣椒 编辑:程序博客网 时间:2024/05/18 01:18

这样一个需求:
编写一个缓存池,把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一个线程,就可以解决问题了。