zuul 作为springCloud 的全家桶组件之一,有着不可或缺的分量。它作为一个普通java API网关,自有网关的好处:

    避免将内部信息暴露给外部;
    统一服务端应用入口;
    为微服务添加额外的安全层;
    支持混合通信协议;
    降低构建微服务的复杂性;
    微服务模拟与虚拟化;

  zuul 基本上已经被springCloud 处理为一个开箱即用的一个组件了,所以基本上只需要添加相应依赖和一些必要配置,该网关就可以跑起来了。(这和nginx反该功能看起来是差不多的)

  让我们来快速实践一下吧!

一、zuul入坑基本实践步骤

1. 引入 pom 依赖

    <parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent> <modelVersion>4.0.0</modelVersion>
<groupId>zuul-test</groupId>
<artifactId>com.youge</artifactId>
<version>1.0</version> <!-- 引入spingcloud 全家桶 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RC2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement> <dependencies>
<!-- 导入服务网关zuul -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>

  以上就是我们整个demo的全部maven依赖了,很简洁吧。这也是springboot的初衷,把所有的工作都移到幕后,让业务更简洁。

2. 编写网关入口类

  如下为整个网关的入口类,实际上就是两个注解发生了化学反应。@EnableZuulProxy 是本文的主角,它会开启网关相关的服务。

@SpringBootApplication
@EnableZuulProxy
public class MyZuulGateway {
// 只有一个空的main方法
public static void main(String[] args) {
SpringApplication.run(MyZuulGateway.class, args);
}
}

  就是这么简单!

3. 添加测试配置项

  在application.properties配置文件中添加如下配置,主要使用一个路由配置验证即可!

server.port=9000
spring.application.name=my-zuul-gateway #本地环境配置zuul转发的规则:
# 忽略所有微服务,只路由指定微服务
# 如下配置为将 /sapi/** 的路径请求,转发到 http://127.0.0.1:8082/file-encrypt-service/ 上去。
zuul.ignored-services=*
zuul.routes.fileenc1.url=http://127.0.0.1:8082/fileenc/
zuul.routes.fileenc1.path=/sapi/**

  如上就可以将网关跑起来了,如果你连后台服务也没有,没关系,自己写一个就好了。

    @GetMapping("hello")
public Object hello() {
return "hello, world";
}

  

4. 测试网关

  以上就已经将整个网关搞好了,run一下就ok. 测试方式就是直接浏览器里访问下该网关地址就好了: http://localhost:9000/sapi/test/hello .

  如果你看到 “hello, world”, 恭喜你,zuul已入坑。

二、zuul是如何转发请求的?

  根据上面的观察,zuul已经基本可以满足我们的开发需求了,后续更多要做的可能就是一些安全相关,业务相关,优化相关的东西了。不过在做这些之前,我们可以先多问一个问题,zuul是如何将请求转发给后台服务的呢?

  这实际上和zuul的架构相关:

  zuul的中核心概念是:Filter. 运行时分为  PRE:这种过滤器在请求被路由之前调用;ROUTING:这种过滤器将请求路由到微服务;POST:这种过滤器在路由到微服务以后执行;ERROR:在其他阶段发生错误时执行该过滤器;

  所以,整体上来说,它的转发流程会经过一系列的过滤器,然后再进行实际的转发。如果只想了解其最终是如何转的可以直奔主题,而如果要添加你的功能,则需要编写一些前置的过滤器。

  原本要分析zuul是如何处理请求的,但是实际上,zuul被整合到spring之后,就完全地符合了一个springmvc的编程模型了。所有对该网关的请求会先调用 ZuulController 进行请求的接收,然后到 service处理,再到响应这么一个过程。

  整个 ZuulController 非常地简单:就是一个请求的委托过程!

// org.springframework.cloud.netflix.zuul.web.ZuulController
public class ZuulController extends ServletWrappingController { public ZuulController() {
setServletClass(ZuulServlet.class);
setServletName("zuul");
setSupportedMethods((String[]) null); // Allow all
} @Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
// We don't care about the other features of the base class, just want to
// handle the request
return super.handleRequestInternal(request, response);
}
finally {
// @see com.netflix.zuul.context.ContextLifecycleFilter.doFilter
RequestContext.getCurrentContext().unset();
}
} }
// org.springframework.web.servlet.mvc.ServletWrappingController#handleRequestInternal
/**
* Invoke the wrapped Servlet instance.
* @see javax.servlet.Servlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
*/
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
throws Exception { Assert.state(this.servletInstance != null, "No Servlet instance");
// 该 servletInstance 是 ZuulServlet, 整个zuul的实现框架由其控制
this.servletInstance.service(request, response);
return null;
}
// com.netflix.zuul.http.ZuulServlet#service
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
// 初始化请求,由 zuulRunner 处理
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse); // Marks this request as having passed through the "Zuul engine", as opposed to servlets
// explicitly bound in web.xml, for which requests will not have the same data attached
// setZuulEngineRan 会旋转一个标识: "zuulEngineRan", true
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan(); try {
// 前置过滤器
preRoute();
} catch (ZuulException e) {
error(e);
// 异常时直接调用后置路由完成请求
postRoute();
return;
}
try {
// 正常的路由请求处理
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
// 正常地后置路由处理
postRoute();
} catch (ZuulException e) {
error(e);
return;
} } catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
// 重置上下文,以备下次使用
RequestContext.getCurrentContext().unset();
}
}

  以上就是整个zuul对于普通请求的处理框架部分了。逻辑还是比较清晰的,简单的,前置+转发+后置处理。我们就几个重点部分说明一下:

2.1. 请求初始化

  该部分主要是将外部请求,接入到 zuul 的处理流程上,当然下面的实现主要是使用了 ThreadLocal 实现了上下文的衔接。

    // com.netflix.zuul.http.ZuulServlet#init
/**
* initializes request
*
* @param servletRequest
* @param servletResponse
*/
void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
zuulRunner.init(servletRequest, servletResponse);
}
// com.netflix.zuul.ZuulRunner#init
/**
* sets HttpServlet request and HttpResponse
*
* @param servletRequest
* @param servletResponse
*/
public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
// RequestContext 使用 ThreadLocal 进行保存,且保证有值
// 且 RequestContext 继承了 ConcurrentHashMap, 保证了操作的线程安全
RequestContext ctx = RequestContext.getCurrentContext();
if (bufferRequests) {
ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
} else {
ctx.setRequest(servletRequest);
} ctx.setResponse(new HttpServletResponseWrapper(servletResponse));
}

  以上就是一个 zuul 请求的初始化了,简单地说就是设置好请求上下文,备用。

2.2. 前置处理过滤器

  前置处理过滤器主要用于标记一些请求类型,权限验证,安全过滤等等。是不可或缺一环。具体实现自行处理!我们来看一个整体的通用流程:

    // com.netflix.zuul.http.ZuulServlet#preRoute
/**
* executes "pre" filters
*
* @throws ZuulException
*/
void preRoute() throws ZuulException {
zuulRunner.preRoute();
}
// com.netflix.zuul.ZuulRunner#preRoute
/**
* executes "pre" filterType ZuulFilters
*
* @throws ZuulException
*/
public void preRoute() throws ZuulException {
// FilterProcessor 是个单例
FilterProcessor.getInstance().preRoute();
}
// com.netflix.zuul.FilterProcessor#preRoute
/**
* runs all "pre" filters. These filters are run before routing to the orgin.
*
* @throws ZuulException
*/
public void preRoute() throws ZuulException {
try {
// 调用Type 为 pre 的过滤器
runFilters("pre");
} catch (ZuulException e) {
throw e;
} catch (Throwable e) {
throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass().getName());
}
}
// com.netflix.zuul.FilterProcessor#runFilters
/**
* runs all filters of the filterType sType/ Use this method within filters to run custom filters by type
*
* @param sType the filterType.
* @return
* @throws Throwable throws up an arbitrary exception
*/
public Object runFilters(String sType) throws Throwable {
if (RequestContext.getCurrentContext().debugRouting()) {
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
// 通过 FilterLoader 的单例,获取所有注册为 sType 的过滤器
// 存放 Filters 的容器自然也是线程安全的,为 ConcurrentHashMap
// - org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter
// - org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter
// - org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter
// - org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter
// - org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter
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;
}
// 获取相应的 filters
// com.netflix.zuul.FilterLoader#getFiltersByType
/**
* Returns a list of filters by the filterType specified
*
* @param filterType
* @return a List<ZuulFilter>
*/
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);
}
}
Collections.sort(list); // sort by priority hashFiltersByType.putIfAbsent(filterType, list);
return list;
} // com.netflix.zuul.FilterProcessor#processZuulFilter
/**
* Processes an individual ZuulFilter. This method adds Debug information. Any uncaught Thowables are caught by this method and converted to a ZuulException with a 500 status code.
*
* @param filter
* @return the return value for that filter
* @throws ZuulException
*/
public Object processZuulFilter(ZuulFilter filter) throws ZuulException { RequestContext ctx = RequestContext.getCurrentContext();
boolean bDebug = ctx.debugRouting();
final String metricPrefix = "zuul.filter-";
long execTime = 0;
String filterName = "";
try {
long ltime = System.currentTimeMillis();
filterName = filter.getClass().getSimpleName(); RequestContext copy = null;
Object o = null;
Throwable t = null; if (bDebug) {
Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);
copy = ctx.copy();
}
// 调用各filter的 runFilter() 方法,触发filter作用
// 如果filter被禁用,则不会调用 zuul.ServletDetectionFilter.pre.disable=true, 代表禁用 pre
// 具体实现逻辑由各 filter 决定
ZuulFilterResult result = filter.runFilter();
ExecutionStatus s = result.getStatus();
execTime = System.currentTimeMillis() - ltime; switch (s) {
case FAILED:
t = result.getException();
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
break;
case SUCCESS:
o = result.getResult();
// 使用 StringBuilder 记录请求处理日志
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
if (bDebug) {
Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
Debug.compareContextState(filterName, copy);
}
break;
default:
break;
}
// 只要发生异常,则抛出
if (t != null) throw t;
// 请求计数器增加
usageNotifier.notify(filter, s);
return o; } catch (Throwable e) {
if (bDebug) {
Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage());
}
usageNotifier.notify(filter, ExecutionStatus.FAILED);
if (e instanceof ZuulException) {
throw (ZuulException) e;
} else {
ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
throw ex;
}
}
}
// com.netflix.zuul.ZuulFilter#runFilter
/**
* runFilter checks !isFilterDisabled() and shouldFilter(). The run() method is invoked if both are true.
*
* @return the return from ZuulFilterResult
*/
public ZuulFilterResult runFilter() {
ZuulFilterResult zr = new ZuulFilterResult();
// 如果被禁用则不会触发真正地调用
if (!isFilterDisabled()) {
// shouldFilter() 由各filter决定,返回true时执行filter
if (shouldFilter()) {
Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
try {
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;
}
// org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter#run
@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;
}

  如上,就是一个preFilter的处理流程了:

    1. 从 FilterLoader 中获取所有 pre 类型的filter;
    2. 依次调用各filter的runFilter()方法,触发filter;
    3. 调用前先调用 shouldFilter() 进行判断该filter对于此次请求是否有用, 各filter实现可以从上下文中取得相应的信息,各自判定;
    4. 计数器加1;
    5. 默认就会有多个filter可调用, 不够满足业务场景再自行添加;

2.3. 正常路由处理

  zuul 的本职工作,是对路径的转发路由(正向代理 or 反向代理),如下处理:

    // com.netflix.zuul.http.ZuulServlet#route
/**
* executes "route" filters
*
* @throws ZuulException
*/
void route() throws ZuulException {
zuulRunner.route();
}
// com.netflix.zuul.ZuulRunner#route
/**
* executes "route" filterType ZuulFilters
*
* @throws ZuulException
*/
public void route() throws ZuulException {
FilterProcessor.getInstance().route();
}
// com.netflix.zuul.FilterProcessor#route
/**
* Runs all "route" filters. These filters route calls to an origin.
*
* @throws ZuulException if an exception occurs.
*/
public void route() throws ZuulException {
try {
// 同样,获取filter类型为 route 的 filters, 进行调用处理即可
// - org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter
// - org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter
// - org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter
runFilters("route");
} catch (ZuulException e) {
throw e;
} catch (Throwable e) {
throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_ROUTE_FILTER_" + e.getClass().getName());
}
}
// 其中,Ribbon 的处理需要有 ribbon 组件的引入和配置
// org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter#shouldFilter
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
// 判断是否有 serviceId, 且 sendZuulResponse=true 才会进行 ribbon 处理
return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
&& ctx.sendZuulResponse());
} 以下是普通路由转发的实现,只要配置了相应的路由信息,则会进行相关转发:
// org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter#shouldFilter
@Override
public boolean shouldFilter() {
return RequestContext.getCurrentContext().getRouteHost() != null
&& RequestContext.getCurrentContext().sendZuulResponse();
} @Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
// step1. 构建http请求头信息
HttpServletRequest request = context.getRequest();
MultiValueMap<String, String> headers = this.helper
.buildZuulRequestHeaders(request);
// step2. 构建 params 信息, 如: a=111&&b=222
MultiValueMap<String, String> params = this.helper
.buildZuulRequestQueryParams(request);
// 获取请求类型, GET,POST,PUT,DELETE
String verb = getVerb(request);
// step3. 构建请求体信息,如文件
InputStream requestEntity = getRequestBody(request);
// 如果没有 Content-Length 字段,则设置 chunkedRequestBody:true
if (getContentLength(request) < 0) {
context.setChunkedRequestBody();
}
// step4. 构建要转发的uri地址信息
String uri = this.helper.buildZuulRequestURI(request);
this.helper.addIgnoredHeaders(); try {
// step5. 请求转发出去,等待响应
// 具体如何转发请求,是在 forward 中处理的
CloseableHttpResponse response = forward(this.httpClient, verb, uri, request,
headers, params, requestEntity);
// 将结果放到上下文中,以备后续filter处理
setResponse(response);
}
catch (Exception ex) {
throw new ZuulRuntimeException(ex);
}
return null;
} // step1. 构建http请求头信息
// org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper#buildZuulRequestHeaders
public MultiValueMap<String, String> buildZuulRequestHeaders(
HttpServletRequest request) {
RequestContext context = RequestContext.getCurrentContext();
MultiValueMap<String, String> headers = new HttpHeaders();
Enumeration<String> headerNames = request.getHeaderNames();
// 获取所有的 header 信息,还原到 headers 中
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
// 排除一些特别的的头信息
if (isIncludedHeader(name)) {
Enumeration<String> values = request.getHeaders(name);
while (values.hasMoreElements()) {
String value = values.nextElement();
headers.add(name, value);
}
}
}
}
// 添加本次路由转发新增的头信息
Map<String, String> zuulRequestHeaders = context.getZuulRequestHeaders();
for (String header : zuulRequestHeaders.keySet()) {
headers.set(header, zuulRequestHeaders.get(header));
}
headers.set(HttpHeaders.ACCEPT_ENCODING, "gzip");
return headers;
} // step2. 构建 params 信息, 如: a=111&&b=222
// org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper#buildZuulRequestQueryParams
public MultiValueMap<String, String> buildZuulRequestQueryParams(
HttpServletRequest request) {
// 解析 getQueryString 中的 a=111&b=222... 信息
Map<String, List<String>> map = HTTPRequestUtils.getInstance().getQueryParams();
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
if (map == null) {
return params;
}
for (String key : map.keySet()) {
for (String value : map.get(key)) {
params.add(key, value);
}
}
return params;
}
// 解析请求url中的k=v&k2=v2 为 map 格式
// com.netflix.zuul.util.HTTPRequestUtils#getQueryParams
/**
* returns query params as a Map with String keys and Lists of Strings as values
* @return
*/
public Map<String, List<String>> getQueryParams() { Map<String, List<String>> qp = RequestContext.getCurrentContext().getRequestQueryParams();
if (qp != null) return qp; HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); qp = new LinkedHashMap<String, List<String>>(); if (request.getQueryString() == null) return null;
StringTokenizer st = new StringTokenizer(request.getQueryString(), "&");
int i; while (st.hasMoreTokens()) {
String s = st.nextToken();
i = s.indexOf("=");
if (i > 0 && s.length() >= i + 1) {
String name = s.substring(0, i);
String value = s.substring(i + 1); try {
name = URLDecoder.decode(name, "UTF-8");
} catch (Exception e) {
}
try {
value = URLDecoder.decode(value, "UTF-8");
} catch (Exception e) {
} List<String> valueList = qp.get(name);
if (valueList == null) {
valueList = new LinkedList<String>();
qp.put(name, valueList);
} valueList.add(value);
}
else if (i == -1)
{
String name=s;
String value="";
try {
name = URLDecoder.decode(name, "UTF-8");
} catch (Exception e) {
} List<String> valueList = qp.get(name);
if (valueList == null) {
valueList = new LinkedList<String>();
qp.put(name, valueList);
} valueList.add(value); }
} RequestContext.getCurrentContext().setRequestQueryParams(qp);
return qp;
} // step3. 构建请求体信息,如文件
// org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter#getRequestBody
protected InputStream getRequestBody(HttpServletRequest request) {
InputStream requestEntity = null;
try {
// 先向 requestEntity 中获取输入流,如果没有则向 servlet 中获取
requestEntity = (InputStream) RequestContext.getCurrentContext().get(REQUEST_ENTITY_KEY);
if (requestEntity == null) {
// 向 HttpServletRequest 中获取原始的输入流
requestEntity = request.getInputStream();
}
}
catch (IOException ex) {
log.error("error during getRequestBody", ex);
}
return requestEntity;
} // step4. 构建要转发的uri地址信息
// org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper#buildZuulRequestURI
public String buildZuulRequestURI(HttpServletRequest request) {
RequestContext context = RequestContext.getCurrentContext();
// 原始请求 uri
String uri = request.getRequestURI();
// 路由转换之后的请求 uri
String contextURI = (String) context.get(REQUEST_URI_KEY);
if (contextURI != null) {
try {
// 防止乱码,urlencode 一下
uri = UriUtils.encodePath(contextURI, characterEncoding(request));
}
catch (Exception e) {
log.debug(
"unable to encode uri path from context, falling back to uri from request",
e);
}
}
return uri;
} // step5. 请求转发出去,等待响应
// 具体如何转发请求,是在 forward 中处理的
// org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter#forward
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
uri = StringUtils.cleanPath((host.getPath() + uri).replaceAll("/{2,}", "/"));
long contentLength = getContentLength(request); ContentType contentType = null; if (request.getContentType() != null) {
contentType = ContentType.parse(request.getContentType());
}
// 使用InputStreamEntity封装inputStream请求,该inputStream是从socket接入后的原始输入流
// 后续 httpclient 进行数据读取时,将由其进行提供相应读数据方法
InputStreamEntity entity = new InputStreamEntity(requestEntity, contentLength,
contentType);
// 构建本次要请求的数据,关键
HttpRequest httpRequest = buildHttpRequest(verb, uri, entity, headers, params,
request);
try {
log.debug(httpHost.getHostName() + " " + httpHost.getPort() + " "
+ httpHost.getSchemeName());
// 提交给 httpclient 组件执行 http 请求,并返回结果
CloseableHttpResponse zuulResponse = forwardRequest(httpclient, httpHost,
httpRequest);
this.helper.appendDebug(info, zuulResponse.getStatusLine().getStatusCode(),
revertHeaders(zuulResponse.getAllHeaders()));
return zuulResponse;
}
finally {
// When HttpClient instance is no longer needed,
// shut down the connection manager to ensure
// immediate deallocation of all system resources
// httpclient.getConnectionManager().shutdown();
}
}
// org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter#buildHttpRequest
protected HttpRequest buildHttpRequest(String verb, String uri,
InputStreamEntity entity, MultiValueMap<String, String> headers,
MultiValueMap<String, String> params, HttpServletRequest request) {
HttpRequest httpRequest;
String uriWithQueryString = uri + (this.forceOriginalQueryStringEncoding
? getEncodedQueryString(request) : this.helper.getQueryString(params));
// 根据原始请求的不同类型,做相应类型的转发
// 以下请求处理,都包含了对 文件流一类请求的逻辑
switch (verb.toUpperCase()) {
case "POST":
HttpPost httpPost = new HttpPost(uriWithQueryString);
httpRequest = httpPost;
httpPost.setEntity(entity);
break;
case "PUT":
HttpPut httpPut = new HttpPut(uriWithQueryString);
httpRequest = httpPut;
httpPut.setEntity(entity);
break;
case "PATCH":
HttpPatch httpPatch = new HttpPatch(uriWithQueryString);
httpRequest = httpPatch;
httpPatch.setEntity(entity);
break;
case "DELETE":
BasicHttpEntityEnclosingRequest entityRequest = new BasicHttpEntityEnclosingRequest(
verb, uriWithQueryString);
httpRequest = entityRequest;
// DELETE 时会做两步操作
entityRequest.setEntity(entity);
break;
default:
// 除以上几种情况,都使用 BasicHttpRequest 进行处理即可
httpRequest = new BasicHttpRequest(verb, uriWithQueryString);
log.debug(uriWithQueryString);
}
// 统一都设置请求头,将map转换为 BasicHeader
httpRequest.setHeaders(convertHeaders(headers));
return httpRequest;
}
// org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter#forwardRequest
private CloseableHttpResponse forwardRequest(CloseableHttpClient httpclient,
HttpHost httpHost, HttpRequest httpRequest) throws IOException {
return httpclient.execute(httpHost, httpRequest);
}

  可见整个真正的转发流程,主要分几步:

    1. 解析http请求头信息,并添加自己部分的头信息;
    2. 解析并保留请求参数信息, 如: a=111&&b=222;
    3. 获取原始的inputStream信息,如文件;
    4. 根据路由配置,构建要转发的uri地址信息;
    5. 使用httpclient组件,将请求转发出去,并等待响应,设置到 response中;

  实际上,真正的转发仍然是依次做好相应判断,然后还原成对应的请求,再转发后后端服务中。

  以上,就是一个普通的服务转发实现了。并没有太多的技巧,而是最基础的步骤:接收请求,解析参数,重新构建请求,请求后端,获得结果。

2.4. 后置过滤器

  后置处理器可以做一些请求完服务端之后,对客户端的响应数据,包括正常数据流的输出,错误信息的返回等。如 SendResponseFilter, SendErrorFilter...

    // com.netflix.zuul.http.ZuulServlet#postRoute
/**
* executes "post" ZuulFilters
*
* @throws ZuulException
*/
void postRoute() throws ZuulException {
zuulRunner.postRoute();
} // com.netflix.zuul.ZuulRunner#postRoute
/**
* executes "post" filterType ZuulFilters
*
* @throws ZuulException
*/
public void postRoute() throws ZuulException {
FilterProcessor.getInstance().postRoute();
} // com.netflix.zuul.FilterProcessor#postRoute
/**
* runs "post" filters which are called after "route" filters. ZuulExceptions from ZuulFilters are thrown.
* Any other Throwables are caught and a ZuulException is thrown out with a 500 status code
*
* @throws ZuulException
*/
public void postRoute() throws ZuulException {
try {
// 获取类型为 post 的 filter, 调用
// 默认为: org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter
runFilters("post");
} catch (ZuulException e) {
throw e;
} catch (Throwable e) {
throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_POST_FILTER_" + e.getClass().getName());
}
}
// org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter#shouldFilter
@Override
public boolean shouldFilter() {
// 有响应的数据,就可以进行处理
RequestContext context = RequestContext.getCurrentContext();
return context.getThrowable() == null
&& (!context.getZuulResponseHeaders().isEmpty()
|| context.getResponseDataStream() != null
|| context.getResponseBody() != null);
}
// org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter#run
@Override
public Object run() {
try {
// 添加header信息
addResponseHeaders();
// 输出数据流到请求端
writeResponse();
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
// org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter#addResponseHeaders
private void addResponseHeaders() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletResponse servletResponse = context.getResponse();
if (this.zuulProperties.isIncludeDebugHeader()) {
@SuppressWarnings("unchecked")
List<String> rd = (List<String>) context.get(ROUTING_DEBUG_KEY);
if (rd != null) {
StringBuilder debugHeader = new StringBuilder();
for (String it : rd) {
debugHeader.append("[[[" + it + "]]]");
}
servletResponse.addHeader(X_ZUUL_DEBUG_HEADER, debugHeader.toString());
}
}
// 向 response 中添加header
List<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders();
if (zuulResponseHeaders != null) {
for (Pair<String, String> it : zuulResponseHeaders) {
servletResponse.addHeader(it.first(), it.second());
}
}
if (includeContentLengthHeader(context)) {
Long contentLength = context.getOriginContentLength();
if(useServlet31) {
servletResponse.setContentLengthLong(contentLength);
} else {
//Try and set some kind of content length if we can safely convert the Long to an int
if (isLongSafe(contentLength)) {
servletResponse.setContentLength(contentLength.intValue());
}
}
}
}
// org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter#writeResponse()
private void writeResponse() throws Exception {
RequestContext context = RequestContext.getCurrentContext();
// there is no body to send
if (context.getResponseBody() == null
&& context.getResponseDataStream() == null) {
return;
}
HttpServletResponse servletResponse = context.getResponse();
if (servletResponse.getCharacterEncoding() == null) { // only set if not set
servletResponse.setCharacterEncoding("UTF-8");
} OutputStream outStream = servletResponse.getOutputStream();
InputStream is = null;
try {
if (context.getResponseBody() != null) {
String body = context.getResponseBody();
is = new ByteArrayInputStream(
body.getBytes(servletResponse.getCharacterEncoding()));
}
else {
is = context.getResponseDataStream();
if (is!=null && context.getResponseGZipped()) {
// if origin response is gzipped, and client has not requested gzip,
// decompress stream before sending to client
// else, stream gzip directly to client
if (isGzipRequested(context)) {
servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip");
}
else {
is = handleGzipStream(is);
}
}
} if (is!=null) {
writeResponse(is, outStream);
}
}
finally {
/**
* We must ensure that the InputStream provided by our upstream pooling mechanism is ALWAYS closed
* even in the case of wrapped streams, which are supplied by pooled sources such as Apache's
* PoolingHttpClientConnectionManager. In that particular case, the underlying HTTP connection will
* be returned back to the connection pool iif either close() is explicitly called, a read
* error occurs, or the end of the underlying stream is reached. If, however a write error occurs, we will
* end up leaking a connection from the pool without an explicit close()
*
* @author Johannes Edmeier
*/
if (is != null) {
try {
is.close();
}
catch (Exception ex) {
log.warn("Error while closing upstream input stream", ex);
}
} try {
Object zuulResponse = context.get("zuulResponse");
if (zuulResponse instanceof Closeable) {
((Closeable) zuulResponse).close();
}
outStream.flush();
// The container will close the stream for us
}
catch (IOException ex) {
log.warn("Error while sending response to client: " + ex.getMessage());
}
}
}
// org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter#writeResponse
private void writeResponse(InputStream zin, OutputStream out) throws Exception {
// 默认大小 8192
byte[] bytes = buffers.get();
int bytesRead = -1;
// 依次向 outputStream 中写入字节流
while ((bytesRead = zin.read(bytes)) != -1) {
out.write(bytes, 0, bytesRead);
}
}

  同样,对客户端的输出,就是这么简单:解析出header信息,将response write() 到客户端的socket中。即完成任务。

  以上,我们主要看了几个非常普通的filter的处理过程,理解了下 zuul 的运行流程,当然主要的目的分析zuul是如何转发请求的。基本上上面所有的filter都会继承 ZuulFilter 的抽象,它提供两个重要的统一的方法:isFilterDisabled() 和 shouldFilter() 方法用于控制过虑器是否启用或者是否应该使用,并统一了返回结果。

  zuul 整体实现也是非常简单明了,基于模板方法模式 和 责任链模式 和 单例模式,基本搞定。只是更多的花需要应用自己去玩了。

3. 自行实现一个业务filter

  要想做到通用的框架,这点事情是必须要做的。当然,还必须要足够简单,如下:一个注解加一个继承实现即可!

// 一个注解,@Component, 成功 spring bean 组件
// 一个继承,ZuulFilter, 使用 zuul 可以按照规范进行filter 的接入
@Component
public class MyOneFilter extends ZuulFilter { private final UrlPathHelper urlPathHelper = new UrlPathHelper(); @Autowired
private ZuulProperties zuulProperties; @Autowired
private RouteLocator routeLocator; public MyOneFilter() {
} public MyOneFilter(ZuulProperties zuulProperties,
RouteLocator routeLocator) {
this.routeLocator = routeLocator;
this.zuulProperties = zuulProperties;
} @Override
public String filterType() {
// 自定义过滤器的类型,知道为什么不用枚举类吗?嘿嘿
return PRE_TYPE;
} @Override
public int filterOrder() {
// 定义过滤器的出场顺序,越小越牛
return 1;
} @Override
public boolean shouldFilter() {
// 是否可以启用当前filter, 按你的业务规则来说了算
return true;
} @Override
public Object run() {
// 如果满足了过滤条件,你想怎么做都行,RequestContext中有你想要的一切
RequestContext ctx = RequestContext.getCurrentContext();
Route route = routeLocator.getMatchingRoute(
urlPathHelper.getPathWithinApplication(ctx.getRequest()));
System.out.println("in my one filter");
return null;
} }

  至于其他配置项什么的,自行查看官网即可! https://www.springcloud.cc/spring-cloud-greenwich.html#_router_and_filter_zuul

Spring zuul 快速入门实践 --看zuul如何进行服务转发的更多相关文章

  1. Spring Boot WebFlux 快速入门实践

    02:WebFlux 快速入门实践 Spring Boot 2.0 spring.io 官网有句醒目的话是: BUILD ANYTHING WITH SPRING BOOT Spring Boot ( ...

  2. Spring Boot WebFlux-01——WebFlux 快速入门实践

    第01课:WebFlux 快速入门实践 Spring Boot 2.0 spring.io 官网有句醒目的话是: BUILD ANYTHING WITH SPRING BOOT Spring Boot ...

  3. Spring Cloud 快速入门

     Spring Cloud快速入门 代码地址: https://gitee.com/gloryxu/spring-cloud-test EureKa:服务注册中心 添加依赖 <dependenc ...

  4. Spring Boot快速入门(二):http请求

    原文地址:https://lierabbit.cn/articles/4 一.准备 postman:一个接口测试工具 创建一个新工程 选择web 不会的请看Spring Boot快速入门(一):Hel ...

  5. Spring Boot 快速入门

    Spring Boot 快速入门 http://blog.csdn.net/xiaoyu411502/article/details/47864969 今天给大家介绍一下Spring Boot MVC ...

  6. spring boot入门教程——Spring Boot快速入门指南

    Spring Boot已成为当今最流行的微服务开发框架,本文是如何使用Spring Boot快速开始Web微服务开发的指南,我们将使创建一个可运行的包含内嵌Web容器(默认使用的是Tomcat)的可运 ...

  7. 快速入门python看过的一些资料

    我快速入门python看过的一些资料 B站的视频 10天自学Python,轻松掌握Python基础[千锋] 廖雪峰 - Python教程 https://www.liaoxuefeng.com/wik ...

  8. 笔记:Spring Cloud Zuul 快速入门

    Spring Cloud Zuul 实现了路由规则与实例的维护问题,通过 Spring Cloud Eureka 进行整合,将自身注册为 Eureka 服务治理下的应用,同时从 Eureka 中获取了 ...

  9. Spring Cloud Zuul 快速入门

    Spring Cloud Zuul 实现了路由规则与实例的维护问题,通过 Spring Cloud Eureka 进行整合,将自身注册为 Eureka 服务治理下的应用,同时从 Eureka 中获取了 ...

随机推荐

  1. $releasever 不正确解析

    [nginx] gpgcheck=0 baseurl=http://nginx.org/packages/centos/$releasever/$basearch/ name=nginx repo 这 ...

  2. 「雕爷学编程」Arduino动手做(36)——WS2812B 4位彩灯模块

    37款传感器与模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止37种的.鉴于本人手头积累了一些传感器和模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的,这里 ...

  3. vue与其他框架对比

    https://cn.vuejs.org/v2/guide/comparison.html 1. vue 框架的特点? MVVM框架模式 轻量级,灵活,容易上手 数据驱动 组件化(单文件组件) 插件化 ...

  4. 剑指Offer01之二维数组中查找目标数

    剑指Offer之二维数组中查找目标数 题目描述 ​ 在一个二维数组中(每个一维数组的长度相等),每一行都是从左到右递增的顺序排序,每一列都是从上到下递增的顺序排序,输入这样一个二维数组和一个整数,判断 ...

  5. Python 图像处理 OpenCV (3):图像属性、图像感兴趣 ROI 区域及通道处理

    前文传送门: 「Python 图像处理 OpenCV (1):入门」 「Python 图像处理 OpenCV (2):像素处理与 Numpy 操作以及 Matplotlib 显示图像」 图像属性 图像 ...

  6. java遇到的error解决

    解决Cannot change version of project facet Dynamic web module to 2.5 maven 不能设置为web3.0人解决方法 http://www ...

  7. 王艳 201771010127《面向对象程序设计(java)》第六周学习总结

    实验六 继承定义与使用 一:理论部分: 第五章:继承类. 1.继承:已有类来构建新类的一种机制.档定义了一个新类继承另一个类时,这个新类就继承了这个类的方法和域,同时在新类中添加新的方法和域以适应新的 ...

  8. J2EE项目分类管理中,提交表单数据是二进制形式时,对数据的修改失败。category赋值失败。

    原因: 在条件判断时,对字符串的比较进行了错误比较. 解决方法: A==B,比较的是两个字符串是否是同一个对象. A.equal(B),比较的是两个字符串内容是否相同. 出现错误是用了第一种比较,应该 ...

  9. 解决You should consider upgrading via the 'python -m pip install --upgrade pip' command. (pip工具版本较低导致)

    步骤1:  找到pip- 版本号 dist-info 文件夹 操作: 在python的安装目录下的Lib文件下的site-packages文件夹下找到 ip- 版本号 dist-info 文件夹   ...

  10. Python中ThreadLocal的理解与使用

    一.对 ThreadLocal 的理解 ThreadLocal,有的人叫它线程本地变量,也有的人叫它线程本地存储,其实意思一样. ThreadLocal 在每一个变量中都会创建一个副本,每个线程都可以 ...