Spring Cloud 之 Zuul 网关
最近会有一些定制化 Zuul 网关的需求,所以看了一下 Zuul 的源码,写这篇文章记录一下。
Zuul 简介
Zuul 是 Netflix 开源的一个 API Gateway,作为一个边缘服务,提供动态路由、监控、安全等相关功能的支持。
本文结合 Spring Cloud 相关组件来使用 Zuul,重点分析在微服务架构下 Zuul 相关功能的实现,这里默认各位已经知晓 Zuul 的使用,本文重点放在原理讲解和源码分析上。
本文使用的 Spring Cloud 版本为 Edgware.SR5,Spring Boot 版本为 1.5.13.RELEASE,zuul 的引用使用了 spring-cloud-starter-zuul 包,版本为 1.4.4.RELEASE,对应的 zuul-core 版本为 1.3.0。
内容主要分成了两个部分,一个部分是 Zuul 的正常执行流程,比如 Filter 的执行过程,路由的执行过程,这部分其实很容易理解,所以基本点到为止;
第二部分是 Zuul 的启动过程,包括 Filter 的加载以及路由信息的加载等,会重点说明。
每个部分的内容都尽量的独立,可以分开看,源码较多,省略了无关的代码,只关注主要功能的实现。
一次请求到达 ZuulServlet 的过程
这个部分主要属于 Spring MVC 的内容,简单的说明下。
请求到达服务器后,会被 DispatcherServlet 处理,然后根据 request 找到对应的 HandlerMapping,Zuul 提供了 ZuulHandlerMapping,至于请求为何可以匹配到这个 HandlerMapping 后面会说明,这里先不细讲。
HandlerMapping 返回的 HandlerExecutionChain 中 handler 的实际对象是 ZuulController。
接着获取 HandlerAdapter,这里拿到的会是 SimpleControllerHandlerAdapter,然后使用这个 HandlerAdapter 执行 ZuulController,代码如下
// SimpleControllerHandlerAdapter 的 handle 方法
@Override
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 这里传入的 handler 为 ZuulController
return ((Controller) handler).handleRequest(request, response);
}
接下来就是 ZuulController 的执行过程,可以看到是直接调用的父类的 handleRequestInternal 方法,如下所示:
public class ZuulController extends ServletWrappingController {
public ZuulController() {
// 这里表明持有的 Servlet 是 ZuulServlet
setServletClass(ZuulServlet.class);
setServletName("zuul");
setSupportedMethods((String[]) null);
}
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
// 这里调用 ServletWrappingController 类的 handleRequestInternal 方法
return super.handleRequestInternal(request, response);
}
finally {
RequestContext.getCurrentContext().unset();
}
}
}
ServletWrappingController 中 handleRequestInternal 方法的实现就是调用内部持有的 Servlet 的 service 方法,这里 ZuulController 持有的 Servlet 则是 ZuulServlet,从上面代码中的构造函数中可以看到这一点。
// ServletWrappingController 中的 handleRequestInternal
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 这里实际调用的就会是 ZuulServlet 的 service 方法
this.servletInstance.service(request, response);
return null;
}
最终请求就被 ZuulServlet 处理了,接下来就进入到 Zuul 的内部运行部分。
ZuulFilter 的执行过程
直接来看 ZuulServlet 的 service 方法实现
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
// 这里先忽略,主要是为请求创建了一个 RequestContext
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
// 1. 执行 pre 类型的 filter
preRoute();
} catch (ZuulException e) {
// 错误处理
error(e);
postRoute();
return;
}
try {
// 2. 执行 route 类型的 filter
route();
} catch (ZuulException e) {
// 错误处理
error(e);
postRoute();
return;
}
try {
// 3. 执行 post 类型的 filter
postRoute();
} catch (ZuulException e) {
// 错误处理
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
代码中的关键步骤都进行了注释说明,这里实现的逻辑对应的就是官网提供的请求生命周期图,简单明了。
ZuulFilter 的类型默认有四种 pre、route、post、error,实现一个定制化的 Zuul Filter 需要继承 ZuulFilter 类,并实现四个方法即可,如下所示:
import com.netflix.zuul.ZuulFilter;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
@Component
public class CustomFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return false;
}
@Override
public Object run() {
return null;
}
}
这四个方法分别代表了 Zuul Filter 的特性:
类型
即 filterType 方法的返回值,Zuul 中默认的 Filter 的类型有 pre,route,post,error 四种类型;
执行顺序
filterOrder 方法返回 int 类型的 order 值。在 Zuul 中,各个类型的 Filter 对应一个列表,列表中按照 order 从小到大的顺序进行排序,这也是执行时的顺序;
执行条件
只有当 shouldFilter 方法返回为 true 时,Filter 的 run 方法才会被执行;
动作
即 Filter 执行具体做的事情,具体实现在 run 方法中。
下面通过分析 preRoute 方法,来说明 Zuul Filter 的执行过程,涉及的代码执行如下所示:
// 1. ZuulServlet.java
void preRoute() throws ZuulException {
zuulRunner.preRoute();
}
// 2. ZuulRunner.java
public void preRoute() throws ZuulException {
FilterProcessor.getInstance().preRoute();
}
// ...
// 3. FilterProcessor.java
public Object runFilters(String sType) throws Throwable {
if (RequestContext.getCurrentContext().debugRouting()) {
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
// 拿到某类型的 Filter 的列表,并遍历执行
List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
if (list != null) {
for (int i = 0; i < list.size(); i++) {
ZuulFilter zuulFilter = list.get(i);
// 执行 Filter
Object result = processZuulFilter(zuulFilter);
if (result != null && result instanceof Boolean) {
bResult |= ((Boolean) result);
}
}
}
return bResult;
}
// 4. ZuuFilter.java
public ZuulFilterResult runFilter() {
ZuulFilterResult zr = new ZuulFilterResult();
if (!isFilterDisabled()) {
// 5. 判断是否执行
if (shouldFilter()) {
Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
try {
// 6. run 方法执行的地方
Object res = run();
zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
} catch (Throwable e) {
t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
zr = new ZuulFilterResult(ExecutionStatus.FAILED);
zr.setException(e);
} finally {
t.stopAndLog();
}
} else {
zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
}
}
return zr;
}
关键的地方都有注释说明,Filter 之间通过 RequestContext 共享数据,该类是一个 ConcurrentHashMap,存放在 ThreadLocal 中。
上面分析的是 pre 类型的 Filter 处理的流程,其他类型的处理流程也是相同的,就不再赘述。
Zuul 的路由功能
当我们使用 @EnableZuulProxy 注解时,SpringBoot 会加载 ZuulProxyAutoConfiguration 配置类,这个配置类会注入 DiscoveryClientRouteLocator、PreDecorationFilter、RibbonRoutingFilter 等类,使用这些类配合 EurekaClient 就能够让 Zuul 做基于服务注册中心的动态路由。
整个路由的过程,是通过各个类型的 Filter 共同实现的,首先是 PreDecorationFilter,这是一个 pre 类型的 Filter,实现如下:
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return !ctx.containsKey(FORWARD_TO_KEY) // a filter has already forwarded
&& !ctx.containsKey(SERVICE_ID_KEY); // a filter has already determined serviceId
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
// 寻找路由
Route route = this.routeLocator.getMatchingRoute(requestURI);
// 获取请求的目的地
String location = route.getLocation();
// 设置 serviceId
ctx.set(SERVICE_ID_KEY, location);
return null;
}
简单的来说,当 RequestContext 没有设置 serviceId 时,请求就会被这个 Filter 处理。
在 run 方法中,首先拿到本次请求的 uri,然后通过 uri 获取一个 Route 对象,然后获取 location 值,设置 serviceId 到 RequestContext 中,这就是这个 pre 类型的 Filter 主要的功能,其他部分的功能先忽略不管。
然后进入到 route 类型的 Filter 的执行过程,RibbonRoutingFilter 的实现如下所示:
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
this.helper.addIgnoredHeaders();
try {
// 构建 Ribbon 命令
RibbonCommandContext commandContext = buildCommandContext(context);
// 实际发送请求的地方
ClientHttpResponse response = forward(commandContext);
setResponse(response);
return response;
}
catch (ZuulException ex) {
throw new ZuulRuntimeException(ex);
}
catch (Exception ex) {
throw new ZuulRuntimeException(ex);
}
}
这里通过 RequestContext 构建一个 RibbonCommandContext,这里主要是获取 serviceId 的值,也就是请求最终的去处,最后使用 HTTP 客户端将请求发送出去,这里涉及的 Ribbon 和 Hystrix 相关功能就不细说了。
ZuulFilter 的加载过程
前面的小节主要讲了 Filter 和路由相关功能的执行过程,但是这些 Filter 以及路由配置是如何加载进来,以及运行时的存放形式是怎么样的,下面就这些疑问分别进行分析。
首先来看 Filter 的加载过程,这里通过反推的形式来梳理整个加载流程。
在之前的流程中,获取某类的 Filter 的代码如下:
List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
可以看到是通过 FilterLoader 获取的,它内部的实现如下:
public class FilterLoader {
// 单例
final static FilterLoader INSTANCE = new FilterLoader();
// 用于缓存各个类型的 Filter 列表
private final ConcurrentHashMap<String, List<ZuulFilter>> hashFiltersByType = new ConcurrentHashMap<String, List<ZuulFilter>>();
// Filter 注册表,用于存放所有的 Filter
private FilterRegistry filterRegistry = FilterRegistry.instance();
}
接下来看看 FilterRegistry 的实现
public class FilterRegistry {
private static final FilterRegistry INSTANCE = new FilterRegistry();
public static final FilterRegistry instance() {
return INSTANCE;
}
private final ConcurrentHashMap<String, ZuulFilter> filters = new ConcurrentHashMap<String, ZuulFilter>();
private FilterRegistry() {
}
}
可以看到,这个类也是使用了单例模式,内部使用了一个 ConcurrentHashMap 保存所有的 Filter,这里就是 ZuulFilter 运行时存放的位置。
那这些 Filter 是何时被放到这个 ConcurrentHashMap 中的呢?这里通过查找 put 方法何时被调用,找到了 ZuulFilterInitializer 类,它的实现如下:
1public class ZuulFilterInitializer {
2
3 private final Map<String, ZuulFilter> filters;
4 private final FilterRegistry filterRegistry;
5
6 public ZuulFilterInitializer(Map<String, ZuulFilter> filters, /* 忽略其他 */) {
7 this.filters = filters;
8 // ...
9 }
10
11 @PostConstruct
12 public void contextInitialized() {
13 TracerFactory.initialize(tracerFactory);
14 CounterFactory.initialize(counterFactory);
15 // 将 Filter 放入到 FilterRegistry 中
16 for (Map.Entry<String, ZuulFilter> entry : this.filters.entrySet()) {
17 filterRegistry.put(entry.getKey(), entry.getValue());
18 }
19 }
20}
可以看到在该类的构造函数中,传入了所有的 ZuulFilter,并且在 contextInitialized 方法执行时将 ZuulFilter 放入到 FilterRegistry 中。
接着来看 ZuulFilterInitializer 是在哪里被创建的,可以在 ZuulServerAutoConfiguration 中找到,如下所示:
public class ZuulServerAutoConfiguration { // ... 忽略其他
@Configuration protected static class ZuulFilterConfiguration {
// 自动注入所有的 ZuulFilter @Autowired private Map
@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); } } }
可以看到,所有的 ZuulFilter 是自动注入的,这里会得到所有被 Spring 管理的 ZuulFilter,然后在创建 ZuulFilterInitializer 类的时候,被传递给了构造函数。
这样,ZuulFilter 的加载过程就完整了。
## ZuulServlet 的加载过程
同样在 ZuulServerAutoConfiguration 中,可以看到 ZuulController 和 ZuulHandlerMapping 的注入过程。
public class ZuulServerAutoConfiguration { @Bean public ZuulController zuulController() { return new ZuulController(); }
@Bean public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) { ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController()); mapping.setErrorController(this.errorController); return mapping; } }
上面是注入过程,加载过程则是在 DispatcherServlet 中完成,当 DispatcherServlet 的 init 方法被调用时,最终会执行到 onRefresh 方法,这里负责了相关的初始化工作,实现如下:
@Override protected void onRefresh(ApplicationContext context) { initStrategies(context); } protected void initStrategies(ApplicationContext context) { // ... 忽略
// 初始化 HandlerMapping initHandlerMappings(context); // ... 忽略 }
private void initHandlerMappings(ApplicationContext context) { this.handlerMappings = null; if (this.detectAllHandlerMappings) { // 这里获取所有的 HandlerMapping 的实例,包括了 ZuulHandlerMapping Map
可以看到 DispatcherServlet 会将所有的 HandlerMapping 保存到 handlerMappings 中,每次有请求过来的时候就会去 handlerMappings 中匹配对应的 HandlerMapping。
这样 ZuulServlet 运行时的加载过程就分析完了,与第三部分的内容结合起来看就是一个完整的启动和运行过程。
## 路由信息的加载过程
剩下的就是路由的加载和初始化过程了。
路由信息需要在两个地方起作用,一个是 ZuulHandlerMapping 中,也就是请求如何匹配到 ZuulHandlerMapping,以及在 PreDecorationFilter 中获取 Route 对象。
后者已经在第五部分中进行了说明,这里要解决的问题是请求如何匹配到 ZuulHandlerMapping?
首先再来看看 ZuulHandlerMapping 的配置部分:
public class ZuulServerAutoConfiguration { // 注入 ZuulHandlerMapping 的地方,需要注入一个 RouteLocator 对象 @Bean public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) { ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController()); mapping.setErrorController(this.errorController); return mapping; } /** 这里注入 CompositeRouteLocator,并且使用 @Primary 注解, 所以上面方法中注入的 RouteLocator 对象,始终为 CompositeRouteLocator 同时接收所有其他的 RouteLocator 对象,内部使用一个集合保存 / @Bean @Primary public CompositeRouteLocator primaryRouteLocator(Collection
查看上面的配置以及注释说明,可以知道 ZuulHandlerMapping 中持有一个 RouteLocator 的实现类 CompositeRouteLocator,而 CompositeRouteLocator 类中包含了其他所有的 RouteLocator 的实现类,相当于 Zuul 默认使用 CompositeRouteLocator 来管理其他所有的 RouteLocator。
再来看 ZuulHandlerMapping 内部实现:
public class ZuulHandlerMapping extends AbstractUrlHandlerMapping { // ... 忽略了其他部分
// 该类实际为 CompositeRouteLocator private final RouteLocator routeLocator; private final ZuulController zuul;
// 这里默认为 true private volatile boolean dirty = true;
public ZuulHandlerMapping(RouteLocator routeLocator, ZuulController zuul) { this.routeLocator = routeLocator; this.zuul = zuul; setOrder(-200); }
@Override protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception { // ... if (this.dirty) { synchronized (this) { if (this.dirty) { // 第一次执行时会先注册 hander registerHandlers(); this.dirty = false; } } } return super.lookupHandler(urlPath, request); }
private void registerHandlers() { // 遍历 CompositeRouteLocator 中所有的 RouteLocator 对象 // 并返回每个 RouteLocator 里所有的 Route 对象 Collection
注意注释信息的说明,ZuulHandlerMapping 在初始化时,会将所有 RouteLocator 返回的 Route 中的 path 信息添加到 handlerMap 中,这就是为什么访问配置过的路由信息时,请求可以被 ZuulHandlerMapping 处理,从而进入 Zuul 的处理流程。
而访问没有配置过的路径时,根本就不会进入 Zuul 的处理流程,而是被其他 HandlerMapping 处理。
最后剩下的问题就是,配置的路由信息在运行时是如何被加载进来的呢?
目前可以添加的配置信息的地方有三个,一个是配置文件直接配置,二是获取服务注册中心,生成路由信息,三是自定义实现并注入 RouteLocator 类的子类。
第一中方式是通过配置文件配置,相当于直接赋值了 ZuulProperties 类,在 ZuulServerAutoConfiguration 中会注入 SimpleRouteLocator 类,这个类的构造函数就传入了 ZuulProperties 对象,内部会将配置文件中的路由信息添加到 handlerMap 中。
// ZuulServerAutoConfiguration 类 @Bean @ConditionalOnMissingBean(SimpleRouteLocator.class) public SimpleRouteLocator simpleRouteLocator() { // 传入了配置文件中配置的路由信息 return new SimpleRouteLocator(this.server.getServletPrefix(), this.zuulProperties); }
第二种方式则是通过 DiscoveryClientRouteLocator 类实现,它继承 RouteLocator,所以被管理在 CompositeRouteLocator 对象中,所以当 ZuulHandlerMapping 在执行 registerHandlers 方法时,最终会执行到 DiscoveryClientRouteLocator 的 locateRoutes 方法,该方法实现如下:
@Override protected LinkedHashMap
主要的逻辑就是将服务注册中心的所有服务拉取过来,然后生成 ZuulRoute 对象,完成路由的注册,这里还涉及动态刷新的功能,使用的方式是 Spring 的事件机制,这里不展开说了。
第三种方法就很简单,自定义实现的 RouteLocator 同样会被 CompositeRouteLocator 管理中,在 ZuulHandlerMapping 初始化路由的时候被添加到 handlerMap 中。
## 小结
本文分成几个部分,分别讲了请求如何进到 Zuul 的处理流程、Filter 的执行流程、以及配合服务注册中心的动态路由过程,最后还讲解了 Zuul 启动时相关组件的加载过程。
当然,Zuul 的内容还远不止这些,以后有机会再进行讲解,本文有误的地方欢迎指出来。