Spring zuul 快速入门实践 --看zuul如何进行服务转发
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如何进行服务转发的更多相关文章
- Spring Boot WebFlux 快速入门实践
02:WebFlux 快速入门实践 Spring Boot 2.0 spring.io 官网有句醒目的话是: BUILD ANYTHING WITH SPRING BOOT Spring Boot ( ...
- Spring Boot WebFlux-01——WebFlux 快速入门实践
第01课:WebFlux 快速入门实践 Spring Boot 2.0 spring.io 官网有句醒目的话是: BUILD ANYTHING WITH SPRING BOOT Spring Boot ...
- Spring Cloud 快速入门
Spring Cloud快速入门 代码地址: https://gitee.com/gloryxu/spring-cloud-test EureKa:服务注册中心 添加依赖 <dependenc ...
- Spring Boot快速入门(二):http请求
原文地址:https://lierabbit.cn/articles/4 一.准备 postman:一个接口测试工具 创建一个新工程 选择web 不会的请看Spring Boot快速入门(一):Hel ...
- Spring Boot 快速入门
Spring Boot 快速入门 http://blog.csdn.net/xiaoyu411502/article/details/47864969 今天给大家介绍一下Spring Boot MVC ...
- spring boot入门教程——Spring Boot快速入门指南
Spring Boot已成为当今最流行的微服务开发框架,本文是如何使用Spring Boot快速开始Web微服务开发的指南,我们将使创建一个可运行的包含内嵌Web容器(默认使用的是Tomcat)的可运 ...
- 快速入门python看过的一些资料
我快速入门python看过的一些资料 B站的视频 10天自学Python,轻松掌握Python基础[千锋] 廖雪峰 - Python教程 https://www.liaoxuefeng.com/wik ...
- 笔记:Spring Cloud Zuul 快速入门
Spring Cloud Zuul 实现了路由规则与实例的维护问题,通过 Spring Cloud Eureka 进行整合,将自身注册为 Eureka 服务治理下的应用,同时从 Eureka 中获取了 ...
- Spring Cloud Zuul 快速入门
Spring Cloud Zuul 实现了路由规则与实例的维护问题,通过 Spring Cloud Eureka 进行整合,将自身注册为 Eureka 服务治理下的应用,同时从 Eureka 中获取了 ...
随机推荐
- $releasever 不正确解析
[nginx] gpgcheck=0 baseurl=http://nginx.org/packages/centos/$releasever/$basearch/ name=nginx repo 这 ...
- 「雕爷学编程」Arduino动手做(36)——WS2812B 4位彩灯模块
37款传感器与模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止37种的.鉴于本人手头积累了一些传感器和模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的,这里 ...
- vue与其他框架对比
https://cn.vuejs.org/v2/guide/comparison.html 1. vue 框架的特点? MVVM框架模式 轻量级,灵活,容易上手 数据驱动 组件化(单文件组件) 插件化 ...
- 剑指Offer01之二维数组中查找目标数
剑指Offer之二维数组中查找目标数 题目描述 在一个二维数组中(每个一维数组的长度相等),每一行都是从左到右递增的顺序排序,每一列都是从上到下递增的顺序排序,输入这样一个二维数组和一个整数,判断 ...
- Python 图像处理 OpenCV (3):图像属性、图像感兴趣 ROI 区域及通道处理
前文传送门: 「Python 图像处理 OpenCV (1):入门」 「Python 图像处理 OpenCV (2):像素处理与 Numpy 操作以及 Matplotlib 显示图像」 图像属性 图像 ...
- java遇到的error解决
解决Cannot change version of project facet Dynamic web module to 2.5 maven 不能设置为web3.0人解决方法 http://www ...
- 王艳 201771010127《面向对象程序设计(java)》第六周学习总结
实验六 继承定义与使用 一:理论部分: 第五章:继承类. 1.继承:已有类来构建新类的一种机制.档定义了一个新类继承另一个类时,这个新类就继承了这个类的方法和域,同时在新类中添加新的方法和域以适应新的 ...
- J2EE项目分类管理中,提交表单数据是二进制形式时,对数据的修改失败。category赋值失败。
原因: 在条件判断时,对字符串的比较进行了错误比较. 解决方法: A==B,比较的是两个字符串是否是同一个对象. A.equal(B),比较的是两个字符串内容是否相同. 出现错误是用了第一种比较,应该 ...
- 解决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 文件夹 ...
- Python中ThreadLocal的理解与使用
一.对 ThreadLocal 的理解 ThreadLocal,有的人叫它线程本地变量,也有的人叫它线程本地存储,其实意思一样. ThreadLocal 在每一个变量中都会创建一个副本,每个线程都可以 ...