服务网关
官网地址:文档地址
当服务开始变多,我们就要开始让服务聚合起来。
API Gateway(APIGW / API 网关),顾名思义,是出现在系统边界上的一个面向 API 的、串行集中式的强管控服务,这里的边界是企业 IT 系统的边界,可以理解为企业级应用防火墙,主要起到隔离外部访问与内部系统的作用。在微服务概念的流行之前,API 网关就已经诞生了,例如银行、证券等领域常见的前置机系统,它也是解决访问认证、报文转换、访问统计等问题的。
API 网关的流行,源于近几年来移动应用与企业间互联需求的兴起。移动应用、企业互联,使得后台服务支持的对象,从以前单一的Web应用,扩展到多种使用场景,且每种使用场景对后台服务的要求都不尽相同。这不仅增加了后台服务的响应量,还增加了后台服务的复杂性。随着微服务架构概念的提出,API网关成为了微服务架构的一个标配组件。
API 网关是一个服务器,是系统对外的唯一入口。API 网关封装了系统内部架构,为每个客户端提供定制的 API。所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有非业务功能。API 网关并不是微服务场景中必须的组件,如下图,不管有没有 API 网关,后端微服务都可以通过 API 很好地支持客户端的访问。
而我们的微服务也是这样,一般情况下,可能并不是所有的微服务都需要直接暴露给外部调用,这时我们就可以使用路由机制,添加一层防护,让所有的请求全部通过路由来转发到各个微服务,并且转发给多个相同微服务实例也可以实现负载均衡。
在之前,路由的实现一般使用Zuul,但是已经停更,而现在新出现了由SpringCloud官方开发的Gateway路由,它相比Zuul不仅性能上得到了一定的提升,并且是官方推出,契合性也会更好,所以我们这里就主要讲解Gateway。
部署网关
创建一个项目gateway
第一个依赖就是网关的依赖,而第二个则跟其他微服务一样,需要注册到Eureka才能生效,注意别添加Web依赖,使用的是WebFlux框架。
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-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
yaml
server:
port: 8500
eureka:
client:
service-url:
defaultZone: http://localhost:8888/eureka
spring:
application:
name: gateway
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
但是现在还没有配置任何的路由功能,我们接着将路由功能进行配置:
yaml
spring:
cloud:
gateway:
# 配置路由,注意这里是个列表,每一项都包含了很多信息
routes:
- id: service-book # 路由名称
uri: lb://service-book # 路由的地址,lb表示使用负载均衡到微服务,也可以使用http正常转发
predicates: # 路由规则,断言什么请求会被路由
- Path=/book/** # 只要是访问的这个路径,一律都被路由到上面指定的服务
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
路由规则的详细列表(断言工厂列表)在这里:文档地址 ,可以指定多种类型,包括指定时间段、Cookie携带情况、Header携带情况、访问的域名地址、访问的方法、路径、参数、访问者IP等。 也可以使用配置类进行配置,但是还是推荐直接配置文件,省事。
路由过滤器
路由过滤器支持以某种方式修改传入的 HTTP 请求或传出的 HTTP 响应,路由过滤器的范围是某一个路由,跟之前的断言一样,Spring Cloud Gateway 也包含许多内置的路由过滤器工厂,详细列表:文档地址
比如我们现在希望在请求到达时,在请求头中添加一些信息再转发给我们的服务,那么这个时候就可以使用路由过滤器来完成,我们只需要对配置文件进行修改:
yaml
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: service-book # 路由名称
uri: lb://service-book # 路由的地址,lb表示使用负载均衡到微服务,也可以使用http正常转发
predicates: # 路由规则,断言什么请求会被路由
- Path=/book/** # 只要是访问的这个路径,一律都被路由到上面指定的服务
filters: # 添加过滤器
- AddRequestHeader=Test, HelloWorld!
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
比如我们将所有服务都聚合起来,形成同一个api接口,访问书籍的就用/api/book/**,用户就用/api/user
yaml
spring:
cloud:
gateway:
# 配置路由,注意这里是个列表,每一项都包含了很多信息
routes:
- id: service-book # 路由名称
uri: lb://service-book # 路由的地址,lb表示使用负载均衡到微服务,也可以使用http正常转发
predicates: # 路由规则,断言什么请求会被路由
- Path=/api/book/** # 只要是访问的这个路径,一律都被路由到上面指定的服务
filters:
- AddRequestHeader=Test, HelloWorld!
# 跳过第一个路径前缀,这样就可以统一路径 localhost:8500/api/book/** 转发到 lb:service-book/book/**
- StripPrefix=1
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
我们把 Path 改成 /api/book,然后filters 里添加 - StripPrefix=1
跳过一个路径前缀,这样我们的服务就统一前缀了。
GlobalFilter
除了针对于某一个路由配置过滤器之外,我们也可以自定义全局过滤器,它能够作用于全局。但是我们需要通过代码的方式进行编写:
java
@Order(100)
@Component
public class TestFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//先获取ServerHttpRequest对象,注意不是HttpServletRequest
ServerHttpRequest request = exchange.getRequest();
//打印一下所有的请求参数
System.out.println(request.getQueryParams());
//判断是否包含test参数,且参数值为1
List<String> value = request.getQueryParams().get("test");
if (value != null && value.contains("1")) {
//将ServerWebExchange向过滤链的下一级传递(跟JavaWeb中介绍的过滤器其实是差不多的)
return chain.filter(exchange);
} else {
//直接在这里不再向下传递,然后返回响应
return exchange.getResponse().setComplete();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
原理
Route
Route 是 gateway 中最基本的组件之一,表示一个具体的路由信息载体。
java
public class Route implements Ordered {
private final String id; //标识符,区别于其他 Route。
private final URI uri; // 路由指向的目的地 uri,即客户端请求最终被转发的目的地。
private final int order; // 用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。
private final AsyncPredicate<ServerWebExchange> predicate; // 谓语,表示匹配该 Route 的前置条件,即满足相应的条件才会被路由到目的地 uri。
private final List<GatewayFilter> gatewayFilters; // 过滤器用于处理切面逻辑,如路由转发前修改请求头等。
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
GatewayFilter
很多框架都有 Filter 的设计,用于实现可扩展的切面逻辑。
java
public interface GatewayFilter extends ShortcutConfigurable {
String NAME_KEY = "name";
String VALUE_KEY = "value";
/**
* Process the Web request and (optionally) delegate to the next
* {@code WebFilter} through the given {@link GatewayFilterChain}.
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return {@code Mono<Void>} to indicate when request processing is complete
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
处理Web请求并(可选地)通过给定的{@link GatewayFilterChain}委托给下一个{@code WebFilter}。
Filter 最终是通过 filter chain 来形成链式调用的,每个 filter 处理完 pre filter 逻辑后委派给 filter chain,filter chain 再委派给下一下 filter。
java
public interface GatewayFilterChain {
/**
* Delegate to the next {@code WebFilter} in the chain.
* @param exchange the current server exchange
* @return {@code Mono<Void>} to indicate when request handling is complete
*/
Mono<Void> filter(ServerWebExchange exchange);
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
委派给链中的下一个WebFilter。
Route 构建的原理
Spring boot 遵循规约大于配置的原则,starter 模块都有对应的以模块名称作前缀,以 "AutoConfiguration" 后缀的自动装配类。同样的还有以模块名前缀,以Properties后缀的配置类作为支持。
Gateway 模块自动装配类为 GatewayAutoConfiguration,对应的配置类为 GatewayProperties。
java
@ConfigurationProperties("spring.cloud.gateway") // 表明以 "spring.cloud.gateway" 前缀的 properties 会绑定 GatewayProperties。
@Validated
public class GatewayProperties {
/**
* List of Routes
*/
@NotNull
@Valid
private List<RouteDefinition> routes = new ArrayList<>(); // 用来对 Route 进行定义。
/**
* List of filter definitions that are applied to every route.
*/
private List<FilterDefinition> defaultFilters = new ArrayList<>(); // 用于定义默认的 Filter 列表,默认的 Filter 会应用到每一个 Route 上,gateway 处理时会将其与 Route 中指定的 Filter 进行合并后并逐个执行。
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
RouteDefinition
该组件用来对 Route 信息进行定义,最终会被 RouteLocator 解析成 Route
java
public class RouteDefinition {
@NotEmpty
private String id = UUID.randomUUID().toString(); // 定义 Route 的 id,默认使用 UUID。
@NotEmpty
@Valid
private List<PredicateDefinition> predicates = new ArrayList<>(); // 定义 Predicate。
@Valid
private List<FilterDefinition> filters = new ArrayList<>(); // 定义 Filter。
@NotNull
private URI uri; // 定义目的地 URI。
private int order = 0; // 定义 Route 的序号。
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LoadBalancerClientFilter
java
public class LoadBalancerClientFilter implements GlobalFilter, Ordered {
private static final Log log = LogFactory.getLog(LoadBalancerClientFilter.class);
public static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10100;
private final LoadBalancerClient loadBalancer;
public LoadBalancerClientFilter(LoadBalancerClient loadBalancer) {
this.loadBalancer = loadBalancer;
}
@Override
public int getOrder() {
return LOAD_BALANCER_CLIENT_FILTER_ORDER;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获得 URL
URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
if (url == null || !url.getScheme().equals("lb")) {
return chain.filter(exchange);
}
// 添加 原始请求URI 到 GATEWAY_ORIGINAL_REQUEST_URL_ATTR
//preserve the original url
addOriginalRequestUrl(exchange, url);
log.trace("LoadBalancerClientFilter url before: " + url);
// 获取 服务实例
final ServiceInstance instance = loadBalancer.choose(url.getHost());
if (instance == null) {
throw new NotFoundException("Unable to find instance for " + url.getHost());
}
/*URI uri = exchange.getRequest().getURI();
URI requestUrl = loadBalancer.reconstructURI(instance, uri);*/
//
URI requestUrl = UriComponentsBuilder.fromUri(url)
.scheme(instance.isSecure() ? "https" : "http") //TODO: support websockets
.host(instance.getHost())
.port(instance.getPort())
.build(true)
.toUri();
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
// 添加 请求URI 到 GATEWAY_REQUEST_URL_ATTR
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
// 提交过滤器链继续过滤
return chain.filter(exchange);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
java
URI url=exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
if(url==null||!url.getScheme().equals("lb")){
return chain.filter(exchange);
}
1
2
3
4
2
3
4
获得 URL 。只处理 lb:// 为前缀( Scheme )的地址。
java
final ServiceInstance instance = loadBalancer.choose(url.getHost());
1
调用 LoadBalancerClient#choose(String) 方法,获得一个服务实例( ServiceInstance ) ,从而实现负载均衡。
java
URI requestUrl = UriComponentsBuilder.fromUri(url)
.scheme(instance.isSecure() ? "https" : "http") //TODO: support websockets
.host(instance.getHost())
.port(instance.getPort())
.build(true)
.toUri();
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
1
2
3
4
5
6
7
2
3
4
5
6
7
创建 requestUrl 。
设置 requestUrl 到 GATEWAY_REQUEST_URL_ATTR 。后面 Routing 相关的 GatewayFilter 会通过该属性,发起请求。
提交过滤器链继续过滤。注意,这里不需要创建新的 ServerWebExchange