自研框架(Webx)整合Zuul网关工作总结

Posted 默辨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自研框架(Webx)整合Zuul网关工作总结相关的知识,希望对你有一定的参考价值。

写在前面,最近被分配了一个技术任务,简单描述为自研框架(类比Spring)整合一个微服务网关,并且能用就行。

有人可能会问,想用微服务网关,不是直接引入zuul或者gateway相关的依赖,然后配置一下不就好了吗?为什么我还会写这篇博客。这里原因主要有两个:

  1. 框架是自研的,虽然底层也是基于Spring构建,但Spring MVC部分完全不一样,执行流程需要研究
  2. 这些微服务网关都是基于Spring Boot的自动装配机制实现的(根据名字其实我们也不难理解,毕竟都是spring-cloud-xxx),而我们的项目还停留在Spring,所以直接引入肯定不能用

于是就有了这次任务,收获还是挺大的,特此用写下这篇博客。



文章目录

一、明确调研中间件范围

既然目的是完成类似网关一样的请求转发,那么我们就需要知道被网关请求转发的请求打进来,该请求是如何流转的,然后直接调研对应的技术栈就可以了

1、Tomcat

项目使用原生的Tomcat做为Web容器,所以这块的变化不大。


2、Spring MVC

由于Spring MVC的DispatcherServlet是一个Servlet(Tomcat的扩展点大概率要么Filter、要么Servlet),所以想看看在这里能不能发现点什么(如果要使用gateway就需要去调研Dispatcherhanding)


3、Webx

类比Spring,任务也就是整合WebX和zuul,所以Webx必不可少


4、Zuul

本以为师兄口中的调研,是想让我了解所有的微服务网关中间件,然后选择其中一个。没想到是直接调研zuul的源码实现,直接看zuul的就行(在后来看来,使用zuul做为微服务网关是最符合当前项目技术栈,也是最简单的)





二、阅读中间件源码

1、Tomcat

项目使用Tomcat做为Web容器,好在我之前研究过,有兴趣的小伙伴可以参考我之前的文章。

浅谈Tomcat的启动流程

浅谈Tomcat接收到一个请求后在其内部的执行流程

Tomcat组件架构图梳理

针对Tomcat其实重点放在Filter和Servlet上就行了。



2、Spring MVC

这个大家都不陌生。同样,如果不熟悉的小伙伴可以参考我之前的文章

浅谈SpringMVC源码的DispatcherServlet组件执行流程

我们在使用Spring Boot构建Web项目的时候,其内部都是集成了Spring MVC,所以一个请求都会经过DispatcherServlet,然后经过该组件完成请求的转发,但是它本质上只是一个Servlet,明确这一点很重要。Servlet的调用则由Tomcat决定。研究该组件的初衷是为了获得一些灵感,说不定在什么地方就有可以借鉴的思路(结果是没有)



3、Webx

由于代码太老了,Download Resources的时候提示找不到资源,后来去内部的nexus服务器上看,确实没有源码,于是只能找文章学习了。但是就像我之前说的那样,Tomcat暴露在外的除了一些参数配置,还有就是一些扩展接口,如Filter、Servlet、甚至还有Value,基本这些大差不差,Webx必然是在这些扩展点进行的整合。

于是顺藤摸瓜,终于发现了响应的处理逻辑。Webx在完成请求转发的时候,和SpringMVC不一样,它完成内部业务逻辑的入口不是一个Servlet,而是一个Filter。最开始看到还是有点担心的,毕竟和自己预期的结果有点远,知道入口就好办了。



4、Zuul

最让我意外的还是zuul。前文有说过,由于项目不是Spring Boot,所以没办法用Spring Cloud那一套,于是只能从最原生的地方动刀。可是查看了zuul的源码发现,原生的zuul并不提供什么能力(哪怕有一个请求转发也好呀),他真的就是什么都没有,我不禁陷入沉思,它凭什么呀,希望有一天能明白其中的奥秘。

在原生zuul的代码里,它只定义了一套规范。使用一个抽象类ZuulFilter串连接起了整个链路(就是一个责任链,想到责任链就可以知道原生zuul的核心骨架了)。简单理解为zuul将这些链分为pre、post、error、route四种类型,然后代码借助Groovy的能力,可以热加载对应的ZuulFilter。想具体研究zuul源码的,这里分享下我百度时找到的写的不错的讲解的博客:zuul源码分析-探究原生zuul的工作原理


那么问题来了,既然zuul只有一个模板,那请求转发的动作是谁做的呢?我只能将目光投向了spring-cloud-netflix-zuul。



5、spring-cloud-netflix-zuul

果然,在对应的代码里我找到了他们,同样,为了避免重复造轮子,直接贴图(毕竟这也不是本文的重点)

前面说到,原生zuul里面的ZuulFilter分为四类,这四类ZuulFilter在spring-cloud-netflix-zuul中的实现可以浓缩为下面这张图。具体的执行流程按照原生zuul的逻辑(pre-route-post)

上图来自别人的博客,要看详细源码分析的,可以参考该篇文章SpringCloud源码剖析-Zuul的自动配置和核心Filter详解


至此,代码看完了,接着需要想想如何设计了





三、确定设计方案

1、Spring Boot结合netflix-zuul请求流程

在阅读了上面源码的基础上,我们很容易就可以得到这张流程图。ZuulServlet替代了DispatcherServlet的能力,请求直接调用到ZuulServlet类中,然后完成相关ZuulFilter的责任链调用,这里就会调用到SimpleHostRoutingFilter,该ZuulFilter就是完成远程请求调用的具体实现类,其底层使用的是HttpClient(2.x使用了Netty)


2、当前项目请求流程

研究框架源码发现,当前框架和Tomcat接壤的地方并不是一个Servlet,而是一个Filter,业务代码在该Filter内部完成。


3、初版方案

最开始的设计方案是,我们将未来需要网关转发的请求配置到一个Filter的拦截路径中,并且该拦截器配置在WebxFilter前面,如果当前请求能够匹配到我们的ZuulServletFilter,则说明当前请求是需要网关转发的,那么就可以在内部完成响应的请求转发,即调用到我们的SimpleHostRoutingFilter类,然后远程调用获取结果,最后返回。

这一版是我给出的解决方案,确实我们最开始也是这么做的,并且代码已经实现完成,请求跳转数据获取一切正常。不过后来师兄调整了一版本,想想师兄的确实要优雅一点,尽管换汤不换药(不过这个优化需要基于不同的zuul版本,比如我自己电脑下载1.0.28就没有这个判断,但是1.3.1就有)

我们中间还尝试过想直接借助SendForwardFilter来完成本地请求的转发,后来发现不太行,所以放弃了。

这两个的区别就是如何Request上下文对象的sendZuulResponse属性是false,那么就会结束当前ZuulServletFilter,继续执行其他Filter链,基于此,师兄做了一点小优化


补充:Zuul想的还是比较全面,它既有基于Filter的ZuulServletFilter,也有基于Servlet的ZuulServlet,功能一模一样。由于当前项目的业务处理是基于Filter的,常规的Spring Boot项目是基于Servlet的(自己最开始调试的时候没有注意到这一点,进入了误区,后来师兄在用ZuulServletFilter的时候,自己才恍然大悟)



4、优化方案

同样还是新建一个ZuulServletFilter,此时不再是拦截具体路径,而是拦截所有的路径(ZuulServletFilter的位置一定WebxFilter位置的前面)。与此同时,我们新建一个pre类型的ZuulFilter,用来判断当前请求是否在我们配置的网关转发请求范围内,如果在就继续向下走,如果不在就给对应的属性设置为false,上面有说到,如果属性判断为false,那么就会跳出当前ZuulServletFilter,继而完成Tomcat中的其他Filter,这里其他的Filter就包含了处理当前项目请求接口的Filter(WebxFilter)





四、完成项目编码

前面的设计方案已经确定,那么接下来就是代码落地了。最核心的问题就是:在没有Spring Boot自动装配机制的情况下,如何初始化对象。在我看来这是最麻烦的一步,因为用到的那几个ZuulServletFilter内部像套娃一样,一个Bean又套了另一个Bean,没完没了。当然这一切很大程度是因为自己陷入了一个误区,我总担心差这差那,导致各种判断不通过,然后出现奇奇怪怪的异常。以至于我在找Bean的时候,被它源码牵着鼻子走,它缺一个Bean,我就去看它是什么地方赋值的,怎么赋值的,赋值条件又是什么,结果就是转了一大圈,把自己转晕了,甚至觉得这是个无底洞。

不得不说,前辈就是前辈,秉承着一切从简的原则,师兄在找Bean的时候就是看它能用什么Bean就用什么Bean,哪个Bean需要的参数少,哪个Bean拿着方便就用哪个类型的Bean。这一套操作下来只需要配置11个Bean就配置完成了。我忍不住感叹了一句——确实,师兄只是轻描淡写的来了一句:都是经验。

最终呈现在Git提交记录上的代码只有这三部分:

1、添加pom依赖

<dependency>
    <groupId>com.netflix.zuul</groupId>
    <artifactId>zuul-core</artifactId>
</dependency>
<dependency>
    <groupId>com.netflix.hystrix</groupId>
    <artifactId>hystrix-core</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-context</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-netflix-core</artifactId>
</dependency>
<dependency>
    <groupId>commons-configuration</groupId>
    <artifactId>commons-configuration</artifactId>
</dependency>

2、使用xml配置Bean(Webx不支持@Bean)

<bean id="checkRoute" class="org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute">
    <property name="path" value="/check/**"/>
    <property name="url" value="$zuul_routes_money_url"/>
    <property name="stripPrefix" value="false"/>
</bean>
<bean id="moneyRoute" class="org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute">
    <property name="path" value="/money/**"/>
    <property name="url" value="$zuul_routes_money_url"/>
    <property name="stripPrefix" value="false"/>
</bean>

<bean id="zuulRoutes" class="java.util.HashMap">
    <constructor-arg>
        <map key-type="java.lang.String"
             value-type="org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute">
            <entry key="check" value-ref="checkRoute"/>
            <entry key="money" value-ref="moneyRoute"/>
        </map>
    </constructor-arg>
</bean>

<bean id="zuulProperties" class="org.springframework.cloud.netflix.zuul.filters.ZuulProperties">
    <property name="ignoredServices" value="*"/>
    <property name="sensitiveHeaders" value=""/>
    <property name="addHostHeader" value="true"/>
    <property name="routes" ref="zuulRoutes"/>
</bean>

<bean id="proxyRequestHelper" class="org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper"/>

<bean id="routeLocator" class="org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator">
    <constructor-arg index="0" value="/"/>
    <constructor-arg index="1" ref="zuulProperties"/>
</bean>

<bean id="preDecorationFilter" class="org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter">
    <constructor-arg index="0" ref="routeLocator"/>
    <constructor-arg index="1" value="/"/>
    <constructor-arg index="2" ref="zuulProperties"/>
    <constructor-arg index="3" ref="proxyRequestHelper"/>
</bean>

<bean id="xxxPreRoutingFilter" class="pers.mobian.web.filter.xxxPreRoutingFilter">
    <constructor-arg index="0" ref="routeLocator"/>
</bean>

<bean id="simpleHostRoutingFilter"
      class="org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter">
    <constructor-arg index="0" ref="proxyRequestHelper"/>
    <constructor-arg index="1" ref="zuulProperties"/>
    <constructor-arg index="2">
        <bean class="org.springframework.cloud.commons.httpclient.DefaultApacheHttpClientConnectionManagerFactory"/>
    </constructor-arg>
    <constructor-arg index="3">
        <bean class="org.springframework.cloud.commons.httpclient.DefaultApacheHttpClientFactory"/>
    </constructor-arg>
</bean>

<bean id="sendResponseFilter" class="org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter">
    <constructor-arg index="0" ref="zuulProperties"/>
</bean>

<bean id="zuulFilters" class="java.util.HashMap">
    <constructor-arg>
        <map key-type="java.lang.String" value-type="com.netflix.zuul.ZuulFilter">
            <entry key="xxxPreRoutingFilter" value-ref="xxxPreRoutingFilter"/>
            <entry key="preRoutingFilter" value-ref="preRoutingFilter"/>
            <entry key="simpleHostRoutingFilter" value-ref="simpleHostRoutingFilter"/>
            <entry key="sendResponseFilter" value-ref="sendResponseFilter"/>
        </map>
    </constructor-arg>
</bean>

<bean id="zuulFilterInitializer" class="org.springframework.cloud.netflix.zuul.ZuulFilterInitializer">
    <constructor-arg index="0" ref="zuulFilters"/>
    <constructor-arg index="1">
        <bean class="org.springframework.cloud.netflix.zuul.metrics.EmptyCounterFactory"/>
    </constructor-arg>
    <constructor-arg index="2">
        <bean class="org.springframework.cloud.netflix.zuul.metrics.EmptyTracerFactory"/>
    </constructor-arg>
    <constructor-arg index="3">
        <bean class="com.netflix.zuul.FilterLoader" factory-method="getInstance"/>
    </constructor-arg>
    <constructor-arg index="4">
        <bean class="com.netflix.zuul.filters.FilterRegistry" factory-method="instance"/>
    </constructor-arg>
</bean>

3、编写XxxPreZuulServlet

public class XxxPreRoutingFilter extends ZuulFilter 

    private final RouteLocator routeLocator;

    private final UrlPathHelper urlPathHelper = new UrlPathHelper();

    public PreRoutingFilter(RouteLocator routeLocator) 
        this.routeLocator = routeLocator;
    

    @Override
    public String filterType() 
        return PRE_TYPE;
    

    @Override
    public int filterOrder() 
        return PRE_DECORATION_FILTER_ORDER + 1;
    

    @Override
    public boolean shouldFilter() 
        return true;
    

    @Override
    public Object run() throws ZuulException 
        RequestContext ctx = RequestContext.getCurrentContext();
        String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
        Route route = this.routeLocator.getMatchingRoute(requestURI);
        if (route == null || route.getLocation() == null) 
            ctx.setRouteHost(null);
            ctx.setSendZuulResponse(false);
        

        return null;
    



回头看,虽然需要写的代码并不多,但自认为需要研究的东西还是挺多的。好在自己之前简单研究过Tomcat、Spring MVC、Spring Boot的自动装配以及Spring Cloud Gateway的源码,并且涉及zuul部分的源码并不复杂,当然还有最重要的一点是代码都是师兄写的,我就是跟在旁边指指点点打下手(直接一手云开发),所以整个过程还算比较顺利。师兄仰慕值又+1。

以上是关于自研框架(Webx)整合Zuul网关工作总结的主要内容,如果未能解决你的问题,请参考以下文章

阿里Sentinel整合Zuul网关详解

阿里Sentinel整合Zuul网关详解

SpringCloud系列之四---Zuul网关整合Swaagger2管理API

Spring Cloud 微服务二:API网关spring cloud zuul

如何Spring Cloud Zuul作为网关的分布式系统中整合Swagger文档在同一个页面上

Oauth2.0 整合springCloud的Zuul 解决关键BUG 报错信息:Principal must not be null