这篇文章我们重点介绍下微服务中的一个重要角色:网关,对于网关如何选择,由于阿里系暂时未出网关,当然是选择了Spring cloud Gateway,当然网关还有zuul,但是性能方面肯定远远不如gateway。
传统的单体架构中只有一个服务开放给客户端调用,但是微服务架构中是将一个系统拆分成多个微服务,那么作为客户端如何去调用这些微服务呢?如果没有网关的存在,只能在本地记录每个微服务的调用地址。

无网关的微服务架构往往存在以下问题:
spring-cloud-Gateway是spring-cloud的一个子项目。而zuul则是netflix公司的项目,只是spring将zuul集成在spring-cloud中使用而已。
因为zuul2.0连续跳票和zuul的性能表现不是很理想,所以催生了spring团队开发了Gateway项目。
Zuul使用的是阻塞式的 API,不支持长连接,比如 websockets。底层是DNservletS缓存,Zuul处理的是http请求。没有提供异步支持,流控等均由hystrix支持。
依赖包spring-cloud-starter-netflix-zuul。
Gateway:
Spring Boot和Spring Webflux提供的Netty底层环境,不能和传统的Servlet容器一起使用,也不能打包成一个WAR包。
依赖spring-boot-starter-webflux和spring-cloud-starter-gateway提供了异步支持,提供了抽象负载均衡,提供了抽象流控,并默认实现了RedisRateLimiter。
我们大致可以分为以下四个思路去讨论。
相同点:
不同点:
gateway具有更好的扩展性,并且其已经发布了2.0.0的RELESE版本,稳定性也是非常好的
WebFlux 模块的名称是 spring-webflux,名称中的 Flux 来源于 Reactor中的类 Flux。
Spring webflux 有一个全新的非堵塞的函数式 Reactive Web 框架,可以用来构建异步的、非堵塞的、事件驱动的服务,在伸缩性方面表现非常好。使用非阻塞API。
Websockets得到支持,并且由于它与Spring紧密集成,所以将会是一个更好的开发体验。
Zuul 1.x,是一个基于阻塞io的API Gateway。Zuul已经发布了Zuul 2.x,基于Netty也是非阻塞的,支持长连接,但Spring Cloud暂时还没有整合计划。
总的来说,在微服务架构,如果使用了Spring Cloud生态的基础组件,则Spring Cloud Gateway相比而言更加具备优势,单从流式编程+支持异步上就足以让开发者选择它了。
对于小型微服务架构或是复杂架构(不仅包括微服务应用还有其他非Spring Cloud服务节点),zuul也是一个不错的选择。
网关是所有微服务的门户,路由转发仅仅是最基本的功能,除此之外还有其他的一些功能,比如:认证、鉴权、熔断、限流、日志监控等等…

路由(route):gateway的基本构建模块。它由ID、目标URI、断言集合和过滤器集合组成。如果聚合断言结果为真,则匹配到该路由。
断言(Predicate ):参照Java8的新特性Predicate,允许开发人员匹配HTTP请求中的任何内容,比如头或参数。
过滤器(filter):可以在返回请求之前或之后修改请求和响应的内容。

请大家务必严格的按照上面对应的版本,要不然会有你意想不到的bug
Predicate来自于java8的接口。Predicate接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非)。
可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。
Spring Cloud Gateway内置了许多Predict,这些Predict的源码在org.springframework.cloud.gateway.handler.predicate包中,有兴趣可以阅读一下。内置的一些断言如下图:

下面就以最后一种权重断言为例介绍一下如何配置。配置如下:
spring:
cloud:
gateway:
## 路由
routes:
## id只要唯一即可,名称任意
- id: gateway-provider_1
uri: http://localhost:9024
## 配置断言
predicates:
## Path Route Predicate Factory断言,满足/gateway/provider/**这个请求路径的都会被路由到http://localhost:9024这个uri中
- Path=/gateway/provider/**
## Weight Route Predicate Factory,同一分组按照权重进行分配流量,这里分配了80%
## 第一个group1是分组名,第二个参数是权重
- Weight=group1, 8
## id必须唯一
- id: gateway-provider_2
## 路由转发的uri
uri: http://localhost:9025
## 配置断言
predicates:
## Path Route Predicate Factory断言,满足/gateway/provider/**这个请求路径的都会被路由到http://localhost:9024这个uri中
- Path=/gateway/provider/**
## Weight Route Predicate Factory,同一分组按照权重进行分配流量,这里分配了20%
## 第一个group1是分组名,第二个参数是权重
- Weight=group1, 2
routes下就是配置的路由策略,各个组件如下:
Spring Cloud Gateway中的断言命名都是有规范的,格式:xxxRoutePredicateFactory。
比如权重的断言:WeightRoutePredicateFactory,那么配置时直接取前面的Weight。
默认的路由转发如果路由到了两个,则是的按照配置先后顺序转发,上面都配置了路径:Path=/gateway/provider/**,如果没有配置权重,则肯定是转发到http://localhost:9024。
但是既然配置配置了权重并且相同的分组,则按照权重比例进行分配流量。
过滤器这个概念很熟悉,在Spring mvc 就接触过,Gateway的过滤器的作用以及生命周期都是类似的。
PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择 请求的微服务、记录调试信息等。
POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
Gateway 的Filter从作用范围可分为两种:
Spring Cloud Gateway中内置了许多的局部过滤器,局部过滤器需要在指定路由配置才能生效,默认是不生效的。
以AddResponseHeaderGatewayFilterFactory这个过滤器为例,为原始响应添加Header,配置如下:
spring:
cloud:
gateway:
## 路由
routes:
## id只要唯一即可,名称任意
- id: gateway-provider_1
uri: http://localhost:9024
## 配置断言
predicates:
## Path Route Predicate Factory断言,满足/gateway/provider/**这个请求路径的都会被路由到http://localhost:9024这个uri中
- Path=/gateway/provider/**
## 配置过滤器(局部)
filters:
- AddResponseHeader=X-Response-Foo, Bar
注意:过滤器的名称只需要写前缀,过滤器命名必须是xxxGatewayFilterFactory(包括自定义)。
虽说内置的过滤器能够解决很多场景,但是难免还是有些特殊需求需要定制一个过滤器,下面就来介绍一下如何自定义局部过滤器。
场景:模拟一个授权验证的过程,如果请求头或者请求参数中携带token则放行,否则直接拦截返回401,代码如下:
/**
* 名称必须是xxxGatewayFilterFactory形式
* todo:模拟授权的验证,具体逻辑根据业务完善
*/
@Component
@Slf4j
public class AuthorizeGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthorizeGatewayFilterFactory.Config> {
private static final String AUTHORIZE_TOKEN = "token";
//构造函数,加载Config
public AuthorizeGatewayFilterFactory() {
//固定写法
super(AuthorizeGatewayFilterFactory.Config.class);
log.info("Loaded GatewayFilterFactory [Authorize]");
}
//读取配置文件中的参数 赋值到 配置类中
@Override
public List<String> shortcutFieldOrder() {
//Config.enabled
return Arrays.asList("enabled");
}
@Override
public GatewayFilter apply(AuthorizeGatewayFilterFactory.Config config) {
return (exchange, chain) -> {
//判断是否开启授权验证
if (!config.isEnabled()) {
return chain.filter(exchange);
}
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
//从请求头中获取token
String token = headers.getFirst(AUTHORIZE_TOKEN);
if (token == null) {
//从请求头参数中获取token
token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
}
ServerHttpResponse response = exchange.getResponse();
//如果token为空,直接返回401,未授权
if (StringUtils.isEmpty(token)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//处理完成,直接拦截,不再进行下去
return response.setComplete();
}
/**
* todo chain.filter(exchange) 之前的都是过滤器的前置处理
*
* chain.filter().then(
* 过滤器的后置处理...........
* )
*/
//授权正常,继续下一个过滤器链的调用
return chain.filter(exchange);
};
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Config {
// 控制是否开启认证
private boolean enabled;
}
}
局部过滤器需要在路由中配置才能生效,配置如下:
spring:
cloud:
gateway:
## 路由
routes:
## id只要唯一即可,名称任意
- id: gateway-provider_1
uri: http://localhost:9024
## 配置断言
predicates:
## Path Route Predicate Factory断言,满足/gateway/provider/**这个请求路径的都会被路由到http://localhost:9024这个uri中
- Path=/gateway/provider/**
## 配置过滤器(局部)
filters:
- AddResponseHeader=X-Response-Foo, Bar
## AuthorizeGatewayFilterFactory自定义过滤器配置,值为true需要验证授权,false不需要
- Authorize=true
访问:http://localhost:8080/gateway/provider/

上述的AuthorizeGatewayFilterFactory只是涉及到了过滤器的前置处理,后置处理是在chain.filter().then()中的then()方法中完成的。
访问:http://localhost:8080/gateway/provider?token=11111
就会发现401无认证已经没了。
上述的AuthorizeGatewayFilterFactory只是涉及到了过滤器的前置处理,后置处理是在chain.filter().then()中的then()方法中完成的。
全局过滤器应用到全部路由上,无需开发者配置,Spring Cloud Gateway也内置了一些全局过滤器,如下图:

GlobalFilter的功能其实和GatewayFilter是相同的,只是GlobalFilter的作用域是所有的路由配置,而不是绑定在指定的路由配置上。
多个GlobalFilter可以通过@Order或者getOrder()方法指定每个GlobalFilter的执行顺序,order值越小,GlobalFilter执行的优先级越高。
注意,由于过滤器有pre和post两种类型,pre类型过滤器如果order值越小,那么它就应该在pre过滤器链的顶层,post类型过滤器如果order值越小,那么它就应该在pre过滤器链的底层。示意图如下:

当然除了内置的全局过滤器,实际工作中还需要定制过滤器,下面来介绍一下如何自定义。
场景:模拟Nginx的Access Log 功能,记录每次请求的相关信息。代码如下:
/**
* 实现GlobalFilter
*/
@Slf4j
@Component
@Order(value = Integer.MIN_VALUE)
public class AccessLogGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//filter的前置处理
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().pathWithinApplication().value();
InetSocketAddress remoteAddress = request.getRemoteAddress();
return chain
//继续调用filter
.filter(exchange)
//filter的后置处理
.then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
HttpStatus statusCode = response.getStatusCode();
log.info("请求路径:{},远程IP地址:{},响应码:{}", path, remoteAddress, statusCode);
}));
}
}
好了,全局过滤器不必在路由上配置,注入到IOC容器中即可全局生效。
这样做有什么坏处呢?
服务的IP的地址一旦修改了,路由配置中的uri必须修改服务集群中无法实现负载均衡。
此时就需要集成的注册中心,使得网关能够从注册中心自动获取uri(负载均衡)。
以上具体创建gateway微服务以及如何集成到nacos,请移步至:《SpringCloud alibaba实战–第5章》
值得我们注意的是:路由配置中唯一不同的就是路由的uri,格式:lb://service-name,这是固定写法
为什么指定了lb就可以开启负载均衡,前面说过全局过滤器LoadBalancerClientFilter就是负责路由寻址和负载均衡的。
上述例子都是将网关的一系列配置写到项目的配置文件中,一旦路由发生改变必须要重新项目,这样维护成本很高。
其实我们可以将网关的配置存放到配置中心中,这样由配置中心统一管理,一旦路由发生改变,只需要在配置中心修改,这样便能达到一处修改,多出生效的目的。
有一篇更详细的博文可以带你一步一步的实现如何通过nacos实现动态路由。《springCloud alibaba:Nacos Config-服务配置》

通过前面的测试可以看到一个现象:一旦路由的微服务下线或者失联了,Spring Cloud Gateway直接返回了一个错误页面,如下图:

显然这种异常信息不友好,前后端分离架构中必须定制返回的异常信息。
传统的Spring Boot 服务中都是使用@ControllerAdvice来包装全局异常处理的,但是由于服务下线,请求并没有到达。
因此必须在网关中也要定制一层全局异常处理,这样才能更加友好的和客户端交互。
Spring Cloud Gateway提供了多种全局处理的方式,今天我们只介绍其中一种方式,实现还算比较优雅。
直接创建一个类GlobalErrorExceptionHandler,实现ErrorWebExceptionHandler,重写其中的handle方法,代码如下:
/**
* 用于网关的全局异常处理
* @Order(-1):优先级一定要比ResponseStatusExceptionHandler低
*/
@Slf4j
@Order(-1)
@Component
@RequiredArgsConstructor
public class GlobalErrorExceptionHandler implements ErrorWebExceptionHandler {
private final ObjectMapper objectMapper;
@SuppressWarnings({"rawtypes", "unchecked", "NullableProblems"})
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
}
// JOSN格式返回
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
if (ex instanceof ResponseStatusException) {
response.setStatusCode(((ResponseStatusException) ex).getStatus());
}
return response.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
try {
//todo 返回响应结果,根据业务需求,自己定制
CommonResponse resultMsg = new CommonResponse("500",ex.getMessage(),null);
return bufferFactory.wrap(objectMapper.writeValueAsBytes(resultMsg));
}
catch (JsonProcessingException e) {
log.error("Error writing response", ex);
return bufferFactory.wrap(new byte[0]);
}
}));
}
}
好了,全局异常处理已经定制完成了,在测试一下,此时正常返回JSON数据了,如下图:

Spring Cloud Gateway今天就分享到这里,主要介绍了以下几个知识点: