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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值