20. SpringCloud之Zuul源码解析

1、前言
Zuul组件的作用和使用方式 :
SpringCloud之Zuul分布式服务网关
1.1、如何接受请求?
基于zuul需要接受 用户请求,然后再过滤,再路由到下游服务等功能。首先,zuul内部 肯定得有 接受请求的东西。
要么和SpringMVC一样, 有一个DispatchServlet来接受所有的请求,再调到相对应的Controller的方法。
要么就直接是SpringMVC里的Controller类。
答案是后者。会有一个Controller类来接受请求。
1.2、如何把请求路由到 下游服务的某一台主机?
其次是路由功能。接收到请求之后,根据当前请求的信息,结合路由规则,转发到下游服务的某一个实例。那么,这部分就肯定会涉及到 服务的负载均衡。实际上,这部分功能,在zuul里,就是直接复用的Hystrix,Ribbon组件。
2、源码入口
前面介绍了好几个SpringCloud组件的源码, 源码的入口一般都是这两种方式 :
- @Enable***之类的注解来 导入一些重要的类
- SPI
2.1、@EnableZuulProxy
所以zuul源码,我们先看 启动类上的 @EnableZuulProxy 注解有啥。
@EnableCircuitBreaker
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}
首先可以看到打了 @EnableCircuitBreaker 注解,说明在启动zuul组件的时候,即使我们没有引入hystrix组件和打上@EnableCircuitBreaker注解 ,zuul服务 还是 会 引入 hystrix的相关功能的。
还有一个@Import(ZuulProxyMarkerConfiguration.class),导入了ZuulProxyMarkerConfiguration这个类,看下里面有啥
@Configuration
public class ZuulProxyMarkerConfiguration {
@Bean
public Marker zuulProxyMarkerBean() {
return new Marker();
}
class Marker {
}
}
结果是啥作用也没有。
到目前为止,@EnableZuulProxy这个注解尚未 引入 如何 与zuul功能相关的东西。
2.2、SPI
既然 @EnableZuulProxy注解里没有,那么就只能是SPI的方式了。
org.springframework.cloud:spring-cloud-netflix-zuul:2.0.0.RELEASE 包下的META-INF/spring.factories :

Springboot在启动的时候加载这个文件, 注册 key为EnableAutoConfiguration配置的类 :
org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration
org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration
源码的入口就在 这个文件里
3、ZuulController 处理请求
点进 ZuulServerAutoConfiguration 类:
里面会用@Bean的方式 注册一个ZuulController。

这个ZuulController就是用来接受 所有的请求的。但是和我们平时用@Controller 写的Controller不同,而是继承SpringMVC里的AbstractController,重写handleRequest方法 来处理请求的。

3.1、ZuulServlet的初始化
无参构造方法里会设值 ServletClass 为ZuulServlet。

ZuulServlet就是 一个HttpServlet,不过没有映射请求路径到tomcat容器里。只能被zuulController被动调用。
ZuulController继承的父类ServletWrappingController ,还实现了InitializingBean接口。

那么必定会 实现 InitializingBean的afterPropertiesSet()方法。
在 afterPropertiesSet() 会 根据 ZuulServlet的Class对象 创建其实例 以及初始化。
创建ZuulServlet 实例就是 反射调用构造方法,赋给成员变量servletInstance。

然后还会调用 servletInstance的init方法进行ZuulServlet 实例的初始化。初始化的时候,最重要的是 创建了ZuulRunner实例。ZuulRunner类里封装了Zuul组件里生命周期的执行方法:
preRoute(), route(), postRoute(), and error()

3.2、处理请求
zuul服务的所有请求都会进ZuulController 重写之 AbstractController的handleRequest 方法:

接下来会调用 ZuulServlet 的service方法。


3.2.1、init() : 初始化请求上下文对象
这个方法会调用zuulRunner.init(servletRequest, servletResponse)方法,往RequestContext对象里设置 request,response对象。所以我们在自己写的过滤器中可以通过请求上下文RequestContext对象获取 request,response对象。
RequestContext.getCurrentContext().getRequest()

3.2.1.1、获取RequestContext请求上下文对象
根据当前线程 取出对应的 RequestContext对象。

RequestContext对象有一个threadLocal对象,存放当前请求线程与对应的RequestContext对象。并且为每一个请求线程创建一个RequestContext对象。

所以RequestContext.getCurrentContext() 就是从这个threadLocal里,根据当前线程 取出对应的 RequestContext对象。

3.2.1.2、设置request,response对象到请求上下文对象。
获取到当前线程 对应的 RequestContext对象后,就设置request,response。

3.3、调用各生命周期的过滤器
初始化好 请求上下文之后,ZuulServlet.service()接下来就会调用 各种生命周期对应的过滤器。

可以看到 先调pre类型和 route类型的过滤器,其中任何一个调用过程中 存在异常, 就调error类型的过滤器, 然后调 post类型的过滤器。并return。结束。
如果 pre类型和 route类型的过滤器都被正常调用。那么就正常调用post类型的过滤器。发生异常再掉 error类型的过滤器。
这就是 各种生命周期对应的过滤器 全部的调用 流程。
3.3.1、过滤器的调用
我们看下 route类型的过滤器,看看是咋调用的。其他类型的都是类似的。
点进ZuulServlet的route() ,会调用 zuulRunner 对象的route()方法

调用 FilterProcessor 实例的route方法。

接着调用 runFilters方法,传入 过滤器类型 "route"

3.3.1.1、获取过滤器
接着 调用 FilterLoader实例的getFiltersByType(sType),根据类型获取 过滤器列表。

先从缓存里拿,拿不到就 获取所有的,根据类型过滤器之后,排序,存入缓存,返回出去。

3.3.1.2、调用过滤器
获取到当前类型的过滤器列表之后, 依次调用过滤器的 run方法。

调用 过滤器的 runFilter()。 这个方法我们自己的过滤器没有重写,肯定会调用到过滤器的 父类ZuulFilter的runFilter()方法。

ZuulFilter的runFilter()里,会钩到 子类的shouldFilter方法, 判断是否需要过滤,如果需要过滤再钩到 子类的run方法。子类也就是我们自己写的过滤器 和 zuul内置的过滤器。这是典型的模板设计模式 应用。

最终调用到我们所有过滤器的run方法。
3.3.1.3、过滤器的注册
前面获取过滤器的时候,为什么FilterLoader里的filterRegistry会有所有的过滤器?
首先 filterRegistry 对象 是个 静态常量。


我们自定义的过滤器 是必须要加@Component 注解的,会被Spring扫描到,然后注册到容器中。
然后zuul内置的过滤器会通过spi 加载的ZuulServerAutoConfiguration类和ZuulProxyAutoConfiguration 类里通过@Bean注册。


然后ZuulServerAutoConfiguration 类会有个带有@Configuration注解的 ZuulServerAutoConfiguration内部类。会注入map形式的所有 过滤器 filters对象。
它会用@Bean注册ZuulFilterInitializer。把所有 过滤器 filters对象以及 FilterRegistry类的静态常量filterRegistry对象, 传到ZuulFilterInitializer的构造方法里去。


然后在 有@PostConstruct的contextInitialized()方法 把所有 过滤器 filters对象 挨个put到filterRegistry 静态常量对象中。

所有FilterLoader 里的FilterRegistry静态常量对象 里,就能获取到所有的过滤器。
4、zuul核心内置过滤器
4.1、路由匹配-PreDecorationFilter
4.1.1、作用
服务路由配置示例
zuul.routes.micro-user.path=/user/**
zuul.routes.micro-user.serviceId=micro-user
## 不要忽略前缀 /user 比如/user/getUsername 路由到 micro-user 服务接口url还是/user/getUsername,默认为true,路由到micro-order 会去掉前缀为/getUsername
zuul.routes.micro-user.stripPrefix=false
zuul.routes.micro-user.sensitive-headers=
zuul.routes.micro-user.customSensitiveHeaders=true
当向zuul服务请求 /user/** 的路径时,会转发到 micro-user 服务。
PreDecorationFilter 过滤器就是 根据配置的路由规则 ,判断请求的路径 需要路由到哪个服务的。
4.1.2、属性加载
首先要把我们配置的路由规则加载到Spring容器中,就是ZuulProperties 这个类。
加载Zuul前缀的配置

4.1.3、注册路由定位器 CompositeRouteLocator
4.1.3.1、注册DiscoveryClientRouteLocator
ZuulProxyAutoConfiguration类里会用@Bean注册 DiscoveryClientRouteLocator实例。并且传入 配置信息 ZuulProperties 对象。赋给内部的成员变量。
DiscoveryClientRouteLocator 继承 SimpleRouteLocator,实现 RouteLocator 接口。

4.1.3.2、注册CompositeRouteLocator
ZuulServerAutoConfiguration类会用@Bean注册 CompositeRouteLocator实例。 并把所有的RouteLocator 实例列表,传入到CompositeRouteLocator的构造方法里, 赋给他的routeLocators 成员变量。

也就是说 CompositeRouteLocator对象会持有 DiscoveryClientRouteLocator 对象, DiscoveryClientRouteLocator 对象会 持有 配置信息 zuulProperties对象。
CompositeRouteLocator构造方法 :

4.1.4、路由规则映射关系的建立
在配置文件加载了配置的路由规则后,还需要建立 请求路径和路由规则对象,保存在路由定位器对象中。 这个动作的触发 是由 事件监听器 完成的。
ZuulServerAutoConfiguration 里 会有一个内部类, ZuulRefreshListener。
会用@Bean注册它。

ZuulRefreshListener 是个监听器,当Spring发布相关事件时,会调到他的onApplicationEvent方法来响应时间。

可以看到 这几种 都会重置 路由的映射关系
- ContextRefreshedEvent : 容器加载完成, 这时候就相当于 最开始时的初始化。
- RefreshScopeRefreshedEvent :作用域RefreshScope的bean被刷新时,会重置
- RoutesRefreshedEvent :路由信息刷新时,要重置 路由的映射关系。
- InstanceRegisteredEvent : 新服务 上线时,要要重置 路由的映射关系。
当这几种事件被发布时,就会调用zuulHandlerMapping的setDirty方法。zuulHandlerMapping 对象持有 路由定位器 CompositeRouteLocator对象。调它的refresh()方法。

CompositeRouteLocator的refresh方法就会调用 内部所有RouteLocator接口实例(DiscoveryClientRouteLocator)的refresh方法。

doRefresh()

调到父类 SimpleRouteLocator的doRefresh(),里面调用 locateRoutes()

locateRoutes()方法会钩到子类 DiscoveryClientRouteLocator.
子类又先调用父类SimpleRouteLocator的locateRoutes方法。

4.1.4.1、path表达式 和 路由规则的映射
父类SimpleRouteLocator的locateRoutes()方法 会 从配置信息里,把配置里的path比如/user/**这种 作为key, 对应的路由配置对象作为value,put到map中。返回。

调完父类的之后,这个时候routesMap就会有映射关系了。是path表达式和路由规则的映射关系。

4.1.4.2、/服务id/** 和 路由规则的映射
然后会建立 服务id和 路由规则的映射关系。staticServices对象

然后把服务id 变成 /服务id/ **, 再与服务id建立映射关系,put到routesMap中。

这也是为什么 我们直接向zuul 请求 /服务名/剩余路径/,也能正确路由到下游服务的原因。
最后把这个routesMap返回,把所有的映射关系 设置给了SimpleRouteLocator 的 routes对象。

4.1.5、注册 PreDecorationFilter
ZuulProxyAutoConfiguration类里会用@Bean注册 PreDecorationFilter 实例。并且在方法的参数列表里注入 CompositeRouteLocator 对象 和配置信息zuulProperties 对象。 传到构造方法里, 赋给对应的成员变量。

4.1.5.1、路由规则匹配
在zuul接受请求的时候, 调用PRE类型的过滤器时,会调用到PreDecorationFilter对象的run方法。
4.1.5.1.1、匹配路由规则
先获取接口路径, 然后根据 接口路径获取 匹配的路由规则。

this.routeLocator 对象 就是实例化时传入到构造方法 里的 CompositeRouteLocator对象。
调到CompositeRouteLocator 对象的getMatchingRoute(path) 方法。
里面会遍历内部的所有RouteLocator接口类型的实例,调用getMatchingRoute方法。直到获取到 匹配的路由对象,返回。

这里就会调用到一开始设置进去的 DiscoveryClientRouteLocator对象。由于DiscoveryClientRouteLocator没有重写 getMatchingRoute方法,就会调到父类SimpleRouteLocator的getMatchingRoute( path ) 。

进入SimpleRouteLocator的getMatchingRoute( path )。
获取到 路由规则映射关系之后, 就会根据当前的请求路径,获取路由规则。

就是从 路由规则关系里 挨个与当前请求路径匹配,匹配到了之后,返回这个路由规则对象。

然后包装一下,返回,


4.1.5.1.2、将路由信息 设置到 请求上下文中
根据当前请求路径匹配到 路由规则对象之后,这个时候从路由规则对象里面就 可以知道要 转发的下游服务是哪个了。 这个时候 PreDecorationFilter 的run方法 就会把 相关的路由信息 从 路由规则对象里取出来,放到请求上下文中。 让 下面的请求转发过滤器可以获取到,从而把当前请求 转发到 匹配到的服务。

最终的 请求上下文 是这样的 : 里面有 要转发的 服务名称serviceId 和 对这个服务请求的uri :reuqestURI。

PreDecorationFilter的run结束。
PreDecorationFilter 对当前请求 的路由规则匹配 大致就是这样的。
4.2、请求转发-RibbonRoutingFilter
这个过滤器 主要是 用hystrix组件 + ribbon, 对下游服务 发起调用的。

buildCommandContext 会从请求上下文对象,RequestContetext对象,取出 一系列请求的 信息:比如要调用的服务名称,uri,等等 放到 RibbonCommandContext对象里去,并返回。

然后调forward

根据RibbonCommandContext对象,创建出RibbonCommand ,调他的execute方法。这里的RibbonCommand类型是 HttpClientRibbonCommand。

HttpClientRibbonCommand 继承了 HystrixCommand类,execute方法也是 HystrixCommand类的。

所以就调到了 HystrixCommand的 execute方法,走hystrix的逻辑。

hystrix的相关源码,之前有介绍过 :SpringCloud之Hystrix源码。这里就不再赘述了。
最终调由hystrix + ribbon 组件 对 下游服务发起调用,完成 请求的转发。