(一)Zuul组件如何实现动态过滤

2021-10-28  本文已影响0人  guessguess

在前面有了解到Zuul动态过滤的一个特性。可以让服务在不停机的情况下,对过滤的条件进行变更。
那么这个特性如何使用?首先来讲一下如何使用,最后再讲一下实现原理。

如何使用?

首先需要添加依赖

        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy</artifactId>
        </dependency>

其次编写一个过滤器的实现类

package com.gee.zuulfilter;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY;

import java.util.concurrent.atomic.AtomicInteger;

import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;

import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
public class CustomZuulFilterOne extends com.netflix.zuul.ZuulFilter{
    
    public static AtomicInteger requestCount = new AtomicInteger(0);
    
    @Override
    public boolean shouldFilter() {
        //只拦截通过ribbon进行路由的---此处与ribbon源码一致
        RequestContext ctx = RequestContext.getCurrentContext();
        return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
                && ctx.sendZuulResponse());
    }

    @Override
    public Object run() throws ZuulException {
        requestCount.incrementAndGet();
        System.out.println("CustomZuulFilterOne" + requestCount);
        return null;
    }

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 9999;
    }

}

我编写的demo,其实就是一个简单的统计,统计通过服务id进行路由的请求。
然后过滤器的类型就是pre。前面说过过滤器的类型有,pre,route,post,error这几种。分别在路由的前后环绕执行。随后将过滤器的java文件,放置到接下来指定的一个文件夹(C:\Users\Lenovo\Desktop\custom-zuul-filter)。

如何发现这个服务?

由于动态路由是基于文件扫描的机制,所以需要手动去启用。
那么我是基于服务启动后去做的这个事情。代码如下。

@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
@EnableEurekaClient
public class ZuulApplication  implements CommandLineRunner{
    public static void main(String args[]) {
        //DispatcherServlet
        //ZuulHandlerMapping
        SpringApplication.run(ZuulApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        需要通过过滤器加载器去指定编译器
        FilterLoader.getInstance().setCompiler(new GroovyCompiler());
        通过过滤器文件管理器去指定要扫描的目录(用于存放过滤器代码文件)
        FilterFileManager.init(15, "C:\\Users\\Lenovo\\Desktop\\custom-zuul-filter");
    }
}

最后做一个简单的测试

需要自己搭建一个注册中心,通过zuul服务访问其中的某个服务。


image.png

那么这里说明,过滤器是已经成功加载进去了。

如何新增?

那么再加一个文件。内容如下,再次放到对应的文件夹。

package com.gee.zuulfilter;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY;

import java.util.concurrent.atomic.AtomicInteger;

import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;

import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
public class CustomZuulFilterOne extends com.netflix.zuul.ZuulFilter{
    
    public static AtomicInteger requestCount = new AtomicInteger(0);
    
    @Override
    public boolean shouldFilter() {
        //只拦截通过ribbon进行路由的---此处与ribbon源码一致
        RequestContext ctx = RequestContext.getCurrentContext();
        return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
                && ctx.sendZuulResponse());
    }

    @Override
    public Object run() throws ZuulException {
        requestCount.incrementAndGet();
        System.out.println("CustomZuulFilterOne" + requestCount);
        return null;
    }

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 9999;
    }

}

测试结果如下


image.png

这个时候是俩个过滤器,说明是动态加载的。

如何更新?

其实直接调整文件内容,然后丢回到指定目录即可.下面做一个简单的调整,修改CustomZuulFilterOne的打印内容

package com.gee.zuulfilter;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY;

import java.util.concurrent.atomic.AtomicInteger;

import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;

import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
public class CustomZuulFilterOne extends com.netflix.zuul.ZuulFilter{
    
    public static AtomicInteger requestCount = new AtomicInteger(0);
    
    @Override
    public boolean shouldFilter() {
        //只拦截通过ribbon进行路由的---此处与ribbon源码一致
        RequestContext ctx = RequestContext.getCurrentContext();
        return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
                && ctx.sendZuulResponse());
    }

    @Override
    public Object run() throws ZuulException {
        requestCount.incrementAndGet();
        System.out.println("CustomZuulFilterOne_update" + requestCount);
        return null;
    }

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 9999;
    }
}

通过网关服务再次访问某个服务


image.png

日志也是我们想要的,而旧的日志也不存在了。说明更新成功。

如何删除?

删除其实就比较麻烦,得让同一类型的过滤器列表被清空才可以。而同一类型的过滤器列表被清空则是得通过同类型的过滤器新增或者修改,才会触发过滤器列表被清空。
这里要实现也可以就是比较麻烦。后续源码中会讲到。
删除需要将FilterRegister中的filters对应的对象删除,此外也需要将FilterLoader中的hashFiltersByType对应的列表清空。

实现原理

1.通过FilterFileManager来对过滤器进行扫描以及加载

先来看看FilterFileManager 的结构。

比较简单其实就是通过一个单线程,周期性地去扫描指定的路径。

public class FilterFileManager {
    指定的路径
    String[] aDirectories;
    间隔
    int pollingIntervalSeconds;
    线程
    Thread poller;
    运行的标记
    boolean bRunning = true;
    public static void init(int pollingIntervalSeconds, String... directories) throws Exception, IllegalAccessException, InstantiationException {
        if (INSTANCE == null) INSTANCE = new FilterFileManager();

        INSTANCE.aDirectories = directories;
        INSTANCE.pollingIntervalSeconds = pollingIntervalSeconds;
        INSTANCE.manageFiles();
        INSTANCE.startPoller();
    }
}
下面来看看线程的运行机制。比较简单,死循环,休眠,执行,循环往复。
public class FilterFileManager {
    void startPoller() {
        poller = new Thread("GroovyFilterFileManagerPoller") {
            public void run() {
                while (bRunning) {
                    try {
                        sleep(pollingIntervalSeconds * 1000);
                        管理文件
                        manageFiles();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        poller.setDaemon(true);
        poller.start();
    }

}
线程的工作内容
public class FilterFileManager {
    void manageFiles() throws Exception, IllegalAccessException, InstantiationException {
        先找到目录下的所有文件
        List<File> aFiles = getFiles();
        随后进行处理
        processGroovyFiles(aFiles);
    }
}
如何找到文件呢?
public class FilterFileManager {
    一开始指定的路径
    String[] aDirectories;

    List<File> getFiles() {
        List<File> list = new ArrayList<File>();
        遍历所有路径,找出所有的过滤器文件
        for (String sDirectory : aDirectories) {
            if (sDirectory != null) {
                File directory = getDirectory(sDirectory);
                File[] aFiles = directory.listFiles(FILENAME_FILTER);
                if (aFiles != null) {
                    list.addAll(Arrays.asList(aFiles));
                }
            }
        }
        return list;
    }

    public File getDirectory(String sPath) {
        File  directory = new File(sPath);
        if (!directory.isDirectory()) {
            URL resource = FilterFileManager.class.getClassLoader().getResource(sPath);
            try {
                directory = new File(resource.toURI());
            } catch (Exception e) {
                LOG.error("Error accessing directory in classloader. path=" + sPath, e);
            }
            if (!directory.isDirectory()) {
                throw new RuntimeException(directory.getAbsolutePath() + " is not a valid directory");
            }
        }
        return directory;
    }

}
找到文件后如何处理?
public class FilterFileManager {
    void processGroovyFiles(List<File> aFiles) throws Exception, InstantiationException, IllegalAccessException {
        for (File file : aFiles) {
            FilterLoader.getInstance().putFilter(file);
        }
    }
}

通过FilterLoader进行加载。

2.FilterLoader如何将文件加载

public class FilterLoader {
    单例
    final static FilterLoader INSTANCE = new FilterLoader();
    文件与对应的修改时间的映射
    private final ConcurrentHashMap<String, Long> filterClassLastModified = new ConcurrentHashMap<String, Long>();
    private final ConcurrentHashMap<String, String> filterClassCode = new ConcurrentHashMap<String, String>();
    private final ConcurrentHashMap<String, String> filterCheck = new ConcurrentHashMap<String, String>();
    类型---过滤器列表
    private final ConcurrentHashMap<String, List<ZuulFilter>> hashFiltersByType = new ConcurrentHashMap<String, List<ZuulFilter>>();
    过滤器注册器-用于过滤器的增删改查(针对注册器内部的注册表)
    private FilterRegistry filterRegistry = FilterRegistry.instance();
    动态代码编译器---其实内部就是一个类加载器
    static DynamicCodeCompiler COMPILER;
    
    static FilterFactory FILTER_FACTORY = new DefaultFilterFactory();

    public boolean putFilter(File file) throws Exception {
        sName由路径+文件名组成
        String sName = file.getAbsolutePath() + file.getName();
        如果已经有记录,且修改时间不一致,说明文件更新
        if (filterClassLastModified.get(sName) != null && (file.lastModified() != filterClassLastModified.get(sName))) {
            LOG.debug("reloading filter " + sName);
            注册表移除sName
            filterRegistry.remove(sName);
        }
        ZuulFilter filter = filterRegistry.get(sName);
        如果注册表中不包含该sName,说明该过滤器从来没有注册过/由于更新导致注册表的数据无效被清空。
        if (filter == null) {
            通过编译器获取字节码
            Class clazz = COMPILER.compile(file);
            if (!Modifier.isAbstract(clazz.getModifiers())) {
                通过反射的方式生成对象
                filter = (ZuulFilter) FILTER_FACTORY.newInstance(clazz);
                List<ZuulFilter> list = hashFiltersByType.get(filter.filterType());
                if (list != null) {
                     清空该类型的过滤器列表
                    hashFiltersByType.remove(filter.filterType()); //rebuild this list
                }
                注册
                filterRegistry.put(file.getAbsolutePath() + file.getName(), filter);
                添加修改记录
                filterClassLastModified.put(sName, file.lastModified());
                return true;
            }
        }

        return false;
    }
}

从上面代码其实可以看出,无非就是通过类加载器去加载过滤器对应的java文件,然后生成字节码,最后通过反射生成对象。然后进行注册(添加到注册表中),以及添加修改记录。
另外为什么说删除过滤器比较麻烦,从代码上看的话,其实只支持过滤器的新增以及更新。删除需要另外实现。

那么这里还看到一个点,就是更新的时候,FilterLoader的hashFiltersByType会被清空。但是没关系,因为获取为空的时候会重新生成。

public class FilterLoader {
    public List<ZuulFilter> getFiltersByType(String filterType) {

        List<ZuulFilter> list = hashFiltersByType.get(filterType);
        if (list != null) return list;

        list = new ArrayList<ZuulFilter>();
        获取注册表中的过滤器,进行重新分类,以及排序。
        Collection<ZuulFilter> filters = filterRegistry.getAllFilters();
        for (Iterator<ZuulFilter> iterator = filters.iterator(); iterator.hasNext(); ) {
            ZuulFilter filter = iterator.next();
            if (filter.filterType().equals(filterType)) {
                list.add(filter);
            }
        }
        Collections.sort(list); 
        hashFiltersByType.putIfAbsent(filterType, list);
        return list;
    }
}
最后再来说说编译器如何生成字节码
public class GroovyCompiler implements DynamicCodeCompiler {
    @Override
    public Class compile(File file) throws IOException {
        GroovyClassLoader loader = getGroovyClassLoader();
        Class groovyClass = loader.parseClass(file);
        return groovyClass;
    }
}

那么Zuul实现动态过滤的原理就是大概如此了。


实现原理如下
上一篇 下一篇

猜你喜欢

热点阅读