写在前面,最近被分配了一个技术任务,简单描述为自研框架(类比Spring)整合一个微服务网关,并且能用就行。
有人可能会问,想用微服务网关,不是直接引入zuul或者gateway相关的依赖,然后配置一下不就好了吗?为什么我还会写这篇博客。这里原因主要有两个:
于是就有了这次任务,收获还是挺大的,特此用写下这篇博客。
既然目的是完成类似网关一样的请求转发,那么我们就需要知道被网关请求转发的请求打进来,该请求是如何流转的,然后直接调研对应的技术栈就可以了
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做为微服务网关是最符合当前项目技术栈,也是最简单的)
项目使用Tomcat做为Web容器,好在我之前研究过,有兴趣的小伙伴可以参考我之前的文章。
浅谈Tomcat的启动流程
浅谈Tomcat接收到一个请求后在其内部的执行流程
Tomcat组件架构图梳理
针对Tomcat其实重点放在Filter和Servlet上就行了。
这个大家都不陌生。同样,如果不熟悉的小伙伴可以参考我之前的文章
浅谈SpringMVC源码的DispatcherServlet组件执行流程
我们在使用Spring Boot构建Web项目的时候,其内部都是集成了Spring MVC,所以一个请求都会经过DispatcherServlet,然后经过该组件完成请求的转发,但是它本质上只是一个Servlet,明确这一点很重要。Servlet的调用则由Tomcat决定。研究该组件的初衷是为了获得一些灵感,说不定在什么地方就有可以借鉴的思路(结果是没有)
由于代码太老了,Download Resources的时候提示找不到资源,后来去内部的nexus服务器上看,确实没有源码,于是只能找文章学习了。但是就像我之前说的那样,Tomcat暴露在外的除了一些参数配置,还有就是一些扩展接口,如Filter、Servlet、甚至还有Value,基本这些大差不差,Webx必然是在这些扩展点进行的整合。
于是顺藤摸瓜,终于发现了响应的处理逻辑。Webx在完成请求转发的时候,和SpringMVC不一样,它完成内部业务逻辑的入口不是一个Servlet,而是一个Filter。最开始看到还是有点担心的,毕竟和自己预期的结果有点远,知道入口就好办了。
最让我意外的还是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。
果然,在对应的代码里我找到了他们,同样,为了避免重复造轮子,直接贴图(毕竟这也不是本文的重点)
前面说到,原生zuul里面的ZuulFilter分为四类,这四类ZuulFilter在spring-cloud-netflix-zuul中的实现可以浓缩为下面这张图。具体的执行流程按照原生zuul的逻辑(pre-route-post)
上图来自别人的博客,要看详细源码分析的,可以参考该篇文章SpringCloud源码剖析-Zuul的自动配置和核心Filter详解
至此,代码看完了,接着需要想想如何设计了
在阅读了上面源码的基础上,我们很容易就可以得到这张流程图。ZuulServlet替代了DispatcherServlet的能力,请求直接调用到ZuulServlet类中,然后完成相关ZuulFilter的责任链调用,这里就会调用到SimpleHostRoutingFilter,该ZuulFilter就是完成远程请求调用的具体实现类,其底层使用的是HttpClient(2.x使用了Netty)
研究框架源码发现,当前框架和Tomcat接壤的地方并不是一个Servlet,而是一个Filter,业务代码在该Filter内部完成。
最开始的设计方案是,我们将未来需要网关转发的请求配置到一个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的时候,自己才恍然大悟)
同样还是新建一个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提交记录上的代码只有这三部分:
<dependency>
<groupId>com.netflix.zuulgroupId>
<artifactId>zuul-coreartifactId>
dependency>
<dependency>
<groupId>com.netflix.hystrixgroupId>
<artifactId>hystrix-coreartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-contextartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-netflix-coreartifactId>
dependency>
<dependency>
<groupId>commons-configurationgroupId>
<artifactId>commons-configurationartifactId>
dependency>
<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>
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。