Spring Cloud之Zuul源码剖析

1.前言

写本篇文章的起因是最近想在业务层面做一个类似网关的应用,把和外部对接的一些相似逻辑抽取到网关中,和具体的业务剥离开来。
在做这件事情之前,发现自己对于网关的理解不是很深,于是便找了业界比较流行的网关框架作一番学习。选取了Spring Cloud全家桶中的Zuul作为样例。

2.Zuul是什么

Zuul is an edge service that provides dynamic routing, monitoring, resiliency, security, and more.
Zuul是Netflix公司开源的一个API网关组件。提供了认证&鉴权、限流、动态路由,监控,弹性,安全、负载均衡、协助单点压测、静态响应等边缘服务的框架。
Zuul is the front door for all requests from devices and web sites to the backend of the Netflix streaming application. As an edge service application, Zuul is built to enable dynamic routing, monitoring, resiliency and security. It also has the ability to route requests to multiple Amazon Auto Scaling Groups as appropriate.
以上是在github上的Zuul项目主页截取的一段内容,介绍Zuul是什么。Zuul在Netflix的架构图如下:


image.png

3.Zuul怎么用

Zuul相关案例大家可以参考http://www.ityouknow.com/spring-cloud.html中相关的Zuul章节,或者其他相关博客,本文不在赘述。

4.Zuul源码剖析

Zuul架构图.png

如上图所示,Zuul内部的整体架构和流程可以用一张图描述清楚。
Zuul主要包含两个流程,一个是请求经过Zuul的执行流程;另一个是Zuul过滤器的加载流程。
Zuul中请求流程大致如下,请求经过ZuulServlet,ZuulServlet持有一个ZuulRunner对象,ZuulRunner会初始化一个RequestContext对象,该对象持有整个请求过程中的上下文数据,被所有过滤器锁共享。ZuulRunner会执行具体的pre、routing、post以及error类型的过滤器,执行完成之后,返回返回具体的HttpResponse对象。
Zuul过滤器加载的大致流程如下,ZuulServerAutoConfiguration中有一个ZuulFilterConfiguration定义,该定义中会注册一个ZuulFilterInitializer的实例。ZuulFilterInitializer初始化一些Zuul组件,比如过滤器,FilterLoader,FilterRegistry。FilterRegistry管理了一个ConcurrentHashMap,用作存储过滤器的,并有一些基本的CURD过滤器的方法。FilterLoader是Zuul的一个核心类。它用来在源码发生变化时编译、载入和校验过滤器,同时它通过类型来持有过滤器。FilterLoader类持有FilterRegistry,FilterFileManager类持有FilterLoader,所以最终是由FilterFileManager注入 filterFilterRegistry的ConcurrentHashMap的。FilterFileManager开启了轮询机制,定时的去加载过滤器。FilterFileManager管理目录和轮询修改和新的Groovy 过滤器,其中的startPoller方法负责轮询。

4.1下面开始探索详细的代码

以下代码基于spring-cloud-netflix-core 1.4.2.RELEASE和zuul-core 1.3.0版本。

ZuulApplication

@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }
}

以上是项目开始Zuul的入口,通过@EnableZuulProxy注解开启Zuul。

EnableZuulProxy

@EnableCircuitBreaker
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}

该类直接关联到ZuulProxyMarkerConfiguration的定义。

ZuulProxyMarkerConfiguration

@Configuration
public class ZuulProxyMarkerConfiguration {
    @Bean
    public Marker zuulProxyMarkerBean() {
        return new Marker();
    }

    class Marker {
    }
}

该类负责新增一个标记bean来触发ZuulProxyAutoConfiguration对象。
在ZuulProxyAutoConfiguration申明上可以看到ZuulProxyMarkerConfiguration。

ZuulProxyAutoConfiguration

该类的源码中涉及了一些服务发现、路由相关的对象以及一些bean的注册,我们主要看下该类的父类。

ZuulServerAutoConfiguration

    @Autowired
    protected ZuulProperties zuulProperties;

根据约定规则读取配置。

    @Bean
    @ConditionalOnMissingBean(name = "zuulServlet")
    public ServletRegistrationBean zuulServlet() {
        ServletRegistrationBean servlet = new ServletRegistrationBean(new ZuulServlet(),
                this.zuulProperties.getServletPattern());
        // The whole point of exposing this servlet is to provide a route that doesn't
        // buffer requests.
        servlet.addInitParameter("buffer-requests", "false");
        return servlet;
    }

ZuulServlet的bean注册。

    @Bean
    public ApplicationListener<ApplicationEvent> zuulRefreshRoutesListener() {
        return new ZuulRefreshListener();
    }

注册路由刷新的监听器bean.

    private static class ZuulRefreshListener
            implements ApplicationListener<ApplicationEvent> {

        @Autowired
        private ZuulHandlerMapping zuulHandlerMapping;

        private HeartbeatMonitor heartbeatMonitor = new HeartbeatMonitor();

        @Override
        public void onApplicationEvent(ApplicationEvent event) {
            if (event instanceof ContextRefreshedEvent
                    || event instanceof RefreshScopeRefreshedEvent
                    || event instanceof RoutesRefreshedEvent) {
                this.zuulHandlerMapping.setDirty(true);
            }
            else if (event instanceof HeartbeatEvent) {
                if (this.heartbeatMonitor.update(((HeartbeatEvent) event).getValue())) {
                    this.zuulHandlerMapping.setDirty(true);
                }
            }
        }

    }

监听器的类定义。

    @Bean
    public ServletDetectionFilter servletDetectionFilter() {
        return new ServletDetectionFilter();
    }

过滤器的bean注册。

   @Configuration
    protected static class ZuulFilterConfiguration {

        @Autowired 
        private Map<String, ZuulFilter> filters;

        @Bean
        public ZuulFilterInitializer zuulFilterInitializer(
                CounterFactory counterFactory, TracerFactory tracerFactory) {
            FilterLoader filterLoader = FilterLoader.getInstance();
            FilterRegistry filterRegistry = FilterRegistry.instance();
            return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry);
        }

    }

本段代码比较关键,引入了过滤器加载的流程。

ZuulFilterInitializer

初始化一些Zuul组件,比如过滤器,FilterLoader,FilterRegistry。

FilterRegistry

private final ConcurrentHashMap<String, ZuulFilter> filters = new ConcurrentHashMap<String, ZuulFilter>();

管理了一个ConcurrentHashMap,用作存储过滤器的,并有一些基本的CURD过滤器的方法。

FilterLoader

这是Zuul的一个核心类。它用来在源码变化时编译、载入和校验过滤器。同时它通过类型来持有过滤器。
FilterLoader类持有FilterRegistry,FilterFileManager类持有FilterLoader,所以最终是由FilterFileManager注入 filterFilterRegistry的ConcurrentHashMap的。FilterFileManager开启了轮询机制,定时的去加载过滤器,

    public boolean putFilter(File file) throws Exception {
        String sName = file.getAbsolutePath() + file.getName();
        if (filterClassLastModified.get(sName) != null && (file.lastModified() != filterClassLastModified.get(sName))) {
            LOG.debug("reloading filter " + sName);
            filterRegistry.remove(sName);
        }
        ZuulFilter filter = filterRegistry.get(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;
    }

以上是从File对象中读取过滤器的定义,并将过滤器的实例放入filterRegistry中管理。

FilterFileManager

这个类管理目录和轮询修改和新的Groovy 过滤器。轮询的间隔和目录在类初始化时候指定,一个轮询器会检查修改和新增。
以下是轮询过滤器的的代码:

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();
}

Zuulservlet

除了过滤器加载的流程,另外一个重要的流程是请求执行的流程,详细的代码逻辑如下。

    @Override
    public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

            // Marks this request as having passed through the "Zuul engine", as opposed to servlets
            // explicitly bound in web.xml, for which requests will not have the same data attached
            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();

            try {
                preRoute();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                route();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                postRoute();
            } catch (ZuulException e) {
                error(e);
                return;
            }

        } catch (Throwable e) {
            error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    }

init方法用来初始化给Request和Response对象做封装,然后是具体的过滤器执行逻辑,依次是pre、route、post过滤器,如果执行过程中抛出异常会执行error过滤器。

ZuulFilter

Zuul中所有的过滤器都会继承ZuulFilter抽象类,该类中有4个主要方法。

abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();
Object run();

filterType:该函数需要返回一个字符串来代表过滤器的类型,而这个类型就是在HTTP请求过程中定义的各个阶段。在Zuul中默认定义了四种不同生命周期的过滤器类型,具体如下:
pre:可以在请求被路由之前调用。
routing:在路由请求时候被调用。
post:在routing和error过滤器之后被调用。
error:处理请求时发生错误时被调用。

filterOrder:通过int值来定义过滤器的执行顺序,数值越小优先级越高。数值可以重复。

shouldFilter:返回一个boolean类型来判断该过滤器是否要执行。我们可以通过此方法来指定过滤器的有效范围。isFilterDisabled也会做判断。

run:过滤器的具体逻辑。在该函数中,我们可以实现自定义的过滤逻辑,来确定是否要拦截当前的请求,不对其进行后续的路由,或是在请求路由返回结果之后,对处理结果做一些加工等。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容