说在前面
我们公司有一个线上服务报错通知群,经常报网关服务的一个 EOFException 异常。这个异常报出来好久了,如下图所示,艾特相关的人也不去处理,大概是不重要异常吧,反正看样子是不影响线上核心业务流程。
然后我上级让我优化下这类日志打印,把无法解析的参数,把原url打印出来,另外把这类日志等级调整成 warn,不需要处理的就不要用 error 一直报警了。
在根据打印的日志堆栈信息中,可以看到这些日志主要是 Zuul 默认的 SendErrorFilter 打印的,在思考怎么优化这类日志的同时也冒出很多对 Zuul 的疑问:
- 既然这些日志是 Zuul 框架打印的 error 级别的日志,并且 error 已经是最高级别了,我还能调整级别?我又如何我新增打印参数如 url,请求体等信息?
- 堆栈链路中的
ZuulServlet.service()
方法是啥? - Zuul 网关请求和 DispatcherServlet 有什么联系?它们之间又是如何协作的?
- Zuul 有一堆的 Filter,它们是如何串联起来的?它是如何路由发起请求的?
当时脑子里真的是一堆的疑问,它驱使着我翻看 zuul 源码的好奇心愈发增强。
于是就有了这边关于 Zuul 网关的源码解析。
下面,我将从我当时对 Zuul 的疑问点为切入点,是如何一步步带着问题去阅读源码,把整个流程给弄明白的思路给你们讲清楚。也许,我当时的疑问也正式你的疑问呢,希望可以帮助有需要的人。
Zuul 源码解析
Zuul 的本质就是 Servlet 和 Filter 链。Servlet 是业务入口以及控制整个流程,Filter 链处理整个请求的一些逻辑,包括前置处理,转发路由,后置处理返回响应等等
Zuul 的核心流程
Zuul 的核心流程主要封装在 ZuulServlet 中的 service()
方法中
public class ZuulServlet extends HttpServlet {
// 省略了部分代码方法和属性
// ...
private ZuulRunner zuulRunner;
@Override
public void service(ServletRequest servletRequest,ServletResponse servletResponse){
try {
// 初始化 RequestContext,该 RequestContext 是贯穿 Zuul 处理请求的生命周期的
// 主要是暂存 request,response,异常信息等处理请求过程中中间结果
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);;
try {
// 处理全部 pre 类型的 Filter
preRoute();
} catch (ZuulException e) {
// 抛异常时,在在 error() 方法中存储该异常到 RequestContext 上下文中,后续会用到的
// RequestContext.getCurrentContext().setThrowable(e);
// 接着再处理所有 error 类型 Filter
error(e);
// 接着在执行所有 post 类型的 Filter
postRoute();
// 就这样,一个请求在报错下经过 Zuul 的生命周期就结束了
return;
}
try {
// preRoute() 没有异常,则执行 route 类型的 Filter
route();
} catch (ZuulException e) {
// 同上备注
error(e);
postRoute();
return;
}
try {
// route() 没有异常,则执行 所有 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();
}
}
// ...
}
preRoute()
方法中,处理 Filter 的逻辑是委托给 zuulRunner 去完成的,其他两个 error()
,postRoute()
同理。
// ZuulServlet#preRoute()
void preRoute() throws ZuulException {
zuulRunner.preRoute();
}
可以理解 ZuulServlet 是入口,是控制执行流程的,类似于 DisPatcherServlet。而干实事的是 ZuulRunner,ZuulRunner 的主要职责是主要有两个:
- 初始化 requests 和 responses 到 RequestContext 中
// ZuulRunner#init()
public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
RequestContext ctx = RequestContext.getCurrentContext();
if (bufferRequests) {
ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
} else {
ctx.setRequest(servletRequest);
}
ctx.setResponse(new HttpServletResponseWrapper(servletResponse));
}
2)封装 preRoute()
, route()
, postRoute()
和 error()
的 FilterProcessor 调用
// 如 ZuulRunner#postRoute()
public void postRoute() throws ZuulException {
// 看 FilterProcessor(拦截器处理器)名字就知道这是应用了典型的拦截器模式
FilterProcessor.getInstance().postRoute();
}
再看看 FilterProcessor 的处理逻辑
public class FilterProcessor {
//...
// 执行所有 "post" filters.
public void postRoute() throws ZuulException {
try {
// runFilters 方法只有一个参数
// 它里面主要做的就是通过查找所有 post 类型的 FIlter,然后根据每个 ZuulFilter 的 Order 进行排序
// 排完序之后,执行 shouldFilter(),满足条件的话,在执行 Filter 的实际逻辑方法 run()
runFilters("post");
} catch (ZuulException e) {// 其中一个 Filter 出错则抛出异常
throw e;
} catch (Throwable e) {
throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_POST_FILTER_" + e.getClass().getName());
}
}
// 执行所有 "error" filters.
public void error() {
try {
runFilters("error");
} catch (Throwable e) {// 其中一个 Filter 出错则抛出异常
// 注意这里,在 FilterProcessor 执行所有 error 类型的 Filter 时,只会打印该异常,不会再抛出。
// 文章开头中的日志截图中,其中有一条就是在这里打印的。
logger.error(e.getMessage(), e);
}
}
// 执行所有 "route" filters.
public void route() throws ZuulException {
try {
runFilters("route");
} catch (ZuulException e) {// 其中一个 Filter 出错则抛出异常
throw e;
} catch (Throwable e) {
throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_ROUTE_FILTER_" + e.getClass().getName());
}
}
// 执行所有 "pre" filters
public void preRoute() throws ZuulException {
try {
runFilters("pre");
} catch (ZuulException e) {// 其中一个 Filter 出错则抛出异常
throw e;
} catch (Throwable e) {
throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass().getName());
}
}
// ...
}
如上源代码备注所示,流程已经很清晰了。说实话,由于自己当时对 Zuul 的基础知识不够了解,当时看到这个流程就在想,Zuul 有哪些默认的 Filter 呢,发起服务调用是在哪里,route()
方法吗,意思是发送服务调用也是在 Filter 中去完成?
RouteLocator 路由定位器的职责。
在讲 Zuul 默认的 Filter 之前,我觉得有必要先了解 ZuulProperties,RouteLocator 等基础设施类。
ZuulProperties
@ConfigurationProperties("zuul")
public class ZuulProperties {
private String prefix = "";
private Boolean retryable = false;
/**
* Map of route names to properties.
*/
private Map<String, ZuulRoute> routes = new LinkedHashMap<>();
// ...
public static class ZuulRoute {
private String id;
/**
* The path (pattern) for the route, e.g. /foo/**.
*/
private String path;
// 可以理解是服务名称
private String serviceId;
// 路由转发 url 全路径,它和 serviceId 是二选一的。
// 我们服务中没有使用它
private String url;
// ...
// 先看着,后面会调用
public String getLocation() {
if (StringUtils.hasText(this.url)) {
// url 不为空,返回 url
return this.url;
}
// 否则,返回 serviceId
return this.serviceId;
}
}
}
也许上面配置类中属性看的有点抽象的话,不妨看看如下我目前所在公司的路由配置实例:
#路由规则配置
zuul.routes.usercenter.path = /usercenter/**
zuul.routes.usercenter.serviceId = USERCENTER
zuul.routes.platform.path = /platform/**
zuul.routes.platform.serviceId = PLATFORM
zuul.routes.paycenter.path = /paycenter/**
zuul.routes.paycenter.serviceId = paycenter
...
使用者怎么理解呢?比如一个常规请求:https://网关域名/usercenter/user/detail/1
Zuul 就会取 /usercenter/user/detail/1
去匹配如上配置中的 path 属性,发现 /usercenter/**
符合该路径的匹配规则,就找到对应的 serviceId = USERCENTER
,那么就会路由到 USERCENTER
这个服务的 /user/detail/1
接口。
RouteLocator
// 路由定位器
// 怎么理解呢,主要是做什么的呢,看接口就很直观了
public interface RouteLocator {
Collection<String> getIgnoredPaths();
// 获取所有的路由配置
// 路由定位器和其他组件的交互,是最终把定位的 Routes 以 list 的方式提供出去。后面源码中很多地方都会调用
// 如上面的配置例子中:zuul.routes.usercenter.path = /usercenter/**
// zuul.routes.usercenter.serviceId = USERCENTER
// 这就组成一个 Route 实体
List<Route> getRoutes();
// 根据请求 path,比如如上例子中的 /usercenter/user/detail/1,匹配到一个 Route
// Route 这个类与 ZuulProperties 的内部类 ZuulRoute 非常的相似,都是描述路由的一个类
// 可以理解 ZuulRoute 的配置用的实体路由类, Route 是业务中使用的路由类
Route getMatchingRoute(String path);
}
RouteLocator 有两个实现类,分别是 SimpleRouteLocator 和 DiscoveryClientRouteLocator
SimpleRouteLocator
SimpleRouteLocator(简单路由定位器) 我认为是最核心的一个路由定位器类。它主要负责管理 ZuulProperties 配置的 Route。
这里我也是重点关注它的 getRoutes()
和 getMatchingRoute(String path)
两个方法。因为我觉得了解了这两个方法的实现,对后续阅读 理解 zuul 的核源码非常有必要的。
// 该类的重要属性如下:
public class SimpleRouteLocator implements RouteLocator, Ordered {
// ...
// 配置文件中的路由信息
private ZuulProperties properties;
private PathMatcher pathMatcher = new AntPathMatcher();
// 可以理解是 ZuulProperties 的一个路径模板 path -> ZuulRoute 的 map 缓存
// 目的是避免重复在 ZuulProperties 中计算得到 Route 列表
private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>();
// ...
}
getRoutes()
获取所有的路由
@Override
public List<Route> getRoutes() {
List<Route> values = new ArrayList<>();
// 这几行代码很简单,没啥好说的。我们主要关注下 getRoutesMap() 方法
for (Entry<String, ZuulRoute> entry : getRoutesMap().entrySet()) {
ZuulRoute route = entry.getValue();
String path = route.getPath();
values.add(getRoute(route, path));
}
return values;
}
getRoutesMap()
方法:
// 这个方法可以理解就是初始化上面的 map 缓存(路径模板path -> ZuulRoute)
protected Map<String, ZuulRoute> getRoutesMap() {
// 为空,则初始化
if (this.routes.get() == null) {
// 重点关注下 locateRoutes() 方法
this.routes.set(locateRoutes());
}
return this.routes.get();
}
locateRoutes()
方法:
// 该方法是 protected ,一般情况下要自定义 RouteLocator 时,会重写该方法,比如我需要从数据库中加载 Route
protected Map<String, ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
// 看这里,就是遍历了 ZuulProperties 中的 routes 属性
for (ZuulRoute route : this.properties.getRoutes().values()) {
routesMap.put(route.getPath(), route);
}
return routesMap;
}
getMatchingRoute()
根据请求 url(requestUri)匹配 Route
@Override
public Route getMatchingRoute(final String path) {
// 直接调用下方这个方法
return getSimpleMatchingRoute(path);
}
protected Route getSimpleMatchingRoute(final String path) {
// ...
// This is called for the initialization done in getRoutesMap()
getRoutesMap();
// ...
String adjustedPath = adjustPath(path);
// 根据 requestUri 返回一个 ZuulRoute
// 具体实现,往下看
ZuulRoute route = getZuulRoute(adjustedPath);
// 前面说到业务中使用的都是 Route 实体,并不是 ZuulRoute
// 调用 getRoute() 方法就是把 ZuulRoute 转换加工成 Route 返回
return getRoute(route, adjustedPath);
}
protected ZuulRoute getZuulRoute(String adjustedPath) {
if (!matchesIgnoredPatterns(adjustedPath)) {
// getRoutesMap() 获取前面缓存好的 route 配置 Map
// 遍历,根据模板 path 模糊匹配 requestUri
for (Entry<String, ZuulRoute> entry : getRoutesMap().entrySet()) {
String pattern = entry.getKey();
// 重点在这里,使用 pathMatcher 来匹配一个 ZuulRoute
if (this.pathMatcher.match(pattern, adjustedPath)) {
// 匹配则返回一个 ZuulRoute
return entry.getValue();
}
}
}
return null;
}
// 刷新 routes 缓存时调用的,咋暂时不管
protected void doRefresh() {
this.routes.set(locateRoutes());
}
DiscoveryClientRouteLocator
我习惯叫这个 RouteLocator 为服务发现路由定位器。DiscoveryClientRouteLocator 继承自 SimpleRouteLocator,它的 getRoutes()
方法中,除了获取 ZuulProperties 配置的 zuulRoute 外,还包括从注册中心拉取的动态路由配置。
怎么理解它呢?比如:你部署上线一个新应用服务名称为 ordercenter,然后你不再需要在网关服务中新增 apollo 配置,你就可以向网关发起请求,类似 https://网关域名/ordercenter/order/detail/1001
的方式,网关 Zuul 也会根据你的意愿转发请求到新服务 ordercenter。因为 DiscoveryClientRouteLocator 它会动态拉取新的服务路由配置,动态刷新本地路由集合。
接下,我们来看它是如何实现的。.
public class DiscoveryClientRouteLocator extends SimpleRouteLocator
implements RefreshableRouteLocator {
// 服务发现 client
private DiscoveryClient discovery;
private ZuulProperties properties;
private ServiceRouteMapper serviceRouteMapper;
public void addRoute(String path, String location) {
this.properties.getRoutes().put(path, new ZuulRoute(path, location));
refresh();
}
// ...
// 返回 ZuulProperties 中的路由和服务发现拉取的路由
@Override
protected LinkedHashMap<String, ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
// 把 ZuulProperties 中的路由配置加进来
routesMap.putAll(super.locateRoutes());
if (this.discovery != null) {
// 静态服务 map,也就相当于遍历 ZuulProperties 中的路由配置,把他添加到 staticServices map 中去
Map<String, ZuulRoute> staticServices = new LinkedHashMap<String, ZuulRoute>();
for (ZuulRoute route : routesMap.values()) {
String serviceId = route.getServiceId();
if (serviceId == null) {
serviceId = route.getId();
}
if (serviceId != null) {
staticServices.put(serviceId, route);
}
}
/**
* 添加服务发现中的路由
*/
// 通过 DiscoveryClient 获取所有的服务名称(比如 usercenter,ordercenter 等)
List<String> services = this.discovery.getServices();
// ...
// 遍历获取到的所有的服务名称
for (String serviceId : services) {
// staticServices 静态服务配置是否已经配置了
if (staticServices.containsKey(serviceId)
&& staticServices.get(serviceId).getUrl() == null) {
ZuulRoute staticRoute = staticServices.get(serviceId);
if (!StringUtils.hasText(staticRoute.getLocation())) {
// 赋值 location
staticRoute.setLocation(serviceId);
}
}
// 如果静态服务配置没有配置
if (!PatternMatchUtils.simpleMatch(ignored, serviceId)
&& !routesMap.containsKey(key)) {
// 那么它添加到 routesMap 中去。
routesMap.put(key, new ZuulRoute(key, serviceId));
}
}
}
// ...
// 该方法返回值 map
LinkedHashMap<String, ZuulRoute> values = new LinkedHashMap<>();
// 遍历 routesMap
for (Entry<String, ZuulRoute> entry : routesMap.entrySet()) {
String path = entry.getKey();
// ...
values.put(path, entry.getValue());
}
return values;
}
// 这个方法一般是监听器 Listener 监听到注册中心更新事件时出发本地路由的更新
@Override
public void refresh() {
doRefresh();
}
}
CompositeRouteLocator
这有点类似于 WebMvcConfigurerComposite,CompositeRouteLocator 就是一个集成了所有 RouteLocator 实现的一个组合类
所以在后面源码中,出现 RouteLocator,都是 CompositeRouteLocator 的实现。
public class CompositeRouteLocator implements RefreshableRouteLocator {
// 默认情况下,zuul 的自动配置中会注册默认的 DiscoveryClientRouteLocator 和 SimpleRouteLocator 实现 bean
private final Collection<? extends RouteLocator> routeLocators;
private ArrayList<RouteLocator> rl;
public CompositeRouteLocator(Collection<? extends RouteLocator> routeLocators) {
Assert.notNull(routeLocators, "'routeLocators' must not be null");
rl = new ArrayList<>(routeLocators);
AnnotationAwareOrderComparator.sort(rl);
this.routeLocators = rl;
}
// 这个方法组合了所有 RouteLocator 的实现(包括 DiscoveryClientRouteLocator 和 SimpleRouteLocator)
@Override
public List<Route> getRoutes() {
List<Route> route = new ArrayList<>();
// 编译每个 RouteLocator,然后调用每个 RouteLocator 的 getRoutes()方法聚合全部 Route
for (RouteLocator locator : routeLocators) {
route.addAll(locator.getRoutes());
}
return route;
}
@Override
public Route getMatchingRoute(String path) {
for (RouteLocator locator : routeLocators) {
Route route = locator.getMatchingRoute(path);
if (route != null) {
return route;
}
}
return null;
}
@Override
public void refresh() {
for (RouteLocator locator : routeLocators) {
if (locator instanceof RefreshableRouteLocator) {
((RefreshableRouteLocator) locator).refresh();
}
}
}
}
CompositeRouteLocator#getRoutes()
方法这里其实我存在一个疑问的,我抛出来给你们:
SimpleRouteLocator#getRoutes()
包含了 ZuulProperties 的路由,而 DiscoveryClientRouteLocator 继承了 SimpleRouteLocator,它的 getRoutes()
包含了 ZuulProperties 的路由,然后 CompositeRouteLocator#getRoutes()
方法把这两个实现获取的 Route 集合通过 addAll()
的的形式再组合成一个新的集合,那这样的 ZuulProperties 的配置路由不就重复了么,尽管是不影响路由匹配,你们觉得呢?
RefreshableRouteLocator
当路由发生改变时,route locator 可以刷新本地缓存的一个接口,目前 DiscoveryClientRouteLocator 实现了该接口。
该接口就一个方法:
public interface RefreshableRouteLocator extends RouteLocator {
void refresh();
}
当 Listener 监听到注册中心更新事件时会触发本地路由的更新
还不知道这行代码的 this.zuulHandlerMapping.setDirty(true);
先往下看,后面就知道了。
Zuul 默认的 filter
再回到前面埋的坑,Zuul 有哪些默认的 Filter 呢,发送服务调用也是在 Filter 中去完成么,答案:是的
Pre 过滤器
Pre 类型的过滤器主要是在路由转发请求前的前置处理,比如对 request 的包装,初始化真正转发时需要的一些参数,匹配 Route 等等
ServletDetectionFilter(了解即可)
public class ServletDetectionFilter extends ZuulFilter {
// ...
@Override
public int filterOrder() {
// 优先级为-3,数字越小,越先执行
return SERVLET_DETECTION_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
// 阅读源码过程中,存在过这样一个疑问:执行到这里时,request 设置到RequestContext 中了么?
// ZuulRunner#init() 中已经赋值了
HttpServletRequest request = ctx.getRequest();
if (!(request instanceof HttpServletRequestWrapper)
&& isDispatcherServletRequest(request)) {
// 如果是 DispatcherServlet 过来的,则存储该标识
// 一般的网关请求都是 DispatcherServlet 进来的,我后面会说到。
ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, true);
} else {
ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, false);
}
return null;
}
// ...
}
检测当前请求是通过 Spring 的 DispatcherServlet 处理运行的,还是通过 ZuulServlet 来处理运行的。它的检测结果会以布尔类型保存在当前请求上下文的 isDispatcherServletRequest 参数中,这样后续的过滤器中,我们就可以通过 RequestUtils.isDispatcherServletRequest()
和 ~方法来判断请求处理的源头,以实现后续不同的处理机制
Servlet30WrapperFilter(了解即可)
优先级 -2,ServletDetectionFilter 之后执行
@Override
public boolean shouldFilter() {
return true;
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if (request instanceof HttpServletRequestWrapper) {
request = (HttpServletRequest) ReflectionUtils.getField(this.requestField,
request);
ctx.setRequest(new Servlet30RequestWrapper(request));
}
// 是否是 DispatcherServlet 过来的
else if (RequestUtils.isDispatcherServletRequest()) {
// 包装原始 request
ctx.setRequest(new Servlet30RequestWrapper(request));
}
return null;
}
讲到这里,觉得有必要说下 Zuul 的 com.netflix.zuul.http.HttpServletRequestWrapper
HttpServletRequestWrapper 是对原始 HttpServletRequest 的一个包装。我们知道 HttpServletRequest 的 getInputStream()
方法是不可重复读的。
所以 Zuul 就对它做了一个包装的增强,使得 getInputStream()
可以重复读。原理也很简单,就是本地缓存。
而 Servlet30RequestWrapper 也是继承了 com.netflix.zuul.http.HttpServletRequestWrapper
的,那就相当于在这个过滤器中存储到 RequestContext 中的 request 是可以重复读取 getInputStream()
的。
FormBodyWrapperFilter(了解即可)
它的执行顺序为 -1,是第三个执行的过滤器。该过滤器仅对两类请求生效,第一类是 Context-Type 为 application/x-www-form-urlencoded
的请求,第二类是 Context-Type 为 multipart/form-data
并且是由 Spring 的 DispatcherServlet 处理的请求(用到了ServletDetectionFilter 的处理结果)。而该过滤器的主要目的是将符合要求的请求体包装成 FormBodyRequestWrapper 对象。
PS: FormBodyRequestWrapper 是继承了 Servlet30RequestWrapper。
DebugFilter(了解即可)
它的执行顺序为 1,是第四个执行的过滤器。该过滤器会根据配置参数 zuul.debug.request
和请求中的 debug 参数来决定是否执行过滤器中的操作。而它的具体操作内容是将当前请求上下文中的 debugRouting 和 debugRequest 参数设置为 true。由于在同一个请求的不同生命周期都可以访问到这二个值,所以我们在后续的各个过滤器中可以利用这二个值来定义一些 debug 信息,这样当线上环境出现问题的时候,可以通过参数的方式来激活这些 debug 信息以帮助分析问题,另外,对于请求参数中的 debug 参数,我们可以通过zuul.debug.parameter 来进行自定义。
PreDecorationFilter(较为重要)
执行顺序是 5,是 pre 阶段最后被执行的过滤器,主要是给当前请求上下文中赋值 forward.to
和 serviceId
参数,这两个参数在后面转发和发起调用具体服务时会用到。
public class PreDecorationFilter extends ZuulFilter {
@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());
// 请求 uri 匹配获取 Route
Route route = this.routeLocator.getMatchingRoute(requestURI);
if (route != null) {
// 还记得吗,前面 ZuulProperties 有介绍过这个方法。
// 所以这里获取的 location 是 Route 配置中的 url or serviceId。
String location = route.getLocation();
// locationy 以 http 或 https 开头
if (location.startsWith(HTTP_SCHEME+":") || location.startsWith(HTTPS_SCHEME+":")) {
// 存储在ctx 上下文中,SimpleHostRoutingFilter 会使用使用 该routeHost 发起http 请求完成路由转发
ctx.setRouteHost(getUrl(location));
ctx.addOriginResponseHeader(SERVICE_HEADER, location);
}
// 如果 location 是以 "forward:" 开头,存储对应的属性,SendForwardFilter 会使用到
else if (location.startsWith(FORWARD_LOCATION_PREFIX)) {
ctx.set(FORWARD_TO_KEY,
StringUtils.cleanPath(location.substring(FORWARD_LOCATION_PREFIX.length()) + route.getPath()));
ctx.setRouteHost(null);
return null;
}
else {
// 这个分支,是我们公司主要跑的这种情况,set serviceId,支持服务发现,负载均衡等
// RibbonRoutingFilter
ctx.set(SERVICE_ID_KEY, location);
ctx.setRouteHost(null);
ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location);
}
}
}
}
Route 过滤器
这类型就是路由过滤去,比如请求转发服务。
RibbonRoutingFilter
该过滤器只对请求上下文中存在 serviceId
参数的请求进行处理,即只对通过 serviceId
配置路由规则的请求生效
public class RibbonRoutingFilter extends ZuulFilter {
@Override
public String filterType() {
return ROUTE_TYPE;
}
// order 值为 5,是 Route 类型执行的第一个 Filter
@Override
public int filterOrder() {
return RIBBON_ROUTING_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
// 路由配置中,url 为空,serviceId 不为空时满足条件
return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
&& ctx.sendZuulResponse());
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
this.helper.addIgnoredHeaders();
try {
// 封装 ribbon 上下文
RibbonCommandContext commandContext = buildCommandContext(context);
// 包装请求,发起向服务名称为 serviceId的请求调用,完成网关的转发功能
ClientHttpResponse response = forward(commandContext);
// 暂存请求结果到 context 中
setResponse(response);
return response;
}
catch (ZuulException ex) {
throw new ZuulRuntimeException(ex);
}
catch (Exception ex) {
throw new ZuulRuntimeException(ex);
}
}
}
SimpleHostRoutingFilter
该过滤器只对请求上下文存在 routeHost
参数的请求进行处理,即只对通过 url 配置路由规则的请求生效
public class SimpleHostRoutingFilter extends ZuulFilter {
@Override
public String filterType() {
return ROUTE_TYPE;
}
// order 为 100,在 RibbonRoutingFilter 之后
@Override
public int filterOrder() {
return SIMPLE_HOST_ROUTING_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
// RouteHost 不为空,
// 也就是当路由配置中,serviceid 为空,url 不为空时(在 PreDecorationFilter 中赋值的 RouteHost)
return RequestContext.getCurrentContext().getRouteHost() != null
&& RequestContext.getCurrentContext().sendZuulResponse();
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
// ...
String uri = this.helper.buildZuulRequestURI(request);
this.helper.addIgnoredHeaders();
try {
// 通过 HttpClient 发起实际请求,完成转发
CloseableHttpResponse response = forward(this.httpClient, verb, uri, request,
headers, params, requestEntity);
setResponse(response);
}
catch (Exception ex) {
throw new ZuulRuntimeException(ex);
}
return null;
}
}
SendForwardFilter
该过滤器只对请求上下文中存在的 forward.do
参数进行处理请求,即用来处理路由规则中的 forward 本地跳转装配
public class SendForwardFilter extends ZuulFilter {
@Override
public String filterType() {
return ROUTE_TYPE;
}
// order = 500,在 SimpleHostRoutingFilter 之后
@Override
public int filterOrder() {
return SEND_FORWARD_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
// PreDecorationFilter 中赋值的 FORWARD_TO_KEY 不为空
return ctx.containsKey(FORWARD_TO_KEY)
&& !ctx.getBoolean(SEND_FORWARD_FILTER_RAN, false);
}
@Override
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
String path = (String) ctx.get(FORWARD_TO_KEY);
// RequestDispatcher 服务端转发
RequestDispatcher dispatcher = ctx.getRequest().getRequestDispatcher(path);
if (dispatcher != null) {
ctx.set(SEND_FORWARD_FILTER_RAN, true);
if (!ctx.getResponse().isCommitted()) {
dispatcher.forward(ctx.getRequest(), ctx.getResponse());
ctx.getResponse().flushBuffer();
}
}
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
}
post 过滤器
SendResponseFilter
当请求完成之后,在该过滤器中返回响应到客户端。
public class SendResponseFilter extends ZuulFilter {
// ...
@Override
public String filterType() {
return POST_TYPE;
}
// order = 1000
@Override
public int filterOrder() {
return SEND_RESPONSE_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
// 异常上下文为空,并且转发响应体不为空则执行该过滤器
return context.getThrowable() == null
&& (!context.getZuulResponseHeaders().isEmpty()
|| context.getResponseDataStream() != null
|| context.getResponseBody() != null);
}
@Override
public Object run() {
try {
// 添加响应请求头
addResponseHeaders();
// 返回响应
writeResponse();
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
// ...
}
error 过滤器
SendErrorFilter
pre Filter
,route Filter
,post Filter
在执行过程中,任何一个 Filter 发生异常,则都会进入该过滤器,该过滤器中,主要是转发到错误页面或者默认的 /error
rest 接口中完成响应给客户端。我们可以通过配置禁掉该默认错误 Filter,通过自定义 Error Filter 来处理异常响应。
public class SendErrorFilter extends ZuulFilter {
@Override
public String filterType() {
return ERROR_TYPE;
}
@Override
public int filterOrder() {
return SEND_ERROR_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
// 上下文中的异常信息不为空则执行该过滤器
return ctx.getThrowable() != null
&& !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}
@Override
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
ZuulException exception = findZuulException(ctx.getThrowable());
HttpServletRequest request = ctx.getRequest();
// ...
RequestDispatcher dispatcher = request.getRequestDispatcher(
this.errorPath);
if (dispatcher != null) {
ctx.set(SEND_ERROR_FILTER_RAN, true);
if (!ctx.getResponse().isCommitted()) {
ctx.setResponseStatusCode(exception.nStatusCode);
// 转发到 error path
dispatcher.forward(request, ctx.getResponse());
}
}
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
}
由于全文篇幅太长,我把 Zuul 的源码解析分成了两篇文章,感兴趣可前往 Zuul源码解析(二)