Appearance
Zuul 简介
Zuul 是 Netflix 开源的微服务网关,它可以和 Eureka、Ribbon、Hystrix 等组件配合使用,Zuul 组件的核心是一系列的过滤器,这些过滤器可以完成以下功能:
- 动态路由:动态将请求路由到不同后端集群
- 压力测试:逐渐增加指向集群的流量,以了解性能
- 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求
- 静态响应处理:边缘位置进行响应,避免转发到内部集群
- 身份认证和安全:识别每一个资源的验证要求,并拒绝那些不符的请求。Spring Cloud 对 Zuul 进行了整合和增强。
Zuul 网关服务器搭建准备
创建 Zuul 组件的示例工程 spring-cloud-sample-zuul,复用之前 spring-cloud-sample-feign 工程的代码,删除一些无用的内容
创建zuul服务引入依赖
创建 zuu l网关工程 shop-server-zuul,并添加 zuul 组件的依赖
xml
<!-- spring cloud netflix zuul 组件核心依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>开启zuul网关功能
创建启动类 ZuulServerApplication,在启动类上标识@EnableZuulProxy注解开启zuul网关功能
java
@SpringBootApplication
@EnableZuulProxy // 开启zuul网关功能
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}项目配置
创建工程的配置文件application.yml,并添加基础配置
yml
server:
port: 8080 # 项目端口
spring:
application:
name: shop-server-zuul # 服务名称Zuul 的路由转发
路由转发是指根据请求 URL,将请求分配到对应的处理程序。在微服务体系中,Zuul 负责接收所有的请求。根据不同的 URL 匹配规则,将不同的请求转发到不同的微服务处理。
基础配置
修改工程的application.yml配置文件,配置路由规则
yml
# zuul 路由配置
zuul:
routes:
# 商品微服务
shop-service-product: # 路由名称,名称任意,保持所有路由名称唯一即可
path: /product-service/** # 配置映射路径,当前示例即:localhost:8080/product-service/xxxx/xx
url: http://127.0.0.1:9001 # 配置请求映射路径时转发到对应的实际微服务url地址,可以是实际ip或者域名
sensitiveHeaders: # 默认zuul会屏蔽cookie,cookie不会传到下游服务,这里设置为空则取消默认的黑名单,如果设置了具体的头信息则不会传到下游服务配置项说明:
shop-service-product:配置路由id,可以随意取名,只要保持配置中所有路由id唯一即可path:配置映射路径,上面示例代表所有前缀为/product-service的请求,都会转发到http://127.0.0.1:9001url:映射路径对应的实际url地址
Zuul 路由配置后,启动服务,在浏览器中输入http://localhost:8080/product-service/product/1,即可访问到商品微服务
通过注册中心方式的路由配置
对于一个 URL 请求,最终会确认一个服务实例进行处理。在实际项目众多微服务情况下,如果对每个服务实例手动指定一个唯一访问地址,然后根据 URL 去手动实现请求匹配,显然不可能的。
Zuul 支持与 Eureka 整合开发,根据ServiceID自动的从注册中心中获取服务地址并转发请求,此配置方式不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例的时候不用修改 Zuul 的路由配置
- 修改网关服务,添加注册中心 Eureka 客户端的依赖
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>- 开启Eureka客户端发现功能。在启动类中标识
@EnableDiscoveryClient。注:从Spring Cloud Edgware版本开始,开启服务发现的注解可以省略不写。只需加上相关依赖与相应配置,即可注册服务 - 在
application.yml中添加 Eureka 配置,获取服务信息
yml
# Eureka 客户端配置
eureka:
instance:
prefer-ip-address: true # 将当前服务的ip地址注册到Eureka服务中
instance-id: ${spring.cloud.client.ip-address}:${server.port} # 指定实例id
client:
service-url:
defaultZone: http://localhost:8001/eureka/ # Eureka server 地址,多个eureka server之间用,隔开
registry-fetch-interval-seconds: 5 # 配置获取服务列表的周期为5s,默认值是30s- 修改zuul的映射配置,通过服务名称获取。引入Eureka客户端后,可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能
yml
zuul:
routes:
# 商品微服务
shop-service-product: # 路由名称,名称任意,保持所有路由名称唯一即可
path: /product-service/** # 配置映射路径,当前示例即:localhost:8080/product-service/xxxx/xx
serviceId: shop-service-product # 配置请求转发相应的服务名称,网关会从eureka中获取该服务名称下的服务实例的地址
sensitiveHeaders: # 默认zuul会屏蔽cookie,cookie不会传到下游服务,这里设置为空则取消默认的黑名单,如果设置了具体的头信息则不会传到下游服务配置项说明:
serviceId:指定请求需要转发的微服务实例名称(服务注册到eureka的名称)
依次启动Eureka,商品微服务,API网关,在浏览器上通过访问 http://localhost:8080/product-service/product/1 查看最终效果。
路由配置的简化方式
传统面向服务的路由配置规则是:
zuul.routes.<route>.path=/xxx/**:来指定映射路径。<route>是自定义的路由名zuul.routes.<route>.serviceId=/product-service:来指定服务名
有一种特殊的情况:如果当<route>路由名称和服务名定义一样的时候,Zuul提供了一种简化的配置语法:zuul.routes.<serviceId>=<path>。上面示例的配置简化后如下:
yml
zuul:
routes:
# 当路由的名称和服务名称一样的时候,zuul路由配置可简化成 zuul.routes.<serviceId>=<path>
shop-service-product: /product-service/**默认的路由规则
Zuul不单单提供的路由的简化配置,zuul还有默认的路由规则。默认的路由规则是:一切服务的映射路径就是服务的名称
例如:服务的名称为shop-service-order,则默认的映射路径就是shop-service-order/**。所以示例工程不需要配置order服务,zuul也可以成功转发请求


通过注册中心方式配置zuul路由的架构图

Zuul 的过滤器
Zuul 组件包含了两个核心功能:对请求的路由和过滤。
- 路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础
- 过滤器功能负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础
其实路由功能在真正运行时,它的路由映射和请求转发同样也由几个不同的过滤器完成的。所以过滤器可以说是 Zuul 实现 API 网关功能最为核心的部件,每一个进入Zuul的HTTP请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端。
ZuulFilter 简介
Zuul 中的过滤器跟 Servlet 使用的javax.servlet.Filter不一样,javax.servlet.Filter只有一种类型,可以通过配置 urlPatterns 来拦截对应的请求。而 Zuul 中的过滤器总共有4种类型,且每种类型都有对应的使用场景。
PRE:此过滤器在请求被路由之前调用。可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。ROUTING:此过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用 Apache HttpClient 或 Netfilx Ribbon 请求微服务。POST:此过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTPHeader、收集统计信息和指标、将响应从微服务发送给客户端等。ERROR:在其他阶段发生错误时执行该过滤器。
过滤器的生命周期

正常流程:
- 请求到达首先会经过 pre 类型过滤器,而后到达 routing 类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达 post 过滤器。而后返回响应
异常流程:
- 整个过程中,pre 或者 routing 过滤器出现异常,都会直接进入 error 过滤器,再 error 处理完毕后,会将请求交给 POST 过滤器,最后返回给用户。
- 如果是 error 过滤器自己出现异常,最终也会进入 POST 过滤器,而后返回。
- 如果是 POST 过滤器出现异常,会跳转到 error 过滤器,但是与 pre 和 routing 不同的时,请求不会再到达 POST 过滤器了
不同过滤器的场景:
- 请求鉴权:一般放在 pre 类型,如果发现没有访问权限,直接就拦截了
- 异常处理:一般会在 error 类型和 post 类型过滤器中结合来处理
- 服务调用时长统计:pre 和 post 结合使用。

zuul 内置过滤器

自定义 zuul 过滤器
基础用法
Zuul 提供了自定义过滤器的功能,只需要编写一个类去实现 zuul 提供的 ZuulFilter 接口即可。
Zuul 也提供了过滤器的顶级抽象类 ZuulFilter,一般自定义 zuul 过滤器都继承该抽象父类,其中定义的4个最重要的方法如下:
java
public abstract class ZuulFilter implements IZuulFilter, Comparable<ZuulFilter> {
boolean shouldFilter(); // 继承于IZuulFilter接口
Object run() throws ZuulException; // 继承于IZuulFilter接口
abstract public String filterType();
abstract public int filterOrder();
}shouldFilter():返回一个 Boolean 值,判断该过滤器是否需要执行。返回true执行,返回false 不执行run():过滤器的具体业务逻辑filterType():返回字符串,代表过滤器的类型。包含以下4种:pre:请求在被路由之前执行routing:在路由请求时调用post:在routing和errror过滤器之后调用error:处理请求时发生错误调用
filterOrder():通过返回int类型的值来定义过滤器的执行顺序,数字越小优先级越高
自定义过滤器示例
示例需求:自定义一个过滤器,模拟一个登录的校验。基本逻辑:如果请求头中有access-token参数,则认为请求有效,放行。
在shop-server-zuul工程中创建LoginFilter类,实现ZuulFilter接口,编写请求被路由前的业务逻辑
java
package com.moon.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* zuul自定义过滤器示例 - 请求登陆校验过滤
*/
@Component // 自定义zuul过滤器,需要使用@Component标识该bean给spring容器管理
// 自定义zuul过滤器需要继承抽象父类ZuulFilter
public class LoginFilter extends ZuulFilter {
/**
* 设置过滤器类型
* pre:请求在被路由之前执行
* routing:在路由请求时调用
* post:在routing和errror过滤器之后调用
* error:处理请求时发生错误调用
*
* @return 过滤器的类型字符串
*/
@Override
public String filterType() {
// 登录校验,设置为请求路由之前拦截
return "pre";
}
/**
* 指定过滤器的执行顺序,返回值越小越被优先执行
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 过滤器是否需要执行
*
* @return true: 执行此过滤器,false: 不执行
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 过滤器的业务逻辑方法
* <p>
* 此示例完成身份认证:
* 1.所有的请求需要携带一个参数: access-token
* 2.获取request请求头"access-token"
* 3.判断token是否为空
* 4.1 token==null : 身份验证失败
* 4.2 token!=null : 执行后续操作
*
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
// 在zuul网关中,通过RequestContext的上下文对象,可以获取HttpServletRequest和HttpServletResponse对象
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
HttpServletResponse response = context.getResponse();
// 获取请求头中access-token字段
String token = request.getHeader("access-token");
if (StringUtils.isEmpty(token)) {
// 如果请求头不包含Authorization,则拒绝访问
context.setSendZuulResponse(false);
// 设置响应状态码
context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
// 设置JSON格式的响应信息
context.setResponseBody("{\"code\": \"-1\", \"retMsg\": \"此操作需要登陆系统!\"}");
// 设置contentType为json类型
response.setContentType("application/json;charset=UTF-8");
// 还可以进行其他的操作
}
// 校验通过,返回继续向后执行。(也可以考虑把用户信息放入上下文)
return null;
}
}上面示例的RequestContext是zuul用于在过滤器之间传递消息。它的数据保存在每个请求的ThreadLocal中。它用于存储请求路由到哪里、错误、HttpServletRequest、HttpServletResponse都存储在RequestContext中。RequestContext扩展了ConcurrentHashMap,所以,任何数据都可以存储在上下文中
启动服务进行测试:

服务网关Zuul的核心源码解析
Zuul 网关处理请求的流程

在 Zuul 中,整个请求的过程是:
- 首先将请求给
ZuulServlet处理,ZuulServlet中有一个ZuulRunner对象,该对象中初始化了RequestContext,作为存储整个请求的一些数据,并被所有的ZuulFilter共享。 ZuulRunner中还有FilterProcessor,FilterProcessor作为执行所有的ZuulFilter的管理器。FilterProcessor从FilterLoader中获取ZuulFilter,而ZuulFilter是被FilterFileManager所加载,并支持 groovy 热加载,采用了轮询的方式热加载。- 有了这些 filter 之后,
ZuulServlet首先执行的Pre类型的过滤器,再执行route类型的过滤器,最后执行的是post类型的过滤器,如果在执行这些过滤器有错误的时候则会执行error类型的过滤器。 - 执行完这些过滤器,最终将请求的结果返回给客户端
zuul 的初始化
Spring Cloud 对 Zuul 的封装使得发布一个ZuulServer,根据自动装载原则可以在 spring-cloud-netflix-zuul-2.1.0.RELEASE.jar 下找到 spring.factories 文件

项目启动时会加载ZuulServerAutoConfiguration和ZuulProxyAutoConfiguration类,这两个类是Zuul服务端的自动配置类。其中ZuulProxyAutoConfiguration继承了ZuulServerAutoConfiguration
java
@Configuration
@Import({ RibbonCommandFactoryConfiguration.RestClientRibbonConfiguration.class,
RibbonCommandFactoryConfiguration.OkHttpRibbonConfiguration.class,
RibbonCommandFactoryConfiguration.HttpClientRibbonConfiguration.class,
HttpClientConfiguration.class })
@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class)
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {
....
}查看ZuulServerAutoConfiguration源码,里面创建了几个zuul服务的核心类
java
@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass({ZuulServlet.class, ZuulServletFilter.class})
@ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)
public class ZuulServerAutoConfiguration {
....
@Bean
@Primary
public CompositeRouteLocator primaryRouteLocator(
Collection<RouteLocator> routeLocators) {
return new CompositeRouteLocator(routeLocators);
}
@Bean
@ConditionalOnMissingBean(SimpleRouteLocator.class)
public SimpleRouteLocator simpleRouteLocator() {
return new SimpleRouteLocator(this.server.getServlet().getContextPath(),
this.zuulProperties);
}
@Bean
public ZuulController zuulController() {
return new ZuulController();
}
....
@Configuration
protected static class ZuulFilterConfiguration {
@Autowired
private Map<String, ZuulFilter> filters;
@Bean
public ZuulFilterInitializer zuulFilterInitializer(
CounterFactory counterFactory, TracerFactory tracerFactory) {
FilterLoader filterLoader = FilterLoader.getInstance();
FilterRegistry filterRegistry = FilterRegistry.instance();
return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry);
}
}
....
}CompositeRouteLocator:组合路由定位器,看入参就知道应该是会保存好多个RouteLocator,构造过程中其实仅包括一个DiscoveryClientRouteLocator实例SimpleRouteLocator:默认的路由定位器,主要负责维护配置文件中的路由配置。ZuulController:Zuul 创建的一个 Controller,用于将请求交由ZuulServlet处理。ZuulHandlerMapping:此类会添加到SpringMVC的HandlerMapping链中,只有选择了ZuulHandlerMapping的请求才能出发到Zuul的后续流程。ZuulFilterInitializer:在内部类ZuulFilterConfiguration中,注册ZuulFilterInitializer实例,通过FilterLoader加载应用中所有的过滤器并将过滤器注册到FilterRegistry
以下就是ZuulFilterInitializer是如何将过滤器加载到应用中的源码
java
public class ZuulFilterInitializer {
private static final Log log = LogFactory.getLog(ZuulFilterInitializer.class);
private final Map<String, ZuulFilter> filters;
private final CounterFactory counterFactory;
private final TracerFactory tracerFactory;
private final FilterLoader filterLoader;
private final FilterRegistry filterRegistry;
public ZuulFilterInitializer(Map<String, ZuulFilter> filters,
CounterFactory counterFactory,
TracerFactory tracerFactory,
FilterLoader filterLoader,
FilterRegistry filterRegistry) {
this.filters = filters;
this.counterFactory = counterFactory;
this.tracerFactory = tracerFactory;
this.filterLoader = filterLoader;
this.filterRegistry = filterRegistry;
}
@PostConstruct
public void contextInitialized() {
log.info("Starting filter initializer");
TracerFactory.initialize(tracerFactory);
CounterFactory.initialize(counterFactory);
for (Map.Entry<String, ZuulFilter> entry : this.filters.entrySet()) {
filterRegistry.put(entry.getKey(), entry.getValue());
}
}
@PreDestroy
public void contextDestroyed() {
log.info("Stopping filter initializer");
for (Map.Entry<String, ZuulFilter> entry : this.filters.entrySet()) {
filterRegistry.remove(entry.getKey());
}
clearLoaderCache();
TracerFactory.initialize(null);
CounterFactory.initialize(null);
}
private void clearLoaderCache() {
Field field = ReflectionUtils.findField(FilterLoader.class, "hashFiltersByType");
ReflectionUtils.makeAccessible(field);
@SuppressWarnings("rawtypes")
Map cache = (Map) ReflectionUtils.getField(field, filterLoader);
cache.clear();
}
}zuul 处理请求的转发
在 Zuul 的ZuulServerAutoConfiguration自动配置类中,创建了ZuulHandlerMapping实例,此类为 Spring MVC 中HandlerMapping的拓展实现,会自动的添加到HandlerMapping链中。
java
public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {
private final RouteLocator routeLocator;
private final ZuulController zuul;
private ErrorController errorController;
private PathMatcher pathMatcher = new AntPathMatcher();
private volatile boolean dirty = true;
public ZuulHandlerMapping(RouteLocator routeLocator, ZuulController zuul) {
this.routeLocator = routeLocator;
this.zuul = zuul;
setOrder(-200);
}
// ....省略
private void registerHandlers() {
Collection<Route> routes = this.routeLocator.getRoutes();
if (routes.isEmpty()) {
this.logger.warn("No routes found from RouteLocator");
}
else {
for (Route route : routes) {
registerHandler(route.getFullPath(), this.zuul);
}
}
}
}ZuulHandlerMapping其主要目的就是把所有请求路径导入到ZuulController中。另外的功能是当觉察RouteLocator路由表变更,则更新自己dirty状态,重新注册所有Route到ZuulController
java
public class ZuulController extends ServletWrappingController {
public ZuulController() {
// 在这里已经设置了ZuulServlet
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
// 在这里面会调用ZuulServlet的service方法
return super.handleRequestInternal(request, response);
}
finally {
RequestContext.getCurrentContext().unset();
}
}
}在 ZuulController 中的 handleRequest 方法,会调用已经注册的 ZuulServlet 完成业务请求
java
public class ZuulServlet extends HttpServlet {
// ....省略
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
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
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 内置的过滤器
Zuul 默认注入的过滤器可以在 spring-cloud-netflix-core.jar 中找到

Zuul网关的替换方案
Zuul网关存在的问题
- 性能问题
Zuul 1.x 版本本质上就是一个同步 Servlet,采用多线程阻塞模型进行请求转发。即每来一个请求,Servlet 容器要为该请求分配一个线程专门负责处理这个请求,直到响应返回客户端这个线程才会被释放返回容器线程池。如果后台服务调用比较耗时,那么这个线程就会被阻塞,阻塞期间线程资源被占用,不能干其它事情。而 Servlet 容器线程池的大小是有限制的,当前端请求量大,而后台慢服务比较多时,很容易耗尽容器线程池内的线程,造成容器无法接受新的请求
- 不支持任何长连接,如
WebSocket
替换方案 - Zuul2.x版本
Zuul2.x 版本是对 zuul 的升级版,但并没有被 Spring Cloud 收录
替换方案 - Spring Cloud Gateway
Spring Cloud Gateway 是 Spring Cloud 单独推出的微服务网关组件,比 Zuul 1.x 系列的性能和功能整体要好
Spring Cloud Gateway 详细的说明与使用详见《Spring Cloud Gateway 服务网关》笔记