19.Zuul开发者指南

有关Zuul工作原理的概述,请看Zuul Wiki

Zuul Servlet

Zuul是以Servlet方式实现的。对于一般情况,Zuul被嵌入到Spring Dispatch机制中。这使得Spring MVC可以控制路由。在这种情况下,Zuul缓冲请求。如果在经过Zuul时不需要缓冲请求(例如,上传大文件),在Spring Dispatcher之外也安装了Zuul Servlet。默认情况下,这个servlet的地址是/zuul。可以通过zuul.servlet-path属性来修改这个地址。

译者注:
Zuul Servlet是一个公共实现,默认情况下,Spring Cloud会初始化两个入口,ZuulControllerZuulServlet。对于一般请求都是通过ZuulController;对于特殊需求(如上传大文件)可通过ZuulServlet

Zuul RequestContext

Zuul使用RequestContext在过滤器之间传递信息。其数据保存在每个特定请求的ThreadLocal里。有关路由的请求地址、错误、以及真正的HttpServletRequestHttpServletResponse的信息都存储在这里。RequestContext继承自ConcurrentHashMap,所以可以将任意信息存储在上下文中。FilterConstants中包含了由Spring Cloud Netflix安装的过滤器使用的key

@EnableZuulProxy vs. @EnableZuulServer

根据启用Zuul所使用的注解方式的不同,Spring Cloud Netflix 安装了不同的若干过滤器。@EnableZuulProxy@EnableZuulServer的超集。换句话说,@EnableZuulProxy包含由@EnableZuulServer安装的所有过滤器。Proxy中附加的过滤器启用了路由功能。如果您想用空白的Zuul,请使用@EnableZuulServer。(译者注:这里的空白指的是不含路由功能)

@EnableZuulServer过滤器

@EnableZuulServer创建了一个SimpleRouteLocator,它从Spring Boot配置文件中加载路由定义。

安装了以下过滤器(就像普通的Spring Beans):

@EnableZuulProxy过滤器

创建了一个DiscoveryClientRouteLocator,它从DiscoveryClient(如Eureka)以及配置文件加载路由定义。从DiscoveryClient为每个serviceId创建一个路由。随着新服务的添加,路由将会被刷新。

除了安装了前面描述的过滤器之外(译者注:@EnableZuulServer过滤器),还安装了以下过滤器(就像普通的Spring Beans):

自定义Zuul过滤器实例

以下大部分如何写的实例都在 Sample Zuul Filters 项目中。该仓库中还有一些操纵请求体和响应体的实例。

本章节包含以下示例:

如何写Pre Filter

Pre过滤器在RequestContext中设置数据,以便下游过滤器使用。Pre过滤器主要的使用场景是为route过滤器设置所需的信息。以下示例展示了一个Zuul pre过滤器:

public class QueryParamPreFilter extends ZuulFilter {
	@Override
	public int filterOrder() {
		return PRE_DECORATION_FILTER_ORDER - 1; // run before PreDecoration
	}

	@Override
	public String filterType() {
		return PRE_TYPE;
	}

	@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();
		HttpServletRequest request = ctx.getRequest();
		if (request.getParameter("sample") != null) {
		    // put the serviceId in `RequestContext`
    		ctx.put(SERVICE_ID_KEY, request.getParameter("foo"));
    	}
        return null;
    }
}

上面的过滤器从sample请求参数填充SERVICE_ID_KEY。在实践中,您不应该进行这样的直接映射。相反,应该通过sample的值来查找service ID。

由于填充了SERVICE_ID_KEY,那么就不执行PreDecorationFilter了,而会执行RibbonRoutingFilter

如果您想路由到完整的URL,请使用ctx.setRouteHost(url)

要修改路由过滤器转发的路径,请设置REQUEST_URI_KEY

如何写Route Filter

Route过滤器运行在Pre过滤器之后,并向其它服务发送请求。这里的大部分工作是将请求和响应数据与客户端所需的模型相互转换。下面的例子展示了一个Route过滤器:

public class OkHttpRoutingFilter extends ZuulFilter {
	@Autowired
	private ProxyRequestHelper helper;

	@Override
	public String filterType() {
		return ROUTE_TYPE;
	}

	@Override
	public int filterOrder() {
		return SIMPLE_HOST_ROUTING_FILTER_ORDER - 1;
	}

	@Override
	public boolean shouldFilter() {
		return RequestContext.getCurrentContext().getRouteHost() != null
				&& RequestContext.getCurrentContext().sendZuulResponse();
	}

    @Override
    public Object run() {
		OkHttpClient httpClient = new OkHttpClient.Builder()
				// customize
				.build();

		RequestContext context = RequestContext.getCurrentContext();
		HttpServletRequest request = context.getRequest();

		String method = request.getMethod();

		String uri = this.helper.buildZuulRequestURI(request);

		Headers.Builder headers = new Headers.Builder();
		Enumeration<String> headerNames = request.getHeaderNames();
		while (headerNames.hasMoreElements()) {
			String name = headerNames.nextElement();
			Enumeration<String> values = request.getHeaders(name);

			while (values.hasMoreElements()) {
				String value = values.nextElement();
				headers.add(name, value);
			}
		}

		InputStream inputStream = request.getInputStream();

		RequestBody requestBody = null;
		if (inputStream != null && HttpMethod.permitsRequestBody(method)) {
			MediaType mediaType = null;
			if (headers.get("Content-Type") != null) {
				mediaType = MediaType.parse(headers.get("Content-Type"));
			}
			requestBody = RequestBody.create(mediaType, StreamUtils.copyToByteArray(inputStream));
		}

		Request.Builder builder = new Request.Builder()
				.headers(headers.build())
				.url(uri)
				.method(method, requestBody);

		Response response = httpClient.newCall(builder.build()).execute();

		LinkedMultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>();

		for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) {
			responseHeaders.put(entry.getKey(), entry.getValue());
		}

		this.helper.setResponse(response.code(), response.body().byteStream(),
				responseHeaders);
		context.setRouteHost(null); // prevent SimpleHostRoutingFilter from running
		return null;
    }
}

上面的过滤器,将Servlet请求信息转换为OkHttp3请求信息,然后执行HTTP请求,并将OkHttp3响应信息转换到Servlet响应。

如何写Post Filter

Post过滤器通常负责操作响应。下面的过滤器添加了一个随机UUID作为X-Sample头信息:

public class AddResponseHeaderFilter extends ZuulFilter {
	@Override
	public String filterType() {
		return POST_TYPE;
	}

	@Override
	public int filterOrder() {
		return SEND_RESPONSE_FILTER_ORDER - 1;
	}

	@Override
	public boolean shouldFilter() {
		return true;
	}

	@Override
	public Object run() {
		RequestContext context = RequestContext.getCurrentContext();
    	HttpServletResponse servletResponse = context.getResponse();
		servletResponse.addHeader("X-Sample", UUID.randomUUID().toString());
		return null;
	}
}

其他处理,如转换响应体,要复杂的多,并需要大量的计算。

Zuul Errors工作原理

如果在Zuul过滤器生命周期中的任何地方抛出异常,都会执行error过滤器。只有RequestContext.getThrowable()非空时才执行SendErrorFilter。然后它在请求中设置特定的javax.servlet.error.*属性,并将请求转发到Spring Boot错误页。

Zuul Eager Application Context Loading

Zuul内部使用Ribbon调用远程URLs。默认情况下,Ribbon客户端在第一次调用时由Spring Cloud延迟加载。可以通过以下配置来修改Zuul的此行为,从而在应用启动时立即加载子Ribbon相关的应用上下文。下面的例子展示了如何启用即时加载:

application.yml.

zuul:
  ribbon:
    eager-load:
      enabled: true