如果所有的微服务系统都对外提供服务,那么这些微服务系统都需要实现诸如数据验证、安全校验、接口权限等功能,这对于系统的维护非常不利,这也就是api网关存在的原因。在SpringCloud的大家庭中,使用的是zuul组件来搭建api网关。zuul是一个能够实现动态路由、监控、弹性扩展并且安全的API网关组件。
在之前的一篇博客《API网关系统架构》中,有讨论过一个成熟的网关系统应该具备注入:统一接入、安全防护、流量管控、协议转换、安全、高可用、高并发、方便扩展、方便运维等能力,所以这里不过多讨论api网关应该如何设计,本文重点从源码的角度分析Zuul的实现。
一 请求路由
路由的功能其实比较简单,代码层面只需要能够实现将用户的请求路径和服务提供者相对应即可,所以这一部分没有附带源码,仅简单的列举了其主要的路由规则。
1 传统路由方式
配置规则
zuul.routes.<路由名称>.path=<路径映射>
zuul.routes.<路由名称>.url=<服务提供者地址>
示例如下:
zuul.routes.api-a-url.path=/test/**
zuul.routes.api-a-url.url=http://localhost:8080/test
此规则表示所有/test/**请求都会使用 http://localhost:8080/test对应的微服务上。
2 面向服务的路由方式
配置规则:
zuul.routes.<路由名称>.path=<路径映射>
zuul.routes.<路由名称>.serviceId=<服务名>
示例如下:
zuul.routes.api-a.path=/testa/**
zuul.routes.api-a.serviceId=testa-service
zuul.routes.api-b.path=/testb/**
zuul.routes.api-b.serviceid=testb- service
此规则表示所有/testa/**请求都会使用testa-service的微服务,所有/testb/**请求都会使用testb-service的微服务,当然这需要配合Eureka来使用。
二 过滤器
如果想让底层的业务系统收到的请求都是经过安全验证的,返回结果具有统一格式等,在zuul中可以通过过滤器实现,
1 请求生命周期
1) zuul 1.0结构
https://github.com/Netflix/zuul/wiki/How-it-Works,如下所示:
2) zuul 2.0结构
https://github.com/Netflix/zuul/wiki/How-It-Works-2.0,如下所示:
Inbound Filters是前置过滤器,Outbound Fileter是后置过滤器
2 过滤器
可以继承ZuulFilter创建过滤器,示例如下:
public class TestFilter extends ZuulFilter {
@Override
public String filterType() {
//pre、post、error
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
// 需要使用此过滤器拦截,则需要返回true
return true;
}
@Override
public Object run() {
// 过滤的逻辑
}
}
filterType: 过滤器的类型,取值包括:
pre:可以在请求被路由之前调用。
routing:在路由请求时被调用。
post: 在routing和error过滤器之后被调用。
error: 处理请求时发生错误时被调用。
filterOrder:过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时, 需要根据该方法返回的值来依次执行,数字越小越先执行。
shouldFilter:判断该过滤器是否需要被执行,可以利用该函数来指定过滤器的有效范围。
run:过滤器的具体逻辑
3 Zuul提供的过滤器
1) 处理过程
默认情况下,zuul提供的filter会按照以下顺序执行。
2) Pre过滤器
a) ServletDetectionFilter
用来检测当前请求是通过Spring的DispatcherServlet处理运行的,还是通过Zuu1Servlet来处理运行的。可以通过一下代码获取检测结果:
RequestContext ctx = RequestContext.getCurrentContext();
boolean isDetectionRequest = ctx.get(RequestUtils.IS_DISPATCHERSERVLETREQUEST);
一般情况下,发送到API网关的外部请求都会被Spring的DispatcherServlet处理,除了通过/zuul/路径访问的请求外,因为此种请求会被Zuu1Servlet处理,主要用来应对处理大文件上传的情况。可以通过zuul.servletPath参数来进行修改。
b) Servlet30WrapperFilter
主要为了将HttpServletRequest包装成Servlet30RequestWrapper对象。
c) FormBodyWrapperFilter
该过滤器仅对contentType为application/x-www-form-urlencoded或者multipart/form-data的请求有效。作用是将HttpServletRequest包装成FormBodyRequestWrapper对象。
d) DebugFilter
根据配置参数zuul.debug.requet和请求中的debug参数来决定是否执行过滤器中的操作,其代码如下:
@Override
public boolean shouldFilter() {
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
if ("true".equals(request.getParameter(DEBUG_PARAMETER.get()))) {
return true;
}
return ROUTING_DEBUG.get();
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.setDebugRouting(true);
ctx.setDebugRequest(true);
return null;
}
e) PreDecorationFilter
该过滤器会判断当前请求上下文中是否存在forward.to和serviceId参数 ,如 果都不存在,那么会执行过滤器的操作。
此过滤器的内容是为当前请求做一些预处理,比如,进行路由规则的匹配、 在请求上下文中设置该请求的基本信息以及将路由匹配结果等一些设置信息等,这些信息将是后续过滤器进行处理的重要依据,我们可以通过RequestContext.getCurrentContext ()来 访问这些信息。
3) Routing过滤器
a) RibbonRoutingFilter
该过滤器只对请求上下文中存在serviceid参数的请求进行处理,即只对通 过service工d配置路由规则的请求生效。而该过滤器的执行逻辑就是面向服务路 由的核心,它通过使用Ribbon和Hystrix来向服务实例发起请求,并将服务实例的请求结果返回。
b) SimpleHostRoutingFilter
该过滤器只对通过url配置路由规则的请求生效。而该过滤器的执行逻辑就是直接向routeHost参数的物理地址发起请求。
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 (request.getContentLength() < 0) {
context.setChunkedRequestBody();
}
String uri = this.helper.buildZuulRequestURI(request);
this.helper.addIgnoredHeaders();
try {
HttpResponse response = forward(this.httpClient, verb, uri, request, headers,
params, requestEntity);
setResponse(response);
}
catch (Exception ex) {
context.set(ERROR_STATUS_CODE,
HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
context.set("error.exception", ex);
}
return null;
}
c) SendForwardFilter
仅仅对forward请求有效,该过滤器只对请求上下文中存在forward.to参数的请求进行处理。
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return ctx.containsKey("forward.to")
&& !ctx.getBoolean(SEND_FORWARD_FILTER_RAN, false);
}
@Override
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
String path = (String) ctx.get("forward.to");
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;
}
4) post过滤器
a) SendErrorFilter
该过滤器仅在请求上下文中 包含error.status_code参数并且还没有被该过滤器处理过的时候执行。该过滤器逻辑就是forward到API网关/error来影响错误信息。
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
// only forward to errorPath if it hasn't been forwarded to already
return ctx.containsKey("error.status_code")
&& !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}
@Override
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
int statusCode = (Integer) ctx.get("error.status_code");
request.setAttribute("javax.servlet.error.status_code", statusCode);
if (ctx.containsKey("error.exception")) {
Object e = ctx.get("error.exception");
log.warn("Error during filtering", Throwable.class.cast(e));
request.setAttribute("javax.servlet.error.exception", e);
}
if (ctx.containsKey("error.message")) {
String message = (String) ctx.get("error.message");
request.setAttribute("javax.servlet.error.message", message);
}
RequestDispatcher dispatcher = request.getRequestDispatcher(
this.errorPath);
if (dispatcher != null) {
ctx.set(SEND_ERROR_FILTER_RAN, true);
if (!ctx.getResponse().isCommitted()) {
dispatcher.forward(request, ctx.getResponse());
}
}
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
b) SendResponseFilter
该过滤器会检查请求上下文中是否包含请求响应相关的头信息、响应数据流或是响应体,只要包含其中一个的时候执行处理逻辑。
该过滤器的处理逻辑是利用请求上下文的响应信息来组织需要发送回客户端的响应内容。
@Override
public boolean shouldFilter() {
return !RequestContext.getCurrentContext().getZuulResponseHeaders().isEmpty()
|| RequestContext.getCurrentContext().getResponseDataStream() != null
|| RequestContext.getCurrentContext().getResponseBody() != null;
}
@Override
public Object run() {
try {
addResponseHeaders();
writeResponse();
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
4 禁用过滤器
默认情况下,过滤器都是启用状态的。那么有哪些方式来禁用过滤器呢?
1) shouldFilter返回false
@Override
public boolean shouldFilter() {
return false;
}
这种方式对于自定义过滤器来说,可以实现禁用的功能,但是不太方便,因为每次修改都要重新修改代码,重新打包。
2) 配置项
Zuul中特别提供了配置项来禁用过滤器,该配置格式如下:
zuul.<SimpleClassName>.<filterType>.disable=true
<SimpleClassName>代表过滤器的类名,< filterType >代表过滤器类型。如果自定义了一个前置过滤器类AccessFilter,当希望禁用此过滤器类时,可以这样配置:
zuul.AccessFilter.pre.disable=true
三 参考文档
1 博客
2 书籍
《Spring Cloud微服务实战》
代码:https://github.com/dyc87112/SpringCloudBook.git