Appearance
Spring Cloud Gateway
1. 介绍
Gateway,即为网关,为用户提供了一个统一入口,并将请求路由到后端不同的微服务。
假设现在有多个微服务,包括订单服务、商品服务、支付服务等,并且这些微服务部署在多台服务器上,那么前端应用应该访问哪个请求路径呢?显然,后台微服务应该给出一个统一的入口,这就是网关的作用之一。
当请求到达网关后,例如https://gateway-ip:port/createOrder,网关怎么判断这个请求应该转发给哪个微服务呢?所以,我们应该在网关服务里设置路由规则,由路由规则决定将请求转发给哪个微服务。
为了获取微服务的地址,网关也需要到服务注册与发现中心(例如Nacos)上找到对应微服务的地址。

2. 环境搭建
单独新建一个模块,引入如下依赖:
xml
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>注意,Spring Cloud Gateway提供两套实现,spring-cloud-starter-gateway和spring-cloud-starter-gateway-mvc,前者基于Reactive模型,后者基于传统的Servlet模型,一般使用前者。

网关主启动类:
java
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication
{
public static void main( String[] args )
{
SpringApplication.run(GatewayApplication.class, args);
}
}然后在配置文件中修改:
yaml
server:
port: 80
spring:
application:
name: gateway
cloud:
gateway:
enabled: true为了前端访问时不带端口号,一般将端口改为80。
3. 路由介绍
网关中最主要的功能就是请求路由,对应的是路由规则。
3.1 术语与流程介绍
路由规则涉及三个术语:
Route:一个Route就是一个路由规则,由ID、URI、一组Predicate(断言)和一组Filter(过滤器)组成。当请求到达网关时,如果该路由规则的断言全部匹配(返回true),表示该请求可以由该路由规则处理,会将该请求转发到URI对应的路径;Predicate:断言,是一个函数式接口(Predicate),当请求到来时,被封装为ServerWebExchange,断言可以对该请求实行特定的匹配规则,返回boolean值;Filter:过滤器,更容易理解的话应该称为拦截器,分为拦截请求和拦截响应,当一个请求要转发给某个微服务时,可以使用请求拦截对该请求做出修改,也可以使用响应拦截对微服务返回的响应做出修改;
网关工作图如下:

当请求到达网关时,如果Handler Mapping判读该请求全部符合某个Route的断言规则,那么就交给Web Handler进行请求转发,首先会经过拦截器链的前执行器(在请求到达微服务之前),之后请求到达微服务执行业务,之后响应从微服务回到网关,经过拦截器的后执行器(请求从微服务回来),最终返回给前端。
3.2 配置路由规则
我们可以在网关配置文件中配置路由规则,如下:
yaml
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
- id: product-service
uri: lb://product-service
predicates:
- Path=/api/product/**spring.cloud.gateway.routes是一个列表,其中的元素是RouteDefinition,包含如下内容:javapublic class RouteDefinition { private String id; private @NotEmpty @Valid List<PredicateDefinition> predicates = new ArrayList(); private @Valid List<FilterDefinition> filters = new ArrayList(); private @NotNull URI uri; private Map<String, Object> metadata = new HashMap(); private int order = 0; }id:路由规则标识符;uri:请求匹配上该路由规则后,需要将该请求转发到哪个地址;可以配置的内容如下:- 具体的主机和端口,格式为:
http://hostname:port或https://hostname:port; - 带负载均衡的服务发现名称,格式为:
lb://service-name,lb表示load balancer,负载均衡的意思,service-name为微服务名称,需要与Nacos中的微服务名称相同; - WebSocket 连接,格式为:
ws://hostname:port或wss://hostname:port
- 具体的主机和端口,格式为:
predicates:一组断言规则,具体后续再讲解;filters:一组过滤器规则,具体后续再讲解;metadata:元数据,具体后续再讲解;order:该路由规则的优先级,数字越低优先级越高。路由规则是有顺序的,如果前面的路由规则匹配上该请求,那么就不再进行后续匹配了。
3.3 测试
通过上面的配置,我们就可以测试路由是否生效了,不过,还需要注意的是,我们匹配的路径是/api/order/**等,表示请求路径应该以/api/order/开头,转发后到达微服务的路径也是这样的,所以需要修改微服务的请求路径,统一以/api/order/开头,在配置文件中修改如下:
properties
server.servlet.context-path=/api/orderproperties
server.servlet.context-path=/api/product并且,我们之前的订单服务,使用OpenFeign远程调用了商品服务,也需要添加前缀:
java
@FeignClient(value = "product-service", path = "/api/product", fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {
@GetMapping("/product/{id}")
Product getProduct(@PathVariable("id") Long id);
}由@FeignClient中的path指定前缀路径。
启动如下微服务和网关:

然后在浏览器中访问如下地址:

测试成功。
4. Predicate
在之前的例子中,我们写的断言如下:
yaml
predicates:
- Path=/api/order/**为什么要这么写呢?
4.1 什么是断言
断言就是一种匹配规则,根据HTTP请求返回true或false。
在Spring Cloud Gateway中,断言由RoutePredicateFactory定义,其实现类如下:

也就是说,每个实现类就实现了某种断言,例如PathRoutePredicateFactory就是根据请求路径匹配请求,该断言的名称就是Path。
我们就以PathRoutePredicateFactory为例,介绍断言是如何工作的,首先在PathRoutePredicateFactory中,存在以下内部类:
java
public static class Config {
private List<String> patterns = new ArrayList<>();
private boolean matchTrailingSlash = true;
}patterns表示可以匹配的路径,可以有多个;matchTrailingSlash表示是否可以匹配路径末尾的/,默认值为true,如果设置为false,假设可以匹配的路径是/red/{segment},/blue/{segment},那么路径为/red/1/的请求就无法通过该断言,最终会导致该路由规则无法匹配到该请求;
之后,在PathRoutePredicateFactory的方法apply中,就可以根据配置判断是否匹配路径:
java
@Override
public Predicate<ServerWebExchange> apply(Config config) {
// 首先从config中解析可以匹配的路径规则
final ArrayList<PathPattern> pathPatterns = new ArrayList<>();
synchronized (this.pathPatternParser) {
pathPatternParser.setMatchOptionalTrailingSeparator(config.isMatchTrailingSlash());
config.getPatterns().forEach(pattern -> {
PathPattern pathPattern = this.pathPatternParser.parse(pattern);
pathPatterns.add(pathPattern);
});
}
// 返回GatewayPredicate对象
return new GatewayPredicate() {
// 在test方法中判断是否匹配
@Override
public boolean test(ServerWebExchange exchange) {
// 拿到请求的路径
PathContainer path = (PathContainer) exchange.getAttributes().computeIfAbsent(
GATEWAY_PREDICATE_PATH_CONTAINER_ATTR,
s -> parsePath(exchange.getRequest().getURI().getRawPath()));
// config中的路径逐一与请求路径判断是否匹配
PathPattern match = null;
for (int i = 0; i < pathPatterns.size(); i++) {
PathPattern pathPattern = pathPatterns.get(i);
if (pathPattern.matches(path)) {
match = pathPattern;
break;
}
}
if (match != null) {
// 匹配上了,返回true
traceMatch("Pattern", match.getPatternString(), path, true);
PathMatchInfo pathMatchInfo = match.matchAndExtract(path);
putUriTemplateVariables(exchange, pathMatchInfo.getUriVariables());
exchange.getAttributes().put(GATEWAY_PREDICATE_MATCHED_PATH_ATTR, match.getPatternString());
String routeId = (String) exchange.getAttributes().get(GATEWAY_PREDICATE_ROUTE_ATTR);
if (routeId != null) {
// populated in RoutePredicateHandlerMapping
exchange.getAttributes().put(GATEWAY_PREDICATE_MATCHED_PATH_ROUTE_ID_ATTR, routeId);
}
return true;
}
else {
// 没有匹配上,返回false
traceMatch("Pattern", config.getPatterns(), path, false);
return false;
}
}
@Override
public Object getConfig() {
return config;
}
@Override
public String toString() {
return String.format("Paths: %s, match trailing slash: %b", config.getPatterns(),
config.isMatchTrailingSlash());
}
};
}除了匹配方法,还存在以下方法需要关注shortcutFieldOrder,表示简写形式:
java
private static final String MATCH_TRAILING_SLASH = "matchTrailingSlash";
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("patterns", MATCH_TRAILING_SLASH);
}4.2 断言的写法
断言有两种形式的写法:完整形式与简写形式。
完整形式需要指明断言的名称与参数,例如:
yaml
predicates:
- name: Path
args:
patterns:
- /api/order/**
matchTrailingSlash: falsename:就是断言实现类的前缀名称,例如PathRoutePredicateFactory的名称就是Path,MethodRoutePredicateFactory的名称就是Method;args:断言实现类内部类Config的配置参数,例如PathRoutePredicateFactory的内部配置类有patterns和matchTrailingSlash,我们就可以在args中写上对应的配置;
简写形式的形式为name=args,例如:
yaml
predicates:
- Path=/api/order/**也就是说使用名称为Path的断言实现,并且参数为/api/order/**,表示匹配路径是/api/order/**,如果要配置matchTrailingSlash,可以这样写:
yaml
predicates:
- Path=/api/order/**,false5. Filter
5.1 Filter怎么配
与断言相似,Filter也有两种形式:完整形式与简写形式。
完整形式:
yaml
filters:
- name: AddRequestHeader
args:
name: X-Request-Id
value: abcname:即GatewayFilterFactory的前缀,例如AddRequestHeaderGatewayFilterFactory的名称即为AddRequestHeader;args:过滤器的参数,可以查看apply()方法的参数,例如NameValueConfig:javapublic static class NameValueConfig { @NotEmpty protected String name; @NotEmpty protected String value; }
简写形式:
就是以name=args-value的形式定义的Filter,例如:
yaml
filters:
- AddResponseHeader=X-Request-Id,abc完整的Filter列表请参照:https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway-server-webflux/gatewayfilter-factories.html
5.2 Filter作用
例如,我们以如下配置演示Filter的作用:
yaml
spring:
application:
name: gateway
cloud:
gateway:
enabled: true
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- AddRequestHeader=X-Request-Id,abc
- name: AddResponseHeader
args:
name: X-Gateway-name
value: ${spring.application.name}以上配置了两个Filter,其中:
AddRequestHeader在向为服务转发请求时,会添加一个请求头,名称为X-request-Id,值为abc;AddResponseHeader在向前端返回响应时,也会添加一个响应头,名称为X-Gateway-name,值为${spring.application.name},即gateway

5.3 自定义Filter
可以实现GatewayFilterFactory接口来实现自己的Filter,实现如下方法:
apply():具体的Filter行为,如果是在响应中添加响应头,需要在then()方法中添加逻辑,函数式编程;shortcutFieldOrder():定义了简写形式;getConfigClass():获取配置类;
java
@Component
public class CustomResponseHeaderGatewayFilterFactory implements GatewayFilterFactory<AbstractNameValueGatewayFilterFactory.NameValueConfig> {
@Override
public GatewayFilter apply(AbstractNameValueGatewayFilterFactory.NameValueConfig config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(()->{
String headerName = config.getName();
String value = "";
String headerValue = config.getValue();
switch (headerValue){
case "uuid": value = UUID.randomUUID().toString();break;
default: value = headerValue;
}
exchange.getResponse().getHeaders().add(headerName, value);
}));
}
};
}
@Override
public List<String> shortcutFieldOrder() {
return List.of("name", "value");
}
@Override
public Class<AbstractNameValueGatewayFilterFactory.NameValueConfig> getConfigClass() {
return AbstractNameValueGatewayFilterFactory.NameValueConfig.class;
}
}将自定义的Filter加入到容器中。
之后,就可以在配置文件中使用自定义的Filter:
yaml
filters:
- CustomResponseHeader=customer-header,uuid测试结果如下:

5.4 默认Filter
除了为每个路由规则配置Filter外,我们也可以配置默认的Filter:
yaml
spring:
cloud:
gateway:
default-filters:
- AddResponseHeader=X-Response-Default-Red, Default-Blue
- PrefixPath=/httpbin5.5 RewritePathGatewayFilterFactory
在前面的例子中,为了匹配微服务,我们在请求路径上加了/api/order/,并且微服务的路径(controller)也要加上相同的路径前缀,如果没有路径,则会提示404。
我们可以使用RewritePathGatewayFilterFactory可以改写路径,即请求到达网关后,匹配到对应的微服务后,网关会改写请求路径,使其匹配后端微服务的路径。
使用如下:
yaml
- RewritePath=/api/order/?(?<segment>.*), /$\{segment}具体解释如下:
/api/order/?(?<segment>.*):这是一个正则表达式,匹配/api/order/后面的内容,并将其捕获到名为segment的组中。? 表示/api/order/后面可以没有内容或有任意内容。/$\{segment}:重写后的路径,将原路径中/api/order/后面的内容(即${segment})拼接到根路径 / 后面。
例如,请求路径为 /api/order/test 会被重写为 /test。
6. Metadata
6.1 基本使用
metadata中可以配置额外的信息,随意配置,例如:
yaml
metadata:
author: shuimuzi
description: service of gateway之后,可以按照如下方式获取metadata:
java
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
// get all metadata properties
route.getMetadata();
// get a single metadata property
route.getMetadata(someKey);例如,我们在自定义的CustomResponseHeaderGatewayFilterFactory中,获取metadata并加入到响应头中:
java
@Override
public GatewayFilter apply(AbstractNameValueGatewayFilterFactory.NameValueConfig config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(()->{
String headerName = config.getName();
String value = "";
String headerValue = config.getValue();
switch (headerValue){
case "uuid": value = UUID.randomUUID().toString();break;
default: value = headerValue;
}
exchange.getResponse().getHeaders().add(headerName, value);
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
Map<String, Object> metadata = route.getMetadata();
metadata.forEach((k,v)->{
exchange.getResponse().getHeaders().add(k, v.toString());
});
}));
}
};
}
6.2 CORS
我们可以在metadata中配置跨域相关内容:
yaml
metadata:
cors:
allowedOrigins: '*'
allowedMethods:
- GET
- POST
allowedHeaders: '*'
maxAge: 30
可以看到,响应头中多了一些关于跨域的内容。