Zuul的核心功能都是通过一个个过滤器(Filter)来实现的。本章,我就对Zuul的各种过滤器(Filter)进行讲解。Zuul一共有四种类型的过滤器,Spring Cloud启动时会注入以下类型的过滤器(数字表示优先级):
Pre过滤器:
- -3:ServletDetectionFilter
- -2:Servlet30WrapperFilter
- -1:FromBodyWrapperFilter
- 1:DebugFilter
- 5:PreDecorationFilter
Route过滤器:
- 10:RibbonRoutingFilter
- 100:SimpleHostRoutingFilter
- 500:SendForwardFilter
Post过滤器:
- 900:LocationRewriteFilter
- 1000:SendResponseFilter
Error过滤器:
- 0:SendErrorFilter
上述这些过滤器全部定义在Spring Cloud Netflix Zuul的filters包目录下:
一、整体流程
ZuulServlet在处理请求时,会将请求交给ZuulRunner处理,而ZuulRunner内部又将请求交给FilterProcessor处理,以Pre Filter为例:
// ZuulServlet.java
public void preRoute() throws ZuulException {
try {
// 执行所有Pre Filters
runFilters("pre");
} catch (ZuulException e) {
throw e;
} catch (Throwable e) {
throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass().getName());
}
}
public Object runFilters(String sType) throws Throwable {
// DEBUG日志
if (RequestContext.getCurrentContext().debugRouting()) {
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
// 获取所有Filters
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;
}
上述比较关键的一行代码是:List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
。这里会获取所有“Pre”类型的Filters,并对其按照优先级排序:
// FilterLoader.java
public List<ZuulFilter> getFiltersByType(String filterType) {
List<ZuulFilter> list = hashFiltersByType.get(filterType);
if (list != null) return list;
list = new ArrayList<ZuulFilter>();
Collection<ZuulFilter> filters = filterRegistry.getAllFilters();
for (Iterator<ZuulFilter> iterator = filters.iterator(); iterator.hasNext(); ) {
ZuulFilter filter = iterator.next();
if (filter.filterType().equals(filterType)) {
list.add(filter);
}
}
// 根据优先级排序,ZuulFilter实现了Comparable接口
Collections.sort(list);
hashFiltersByType.putIfAbsent(filterType, list);
return list;
}
二、Pre过滤器
我们首先来看Pre过滤器,默认情况下的Pre过滤器的执行流程如下:
2.1 ServletDetectionFilter
ServletDetectionFilter的功能比较简单,就是往请求上下文RequestContext中设置了一个标识IS_DISPATCHER_SERVLET_REQUEST_KEY
,用来表示该请求是直接来自DispatcherServlet还是ZuulServlet:
public class ServletDetectionFilter extends ZuulFilter {
//...
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// 设置标识
if (!(request instanceof HttpServletRequestWrapper)
&& isDispatcherServletRequest(request)) {
ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, true);
}
else {
ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, false);
}
return null;
}
private boolean isDispatcherServletRequest(HttpServletRequest request) {
return request.getAttribute(
DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null;
}
}
2.2 Servlet30WrapperFilter
Servlet30WrapperFilter的功能也很简单,就是对HttpServletRequest进行了一层包装:
public class Servlet30WrapperFilter extends ZuulFilter {
//..
@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));
}
else if (RequestUtils.isDispatcherServletRequest()) {
ctx.setRequest(new Servlet30RequestWrapper(request));
}
return null;
}
}
class Servlet30RequestWrapper extends HttpServletRequestWrapper {
private HttpServletRequest request;
Servlet30RequestWrapper(HttpServletRequest request) {
super(request);
this.request = request;
}
@Override
public HttpServletRequest getRequest() {
return this.request;
}
}
2.3 FormBodyWrapperFilter
FormBodyWrapperFilter一般情况下不会执行,仅当是表单请求(application/x-www-form-urlencoded
)或直接来自DispatcherServlet的文件上传请求时才会执行:
// FormBodyWrapperFilter.java
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String contentType = request.getContentType();
// Don't use this filter on GET method
if (contentType == null) {
return false;
}
// Only use this filter for form data and only for multipart data in a DispatcherServlet handler
try {
MediaType mediaType = MediaType.valueOf(contentType);
return MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType)
|| (isDispatcherServletRequest(request)
&& MediaType.MULTIPART_FORM_DATA.includes(mediaType));
}
catch (InvalidMediaTypeException ex) {
return false;
}
}
此外,它的主要功能是用一个FormBodyRequestWrapper对象包装了下HttpServletRequest:
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
FormBodyRequestWrapper wrapper = null;
if (request instanceof HttpServletRequestWrapper) {
HttpServletRequest wrapped = (HttpServletRequest) ReflectionUtils
.getField(this.requestField, request);
// 包装HttpServletRequest
wrapper = new FormBodyRequestWrapper(wrapped);
ReflectionUtils.setField(this.requestField, request, wrapper);
if (request instanceof ServletRequestWrapper) {
ReflectionUtils.setField(this.servletRequestField, request, wrapper);
}
}
else {
wrapper = new FormBodyRequestWrapper(request);
ctx.setRequest(wrapper);
}
if (wrapper != null) {
ctx.getZuulRequestHeaders().put("content-type", wrapper.getContentType());
}
return null;
}
2.4 DebugFilter
DebugFilter,顾名思义,就是打开Debug标识,这样在后续运行过程中会打印一些Debug日志:
public class DebugFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
// 仅当请求中有参数:debug = true时,才会执行该Filter
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
if ("true".equals(request.getParameter(DEBUG_PARAMETER.get()))) {
return true;
}
return ROUTING_DEBUG.get();
}
@Override
public Object run() {
// 设置debug标识
RequestContext ctx = RequestContext.getCurrentContext();
ctx.setDebugRouting(true);
ctx.setDebugRequest(true);
return null;
}
}
2.5 PreDecorationFilter
PreDecorationFilter,是Pre Filter中最核心的一个过滤器。它的核心作用就是解析请求URI,然后根据路由定位器(RouteLocater)找到与该URI匹配的路由:
// PreDecorationFilter.java
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
// 1.获取请求URI
final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
// 2.匹配路由
Route route = this.routeLocator.getMatchingRoute(requestURI);
if (route != null) {
String location = route.getLocation();
if (location != null) {
ctx.put(REQUEST_URI_KEY, route.getPath());
ctx.put(PROXY_KEY, route.getId());
// 省略设置RequestContext的信息...
}
}
else {
log.warn("No route found for uri: " + requestURI);
String forwardURI = getForwardUri(requestURI);
ctx.set(FORWARD_TO_KEY, forwardURI);
}
return null;
}
我们来看最核心的一行代码:this.routeLocator.getMatchingRoute(requestURI)
。默认情况下,routeLocator就是SimpleRouteLocator
,它的作用就是根据application.yml
中的route配置与请求URI进行匹配,找到一个匹配的Route,然后会将Route信息保存到请求上下文:
// SimpleRouteLocator.java
protected Route getRoute(ZuulRoute route, String path) {
if (route == null) {
return null;
}
if (log.isDebugEnabled()) {
log.debug("route matched=" + route);
}
// 下面的代码的核心目的就是解析URI和构造匹配的Route对象
String targetPath = path;
String prefix = this.properties.getPrefix();
if (prefix.endsWith("/")) {
prefix = prefix.substring(0, prefix.length() - 1);
}
if (path.startsWith(prefix + "/") && this.properties.isStripPrefix()) {
targetPath = path.substring(prefix.length());
}
if (route.isStripPrefix()) {
int index = route.getPath().indexOf("*") - 1;
if (index > 0) {
String routePrefix = route.getPath().substring(0, index);
targetPath = targetPath.replaceFirst(routePrefix, "");
prefix = prefix + routePrefix;
}
}
Boolean retryable = this.properties.getRetryable();
if (route.getRetryable() != null) {
retryable = route.getRetryable();
}
return new Route(route.getId(), targetPath, route.getLocation(), prefix,
retryable,
route.isCustomSensitiveHeaders() ? route.getSensitiveHeaders() : null,
route.isStripPrefix());
}
举个例子,比如我们的路由配置如下,请求URI是http://ZuulServer/demo/sayHello
:
zuul:
routes:
MyService:
path: /demo/**
那么解析返回的Route对象如下:
{
"id": 'MyService',
"fullPath": '/demo/sayHello',
"path": '/sayHello',
"location": 'MyService',
"prefix": '/demo',
"retryable": false,
"sensitiveHeaders": [],
"customSensitiveHeaders": false,
"prefixStripped": true
}
三、Route过滤器
我们再来看Route过滤器,默认情况下的Route过滤器的执行流程如下:
3.1 RibbonRoutingFilter
RibbonRoutingFilter,基于Ribbon负载均衡将请求转发到对应的后端服务。它的核心功能就是构造了一个HystrixCommand—— HttpClientRibbonCommand ,这个Command集成了Ribbon的功能,所以最后就变成了执行HystrixCommand实现基于Ribbon的服务调用。整个流程可以用下面这张表表述:
// RibbonRoutingFilter.java
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
this.helper.addIgnoredHeaders();
try {
// 1.构造一个包含Ribbon相关信息的请求上下文
RibbonCommandContext commandContext = buildCommandContext(context);
// 2.调用后端服务
ClientHttpResponse response = forward(commandContext);
// 3.设置响应
setResponse(response);
return response;
}
//...
}
上述代码的重点就是forward
这个方法,它的内部创建一个HystrixCommand:
// RibbonRoutingFilter.java
protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
//...
// 1.构造一个HystrixCommand
RibbonCommand command = this.ribbonCommandFactory.create(context);
try {
// 2.执行Command
ClientHttpResponse response = command.execute();
this.helper.appendDebug(info, response.getRawStatusCode(),
response.getHeaders());
return response;
}
catch (HystrixRuntimeException ex) {
return handleException(info, ex);
}
}
我们来看这个HystrixCommand是如何创建的:
// HttpClientRibbonCommandFactory.java
public HttpClientRibbonCommand create(final RibbonCommandContext context) {
FallbackProvider zuulFallbackProvider = getFallbackProvider(context.getServiceId());
// 服务ID
final String serviceId = context.getServiceId();
// Ribbon客户端
final RibbonLoadBalancingHttpClient client = this.clientFactory
.getClient(serviceId, RibbonLoadBalancingHttpClient.class);
client.setLoadBalancer(this.clientFactory.getLoadBalancer(serviceId));
// 创建一个HttpClientRibbonCommand
return new HttpClientRibbonCommand(serviceId, client, context, zuulProperties,
zuulFallbackProvider, clientFactory.getClientConfig(serviceId));
}
最后,HttpClientRibbonCommand的执行代码如下,实际是调用了父类AbstractRibbonCommand的run()方法:
// AbstractRibbonCommand.java
protected ClientHttpResponse run() throws Exception {
final RequestContext context = RequestContext.getCurrentContext();
RQ request = createRequest();
RS response;
boolean retryableClient = this.client instanceof AbstractLoadBalancingClient
&& ((AbstractLoadBalancingClient) this.client)
.isClientRetryable((ContextAwareRequest) request);
if (retryableClient) {
// 利用RibbonClient发起调用,内部会和Eureka集成
response = this.client.execute(request, config);
}
else {
response = this.client.executeWithLoadBalancer(request, config);
}
context.set("ribbonResponse", response);
if (this.isResponseTimedOut()) {
if (response != null) {
response.close();
}
}
return new RibbonHttpResponse(response);
}
3.2 SimpleHostRoutingFilter
SimpleHostRoutingFilter,直接将请求转发到后端的某个URL。这个Filter的使用场景一般是后端服务就是一个URL地址,所以它的主要逻辑是对请求URI和目标主机地址进行解析,然后利用底层的HttpClient发起调用:
// SimpleHostRoutingFilter.java
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
MultiValueMap<String, String> headers = this.helper.buildZuulRequestHeaders(request);
MultiValueMap<String, String> params = this.helper.buildZuulRequestQueryParams(request);
String verb = getVerb(request);
InputStream requestEntity = getRequestBody(request);
if (getContentLength(request) < 0) {
context.setChunkedRequestBody();
}
// 构造请求URI
String uri = this.helper.buildZuulRequestURI(request);
this.helper.addIgnoredHeaders();
try {
// 请求调用
CloseableHttpResponse response = forward(this.httpClient, verb, uri, request,
headers, params, requestEntity);
// 设置响应
setResponse(response);
}
catch (Exception ex) {
throw new ZuulRuntimeException(handleException(ex));
}
return null;
}
private CloseableHttpResponse forward(CloseableHttpClient httpclient, String verb,
String uri, HttpServletRequest request,
MultiValueMap<String, String> headers,
MultiValueMap<String, String> params,
InputStream requestEntity) throws Exception {
Map<String, Object> info = this.helper.debug(verb, uri, headers, params, requestEntity);
URL host = RequestContext.getCurrentContext().getRouteHost();
// 获取请求目的主机信息
HttpHost httpHost = getHttpHost(host);
uri = StringUtils.cleanPath(MULTIPLE_SLASH_PATTERN.matcher(host.getPath() + uri).replaceAll("/"));
long contentLength = getContentLength(request);
ContentType contentType = null;
if (request.getContentType() != null) {
contentType = ContentType.parse(request.getContentType());
}
InputStreamEntity entity = new InputStreamEntity(requestEntity, contentLength,contentType);
HttpRequest httpRequest = buildHttpRequest(verb, uri, entity, headers, params,request);
try {
// 执行请求
CloseableHttpResponse zuulResponse = forwardRequest(httpclient, httpHost,httpRequest);
return zuulResponse;
}
finally {
}
}
private CloseableHttpResponse forwardRequest(CloseableHttpClient httpclient,
HttpHost httpHost,
HttpRequest httpRequest) throws IOException {
// 就是利用了HttpClient进行调用
return httpclient.execute(httpHost, httpRequest);
}
3.3 SendForwardFilter
SendForwardFilter,会将请求转发给Zuul网关自己的某个服务接口。它会根据请求上下文的信息判断是否需要重定向,如果需要就会通过RequestDispatcher
完成请求重定向:
// SendForwardFilter.java
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
// 请求转发URI
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过滤器
我们再来看Post过滤器,默认情况下的Post过滤器的执行流程如下:
4.1 LocationRewriteFilter
LocationRewriteFilter,这个Filter默认情况下是不执行的,它会根据Http响应码来判断是否需要执行,当响应码为3开头时才会执行:
// LocationRewriteFilter.java
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
int statusCode = ctx.getResponseStatusCode();
// 以3开头的http响应码,
return HttpStatus.valueOf(statusCode).is3xxRedirection();
}
LocationRewriteFilter的作用就是当需要重定向时(注意与请求转发的区别),修改http响应头的信息:
// LocationRewriteFilter.java
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
Route route = routeLocator.getMatchingRoute(urlPathHelper.getPathWithinApplication(ctx.getRequest()));
if (route != null) {
Pair<String, String> lh = locationHeader(ctx);
if (lh != null) {
// 重定向地址
String location = lh.second();
URI originalRequestUri = UriComponentsBuilder
.fromHttpRequest(new ServletServerHttpRequest(ctx.getRequest()))
.build().toUri();
UriComponentsBuilder redirectedUriBuilder = UriComponentsBuilder
.fromUriString(location);
UriComponents redirectedUriComps = redirectedUriBuilder.build();
String newPath = getRestoredPath(this.zuulProperties, route,
redirectedUriComps);
String modifiedLocation = redirectedUriBuilder
.scheme(originalRequestUri.getScheme())
.host(originalRequestUri.getHost())
.port(originalRequestUri.getPort()).replacePath(newPath).build()
.toUriString();
lh.setSecond(modifiedLocation);
}
}
return null;
}
4.2 SendResponseFilter
SendResponseFilter,顾名思义,就是最后响应客户端请求的Filter。它的核心逻辑就是添加响应头,然后写字节流:
// SendResponseFilter.java
public Object run() {
try {
// 1.添加一些响应头信息
addResponseHeaders();
// 2.写响应数据流
writeResponse();
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
具体代码我就不贴了,就是些常规的J2EE Servlet的HttpServletResponse的数据读写方法。
五、Error过滤器
最后,我们来看下Error过滤器, Error过滤器会在Pre、Route、Post过滤器执行过程中抛出异常时执行,但是它执行完成后最终还是会流向Post过滤器 ,因为需要通过Post过滤器将请求结果返回给客户端。
5.1 SendErrorFilter
在Pre、Route、Post阶段,任何一个阶段抛出异常,都会执行SendErrorFilter,它最终会将请求转发到/error
路径::
// SendErrorFilter.java
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
ExceptionHolder exception = findZuulException(ctx.getThrowable());
HttpServletRequest request = ctx.getRequest();
request.setAttribute("javax.servlet.error.status_code",exception.getStatusCode());
log.warn("Error during filtering", exception.getThrowable());
request.setAttribute("javax.servlet.error.exception", exception.getThrowable());
if (StringUtils.hasText(exception.getErrorCause())) {
request.setAttribute("javax.servlet.error.message", exception.getErrorCause());
}
// 请求转发,this.errorPath == ${error.path:/error},默认为/error,可通过参数配置
RequestDispatcher dispatcher = request.getRequestDispatcher(this.errorPath);
if (dispatcher != null) {
ctx.set(SEND_ERROR_FILTER_RAN, true);
if (!ctx.getResponse().isCommitted()) {
ctx.setResponseStatusCode(exception.getStatusCode());
dispatcher.forward(request, ctx.getResponse());
}
}
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
六、总结
本章,我对Spring Cloud Netflix Zuul中的默认过滤器进行了详细讲解。至此,Zuul的核心源码就分析完了。其实Zuul相对于其它几个Netflix微服务框架的源码,本身就是比较简单的,它的核心就是利用了职责链模式对请求进行拦截处理,以及路由匹配机制。
从下一章开始,我就要进入实践环节的讲解了,理解了Netfilx微服务框架的源码,在实践中去运用这些框架将会知其所以然。我将引入一个分布式电商系统作为示例进行讲解,重点针对生产运行过程中Spring Cloud Netflix的各个框架的核心参数进行分析。
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。