本章目标
学习目标
1、服务网关 Gateway
2、ServerWebExchange
服务网关Gateway
API 网关是一个服务,是系统的唯一入口。从面向对象设计的角度看,它与外观模式类似。API 网关封装了系统内部架构,为每个客户端提供一个定制的 API 。它可能还具有其它职责,如身份验证、监控、负载均衡、限流、降级与应用检测。
1. Spring Cloud Gateway 介绍
Spring Cloud Gateway 基于 Spring Boot 2,是 Spring Cloud 的全新项目。Gateway 旨在提供一种简单而有效的途径来转发请求,并为它们提供横切关注点。
Spring Cloud Gateway 中最重要的几个概念:
- 路由 Route:路由是网关最基础的部分,路由信息由一个 ID 、一个目的 URL 、一组断言工厂和一组 Filter 组成。如果路由断言为真,则说明请求的 URL 和配置的路由匹配。
- 断言 Predicate:Java 8 中的断言函数。Spring Cloud Gateway 中的断言函数输入类型是 Spring 5.0 框架中的 ServerWebExchange 。Spring Cloud Gateway 中的断言函数允许开发者去定义匹配来自 Http Request 中的任何信息,比如请求头和参数等。
- 过滤器 Filter:一个标准的 Spring Web Filter。Spring Cloud Gateway 中的 Filter 分为两种类型:Gateway Filter 和 Global Filter。过滤器 Filter 将会对请求和响应进行修改处理。
2. 入门案例
作为网关来说,网关最重要的功能就是协议适配和协议转发,协议转发也就是最基本的路由信息转发。
创建项目 gateway-server
,演示 Gateway 的基本路由转发功能,也就是通过 Gateway 的 Path 路由断言工厂实现 url 直接转发。
-
引入 Spring Cloud Gateway:Spring Cloud Routing > Gateway :
注意:Gateway 自己使用了 netty 实现了 Web 服务,此处『不需要引入 Spring Web』,如果引入了,反而还会报冲突错误,无法启动。
<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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
-
编写主入口程序代码,如下:
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayServerApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServerApplication.class, args);
}
/**
* 配置
*/
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
/**return builder.routes()
.route(r -> r
.path("/jd")
.uri("http://www.jd.com/")
.id("jd_route")
).build();*/
return builder.routes().route(new Function<PredicateSpec, Route.AsyncBuilder>() {
@Override
public Route.AsyncBuilder apply(PredicateSpec predicateSpec) {
return predicateSpec.path("/jd")
.uri("http://www.jd.com/")
.id("jd_route");
}
}).build();
}
}
3、application.yml
server:
port: 9000
spring:
application:
name: eureka-gateway
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
logging:
level:
org:
springframework:
cloud:
gateway: debug
management:
endpoints:
web:
exposure:
include: '*'
RouteLocator三个实现类
RouteDefinitionRouteLocator 基于路由定义的定位器
CachingRouteLocator 基于缓存的路由定位器
CompositeRouteLocator 基于组合方式的路由定位器
上述代码配置了一个路由规则:当用户输入 /jd 路径时,Gateway 会将请求导向到 [http://www.jd.com]网址。
除了这种编码方式配置外,Gateway 还支持通过项目配置文件配置。例如:
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: 163_route
uri: http://www.163.com
predicates:
- Path=/163
当用户输入 /163 路径时,Gateway 将会导向到 http://www.163.com 网址。
两种配置方式可以同时使用。
另外,Spring Cloud Gateway 提供了一个 gateway actuator ,如果你在项目中引入了 actuator ,并在配置文件中加入如下配置:
management:
endpoints:
web:
exposure:
include: '*'
#表示所有的端点,通过HTTP公开所有的端点
访问网址 http://localhost:8080/actuator/gateway/routes 可看到如下内容
[{
"predicate":"Paths: [/jd], match trailing slash: true",
"route_id":"jd_route",
"filters":[],
"uri":"http://www.jd.com:80",
"order":0
},{
"predicate":"Paths: [/163], match trailing slash: true",
"route_id":"163_route",
"filters":[],
"uri":"http://www.163.com:80/",
"order":0
}]
3. Gateway 内置 Predicate
Spring Cloud Gateway 是由很多的路由断言工厂组成。当 HTTP Request 请求进入 Spring Cloud Gateway 的时候,网关中的路由断言工厂就会根据配置的路由规则,对 HTTP Request 请求进行断言匹配。匹配成功则进行下一步处理,否则,断言失败直接返回错误信息。
早期的 Gateway 断言的配置是通过代码中的 @Bean 进行配置,后来才推出配置文件配置。
3.1 Path 路由断言
前面 163
的示例中,我们使用的就是 Path 路由断言。
spring:
cloud:
gateway:
routes:
- id: <id> # 路由 ID,唯一
uri: <目标 URL> # 目标 URI,路由到微服务的地址
predicates:
- Path=<匹配规则> # 支持通配符
uri 也可以写成为微服务的服务名 :
语法 uri: lb://微服务名
例如:
spring:
cloud:
gateway:
routes:
- id: 商品微服务
uri: http://127.0.0.1:8080 ###或者写成uri: lb://provider-service
predicates:
- Path=/users/**
- id: 品牌微服务
uri: http://www.jd.com
predicates:
- Path=/xxx/**
Path 断言不会改变请求的 URI ,即,Gateway 收到的 URI 是什么样的,那么它将请求转给目标服务的时候,URI 仍然是什么。整个过程中只有 IP、端口部分会被『替换』掉 。这和 Zuul 网关默认会截断一段 URI 的行为『刚好相反』。
再重复一遍:Path 断言不会改变请求的 URI ,整个过程中只有 IP、端口部分会被『替换』掉 。
3.2 其它断言(了解、自学)
-
After 路由断言
After 路由断言会要求你提供一个 UTC 格式的时间,当 Gateway 接收到的请求时间在配置的 UTC 时间之后,则会成功匹配,予以转发,否则不成功。
@Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { ZonedDateTime dateTime = LocalDateTime.now().minusHours(1).atZone(ZoneId.systemDefault()); return builder.routes() .route(r -> r.after(dateTime) .uri("http://www.jd.com:80/") .id("jd_route")) .build(); }
等价的 application.yml 配置:
spring: cloud: gateway: routes: - id: 商品微服务 uri: lb://provider-service predicates: - Path=/users/** - id: 品牌微服务 uri: http://www.jd.com predicates: #品牌微服务有2个断言 - Path=/xxx/** - After=2022-07-21T15:33:11.009+08:00[Asia/Shanghai]
说明:当请求/xxx地址时,如果当前系统时间在20227-21之后,则会成功,否则失败
UTC是根据原子钟来计算时间,而GMT是根据地球的自转和公转来计算时间。UTC是现在用的时间标准,GMT是老的时间计量标准。UTC更加精确,由于现在世界上最精确的原子钟50亿年才会误差1秒,可以说非常精确。
对于 UTC 的时间格式,你可以使用如下 Java 代码生成:
String datetime = ZonedDateTime.now().minusHours(1).format(DateTimeFormatter.ISO_DATE_TIME); //当前系统时间 减去一个小时 System.out.println(datetime);
-
Before 路由断言
Before 路由断言和之前的 After 路由断言类似。它会取一个 UTC 时间格式的时间参数,当请求进来的当前时间在配置的时间之前会成功(放行),否则不能成功。
spring: cloud: gateway: routes: - id: 品牌微服务 uri: http://www.jd.com predicates: #品牌微服务有2个断言 - Path=/xxx/** - Before=2022-07-21T15:33:11.009+08:00[Asia/Shanghai]
-
Between 路由断言
spring: cloud: gateway: routes: - id: 品牌微服务 uri: http://www.jd.com predicates: - Path=/xxx/** - Between=2020-07-21T15:33:11.009+08:00[Asia/Shanghai],2022-07-21T15:33:11.009+08:00[Asia/Shanghai]
-
Cookie 路由断言
Cooke 路由断言会取两个参数:HTTP 请求所携带的 Cookie 的 key 和 value。当请求中携带的 cookie 和 Cookie 路由断言中配置的 cookie 一致时,路由才匹配成功。
spring: cloud: gateway: routes: - id: 品牌微服务 uri: http://www.jd.com predicates: - Cookie=username, tom - Path=/xxx/**
该功能可以使用 Postman 进行测试。在 postman 中为请求添加携带的 Cookie 有两种方式。
- 直接在 Headers 中添加 Cookie和 username=tom
- 在
Cookies
功能中使用Add Cookie
添加
Header 路由断言用于根据 HTTP 请求的 header 中是否携带所配置的信息与否,来决定是否通过断言。
spring:
cloud:
gateway:
routes:
- id: 商品微服务
uri: lb://provider-service
predicates:
- Header=token, tom
- Path=/xxx/**
-
Method 路由断言
Method 路由断言会根据路由信息所配置的 method 对请求方式是 GET 或者 POST 等进行断言匹配。
spring: cloud: gateway: routes: - id: 商品微服务 uri: lb://provider-service predicates: - Method=GET #必须是get请求 - Path=/users/**
-
Query 路由断言
Query 断言会从请求中获取两个参数,将请求参数和 Query 断言中的配置进行匹配。
例如,http://localhost:9000/test?username=tom 中的 username=tom 和 r.query(“username”, “tom”) 匹配。
spring: cloud: gateway: routes: - id: 商品微服务 uri: lb://provider-service predicates: - Query=username, tom #必须携带请求参数username,而且值必须是tom,不能是其它的值 - Path=/users/**
-
组合使用
各种 Predicates 同时存在于同一个路由时,请求必须『同时满足所有』的条件才被这个路由匹配。
spring: cloud: gateway: routes: - id: 商品微服务 uri: lb://provider-service order: 0 predicates: - Path=/user/** - Method=GET - Header=X-Request-Id, \d+ - Query=name, zhangsan. #请求参数必须是name=zhangsan. ”点“ 表示匹配任务一个字符
order代表的优先级是从小往大排序的,即数值越小,优先级越高
4. 自定义路由断言
自定义路由断言,就是允许你自定义路由的评判规则。自定义路由断言有几个前提要求:
- 自定义的路由断言要继承 AbstractRoutePredicateFactory 类。
- 自定义的路由断言按惯例叫作:XxxRoutePredicateFactory ,这样,在未来使用时可直接使用 Xxx 作为其名字引用。当然,你可以通过 name() 方法自定义名字,后续使用时,就使用 name() 返回的字符串。
- 每个 RoutePredicateFactory 都会有一个 Config 类与之对应,由于它们常见是 1:1 的关系,所以,通常会将 Config 类定义成 RoutePredicateFactory 内部类的形式。
例如:
@Component
public class XxxRoutePredicateFactory
extends AbstractRoutePredicateFactory<XxxRoutePredicateFactory.Config> {
public XxxRoutePredicateFactory() {
super(Config.class);
}
/*@Override
public String name() {
return "yyy";
}*/
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
String requestURI = exchange.getRequest().getURI().getPath();
log.debug("request-uri: {}", requestURI);
if (requestURI.length() % 2 == 0)
return true;
else
return false;
};
}
public static class Config {
// 简单情况下,Config 类里可以什么都没有
}
}
想要使用自定义的路由断言只需要这样配置它:
spring:
cloud:
gateway:
routes:
- id: 商品微服务
uri: lb://provider-service
predicates:
- name: Xxx #自定义断言名字, 为XxxRoutePredicateFactory 类的前缀Xxx
- Path=/users/**
Config 类的作用是用于指出自定义的 Xxx 路由断言的配置中支持自定义的属性。
为自定义的路由断言添加属性及 getter/setter:
@Data
public static class Config{
private String name;
private String password;
}
这样在配置中,你就可以『多』添加两个自定义的属性:
- id: 商品微服务
uri: lb://provider-service
predicates:
- name: Xxx
args:
name: 张三丰 # 看这里
password: 111 # 和这里
- Path=/users/**
回头,你可以在你的自定义路由断言的 apply 方法中取到你在配置文件中所填写的值。(因为 apply 方法的参数就是 Config 对象)。
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
String path = serverWebExchange.getRequest().getURI().getPath();
MultiValueMap<String, String> queryParams = serverWebExchange.getRequest().getQueryParams();
List<String> queryNames = queryParams.get("name");
List<String> queryPass = queryParams.get("pass");
String name = config.getName();
String pass = config.getPassword();
if(name.equals(queryNames.get(0)) && pass.equals(queryPass.get(0))){
return true;
}
return false;
}
};
}
5. Gateway 内置 Filter(了解)
Spring Cloud Gateway 中内置了很多的过滤器,你也可以根据自己的实际需求定制并添加自己的路由过滤器。
路由过滤器允许以某种方式修改请求进来的 HTTP 请求或返回的 HTTP 响应。
5.1 AddRequestHeader 过滤器
AddRequestHeader 过滤器用于对匹配上的请求加上指定的 header 。
在另一个端口(例如 8081)运行一个服务提供者项目,其中代码负责从请求的请求头中获取数据,类似如下:
@RequestMapping("/user/test")
public String header(@RequestHeader(name="jwtToken", required = false) String token,String name,String pass) {
System.out.println(token);
return username == null ? "null" : username;
}
.yml 配置文件配置
spring:
application:
name: eureka-gateway
cloud:
gateway:
routes:
- id: route1
uri: http://localhost:8081/ ##uri: lb://eureka-consumer
predicates:
- Path=/user/**
filters:
- AddRequestHeader=jwtToken, aaaaaaaaaaaa
当我们在浏览器输入http://localhost:9000/user/test?username=zhangsan&password=111时,首先网关能匹配到请求url,然后转发给8081,其实是把IP和端口替换掉,则请求url为http://localhost:8081/user/test?username=zhangsan&password=111,在请求8081之前,过滤器帮我们在请求头添加 jwtToken=aaaaaaaaaaaa,
5.2 StripPrefix 过滤器
StripPrefixGatewayFilterFactory 是一个针对请求 url 进行处理的 filter 工厂,用于去除前缀。使用数字表示要截断的路径的数量。
spring:
cloud:
gateway:
routes:
- id: authentication_route
uri: http://127.0.0.1:8081
predicates:
- Path=/user/**
filters:
- StripPrefix=1
8081服务请求方法如下
@RequestMapping("/test")
public String xxx(){
return "ok";
}
请求url:http://localhost:9000/user/test
则网关转发到8081时,去掉一个前缀,真实url为:http://localhost:8081/test
5.3 RewritePath 过滤器
RewritePath 过滤器可以重写 URI,去掉 URI 中的前缀。例如,下面就是去掉所有 URI 中的 /xxx/yyy/zzz 部分,只留之后的内容,再进行转发。
以上 Java 代码配置等同于 .yml 配置:
spring:
cloud:
gateway:
routes:
- id: 163_route
uri: http://localhost:8081
predicates:
- Path=/xxx/yyy/zzz/**
filters:
- RewritePath=/xxx/yyy/zzz/(?<segment>.*), /$\{segment}
对于请求路径 /xxx/yyy/zzz/hello ,当前的配置在请求到到达前会被重写为 /hello ,
-
命名分组:
(?<name>正则表达式)
与普通分组一样的功能,并且将匹配的子字符串捕获到一个组名称或编号名称中。在获得匹配结果时,可通过分组名进行获取。
(?<segment>.*)
:匹配 0 个或多个任意字符,并将匹配的结果捕获到名称为 segment 的组中。 -
引用捕获文本:
${name}
将名称为 name 的命名分组所匹配到的文本内容替换到此处。
$\{segment}
:将前面捕获到 segment 中的文本置换到此处,注意,\ 的出现是由于避免 YAML 认为这是一个变量而使用的转义字符。如:http://localhost:9000/xxx/yyy/zzz/test 则segment匹配到的内容为test
最终转发到 http://localhost:8080/test
5.4 其他过滤器
略
6. Gateway 整合 Eureka 注册中心实现路由
Gateway 整合 Eureka Sever(注册中心)之后,会以微服务的 name(或者url) 和 URI 的对应关系为依据(利用 Path 路由断言),将它特定 URL 的请求转给对应的微服务。
-
首先将 Gateway 视作普通的 Eureka Client 进行配置、启动。让其『连上』注册中心,从注册中心拉去各个微服务的信息(网址、端口等)。
eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka registry-fetch-interval-seconds: 5 register-with-eureka: true
-
配置若干与 Gateway 相关的配置:
spring.cloud.gateway.discovery.locator.enabled=true spring.cloud.gateway.discovery.locator.lower-case-service-id=true logging.level.org.springframework.cloud.gateway=DEBUG
-
.locator.enabled :
该配置是 Gateway 与注册中心整合的开关项。必然要赋值为 true 。
-
.locator.lower-case-service-id :
由于在 Eureka Server 上各个微服务的 ID 都是全大写英文,因此默认情况下,(未来在)Gateway 的路由路径中出现的也会是全大写。
相较而言,大家看全小写英文会更为习惯一些。另外,一旦开启 lower-case,那么就不能用全大写了,而且大小写不能混用。
-
logging.level.org.springframework.cloud.gateway :
日志是非必要配置,这里配置成 DEBUG 级别是为了验证 Gateway 自动生成了 Path 断言规则。
-
先后启动『注册中心』、『服务提供者』和『Gateway』,访问 Gateway,并在访问路径中加上 /服务提供者的标识
,例如:/microservice-department/hello
,你会发现这个请求会被 Gateway 转给 microservice-department 的 /hello
并且,日志中会有类似如下一条信息:
RouteDefinition matched: ReactiveCompositeDiscoveryClient_EUREKA-CONSUMER
案例:
spring:
cloud:
gateway:
routes:
- id: 商品微服务
uri: lb://provider-service
predicates:
- Path=/users/** #Path断言函数
- id: 品牌微服务
uri: http://www.jd.com
predicates:
- Path=/jd/** #Path断言函数
discovery:
locator:
enabled: true
lower-case-service-id: true
server:
port: 9000
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
registry-fetch-interval-seconds: 5
register-with-eureka: true
请求:http://localhost:9000/eureka-consumer/test
说明:eureka-consumer 表示服务名 对应 localhost:8081
7. 自定义路由局部 Filter
和自定义路由断言一样,自定义路由有几个前提要求:
- 自定义的路由过滤器要继承 AbstractGatewayFilterFactory 。
- 自定义的路由过滤器按惯例叫做:XxxGatewayFilterFactory ,这样,在未来使用时可以使用 Xxx 作为其名字引用。当然,你可以通过 name() 方法自定义名字,
注意:如果没有自定义名字,则自定义过滤器的名字必须叫 名字+GatewayFilterFactory
3.每个 GatewayFilterFactory 都会有一个 Config 类与之对应,由于它们常见是 1:1 的关系,所以,通常会将 Config 类定义成 GatewayFilterFactory 内部类的形式。
@Component
public class XxxGatewayFilterFactory
extends AbstractGatewayFilterFactory<XxxGatewayFilterFactory.Config> {
public XxxGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
// 逻辑代码 ...
if (...) {
// 流程继续向下,走到下一个过滤器,直至路由目标。
return chain.filter(exchange);
} else {
// 否则流程终止,拒绝路由。
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
};
}
}
public static class Config {
// 简单情况下,Config 类里可以什么都没有
}
}
上面的过滤器的逻辑结构所实现的功能:当条件成立时,允许路由;否则,直接返回
路由器的所有代码逻辑都是在『路由前』执行,也就是转发的微服务即使没有启动也会执行。当然,这种形式的过滤器的更简单的情况是:执行某些代码,然后始终是放行。
要注意:当自定义多个局部过滤器时,依靠配置文件 -name 来保证执行顺序,如:filters:
-name: Xxx 先执行 不是按照Order的值来决定,@Order只对全局过滤器起作用
-name: Yyy 后执行另外如果既有局部过滤器,又有全局过滤器,那么先执行所有的局部过滤器,根据局部过滤器根据配置文件配置的先后顺序(默认也是有一个顺序的,从1开始递增),再执行所有的全局过滤器,全局过滤器的顺序看@Order 的值,值最小先执行
案例:对用户信息进行认证
@Component
public class XxxGatewayFilterFactory extends AbstractGatewayFilterFactory<XxxGatewayFilterFactory.Config> {
public XxxGatewayFilterFactory() {
super(Config.class);
}
@Override
public String name() {
return "yyy";
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
List<String> token = exchange.getRequest().getHeaders().get("token");
if (token.get(0).equals("aaa")) {
return chain.filter(exchange);
} else {
// 否则流程终止,拒绝路由。
//exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
//return exchange.getResponse().setComplete();
String jsonStr = "{\"status\":\"-1\", \"msg\":\"token非法\"}";
byte[] bytes = jsonStr.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
return exchange.getResponse().writeWith(Flux.just(buffer));
}
}
};
}
配置文件,注意过滤器重写了名字为 yyy
spring:
application:
name: eureka-gateway
cloud:
gateway:
routes:
- id: 商品微服务
uri: lb://provider-service
predicates:
- Path=/test
filters:
- name: yyy
discovery:
locator:
enabled: true
lower-case-service-id: true
测试:
还可以获取其它的信息
ServerHttpRequest request = exchange.getRequest();
log.info("{}", request.getMethod());
log.info("{}", request.getURI());
log.info("{}", request.getPath());
log.info("{}", request.getQueryParams()); // Get 请求参数
request.getHeaders().keySet().forEach(key -> {
log.info("{}: {}", key, request.getHeaders().get(key))
});
8. JSON 形式的错误返回
上述的『拒绝』是以 HTTP 的错误形式返回,即 4xx、5xx 的错误。
有时,我们的返回方案是以 200 形式的『成功』返回,然后再在返回的信息中以自定义的错误码和错误信息的形式告知请求发起者请求失败。
此时,就需要 过滤器『成功』返回 JSON 格式的字符串:
String jsonStr = "{\"status\":\"-1\", \"msg\":\"error\"}";
byte[] bytes = jsonStr.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
return exchange.getResponse().writeWith(Flux.just(buffer));
9. 获取 Body 中的请求参数(了解、自学)
由于 Gateway 是基于 Spring 5 的 WebFlux 实现的(采用的是 Reactor 编程模式),因此,从请求体中获取参数信息是一件挺麻烦的事情。
有一些简单的方案可以从 Request 的请求体中获取请求参数,不过都有些隐患和缺陷。
最稳妥的方案是模仿 Gateway 中内置的 ModifyRequestBodyGatewayFilterFactory,不过,这个代码写起来很啰嗦。
具体内容可参考这篇文章:Spring Cloud Gateway(读取、修改 Request Body)
不过考虑到 Gateway 只是做请求的『转发』,而不会承担业务责任,因此,是否真的需要在 Gateway 中从请求的 Body 中获取请求数据,这个问题可以斟酌。
10. 过滤器的另一种逻辑形式
有时你对过滤器的运用并非是为了决定是否继续路由,为了在整个流程中『嵌入』额外的代码、逻辑:在路由之前和之后执行某些代码
如果仅仅是在路由至目标微服务之前执行某些代码逻辑,那么 Filter 的形式比较简单:
return (exchange, chain) -> {
// 逻辑代码 ...
// 流程继续向下,走到下一个过滤器,直至路由目标。
return chain.filter(exchange);
}
如果,你想在路由之前和之后(即,目标微服务返回之后)都『嵌入』代码,那么其形式就是:
@Override
public GatewayFilter apply(Config config) {
return ((exchange, chain) -> {
log.info("目标微服务【执行前】执行");
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
log.info("目标微服务【执行后】执行");
}));
});
}
例如,显示一个用于统计微服务调用时长的过滤器以及微服务返回的结果:
@Override
public String name() {
return "yyy";
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
List<String> token = exchange.getRequest().getHeaders().get("token");
long startTime = System.currentTimeMillis();
return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() {
@Override
public void run() {
long endTime = System.currentTimeMillis();
HttpStatus statusCode = exchange.getResponse().getStatusCode();
log.info("用了{}",(endTime-startTime));
}
}));
};
}
配置:
spring:
cloud:
gateway:
routes:
- id: 商品微服务
uri: lb://provider-service
predicates:
- Path=/users/**
filters:
- name: yyy
11. 自定义过滤器的参数
和自定义路由断言一样,自定义的过滤器断言可以自定义参数。
定义的形式是写成 Config 类的属性;使用的形式是在配置中使用 args 配置。
filters:
- name: yyy
args:
name: hello
password: world
12.网关异常处理
问题:项目中使用springcloud-gateway,请求到网关,再路由到微服务时出现微服务未找到异常,我们先通过过滤器放行,对于微服务的结果,通过状态值来进行判定,如收到微服务的500、400、404的错误。对于网关抛出的异常可以定义统一的异常处理器
1、获取微服务相关的异常状态信息
@Component
public class XxxGatewayFilterFactory extends AbstractGatewayFilterFactory<XxxGatewayFilterFactory.Config> {
public XxxGatewayFilterFactory(){
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
List<String> token = exchange.getRequest().getHeaders().get("token");
if(token.get(0).equals("qqq")){
return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() {
@Override
public void run() { //这是一个异步操作
HttpStatus statusCode = exchange.getResponse().getStatusCode();
if (statusCode.value() == 500) {
throw new RuntimeException("{\"status\":\"500\", \"msg\":\"服务器内部报错\"}");
} else if (statusCode.value() == 404) {
throw new RuntimeException("{\"status\":\"404\", \"msg\":\"url地址不合法\"}");
}else if (statusCode.value() == 400) {
throw new RuntimeException("{\"status\":\"400\", \"msg\":\"请求参数错误\"}");
}
}
}));
}else{
String jsonStr = "{\"status\":\"-1\", \"msg\":\"token无效|token为空\"}";
byte[] bytes = jsonStr.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
return exchange.getResponse().writeWith(Flux.just(buffer));
}
}
};
}
public static class Config{}
}
2、定义统一异常处理的相关类,继承ErrorWebExceptionHandler
@Slf4j
@Order(6)
@Component
public class GlobalExceptionConfiguration implements ErrorWebExceptionHandler {
ObjectMapper objectMapper = new ObjectMapper();
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
//response是服务端对客户端请求的一个响应,其中封装了响应头、状态码、内容(也就是最终要在浏览器上显示的HTML
// 代码或者其他数据格式)等,服务端在把response提交到客户端之前,会使用一个缓冲区,并向该缓冲区内写入响应头和
// 状态码,然后将所有内容flush(flush包含两个步骤:先将缓冲区内容发送至客户端,然后将缓冲区清空)。这就标志着该
// 次响应已经committed(提交)。对于当前页面中已经committed(提交)的response,就不能再使用这个response向缓冲区
// 写任何东西 (注:以为JSP中,response是一个JSP页面的内置对象,所以同一个页面中的response.XXX()是同一个response的不同方法,只要其中一个已经导致了committed, 那么其它类似方式的调用都会导致 IllegalStateException异常)
if (response.isCommitted()) {
return Mono.error(ex);
}
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
System.out.println(ex);
return response.writeWith(Mono.fromSupplier(new Supplier<DataBuffer>() {
@Override
public DataBuffer get() {
DataBufferFactory bufferFactory = response.bufferFactory();
try {
return bufferFactory.wrap(objectMapper.writeValueAsBytes(ex.getMessage()));
} catch (JsonProcessingException e) {
log.warn("Error writing response", ex);
return bufferFactory.wrap(new byte[0]);
}
}
}));
}
}
说明:
对于这种微服务的状态捕获的情况,也就是网关应该获取每个微服务错误状态码,所以严格的来说,写在全局过滤器更好
12. 自定义全局 Filter
自定义全局过滤器比局部过滤器要简单,因为它『不需要指定对哪个路由生效,它对所有路由都生效』。
@Order 注解是为了去控制全局过滤器的先后顺序,不是局部的顺序,值越小,优先级越高。
@Component
@Order(0)
public class CustomGlobalFilter implements GlobalFilter{
}
案例:如果ip是本机 就不放行
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
return -100;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
HttpHeaders headers = exchange.getRequest().getHeaders();
String host = exchange.getRequest().getURI().getHost();
System.out.println(host);
// 此处写死了,演示用,实际中需要采取配置的方式
if (host.contains("localhost")) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
String jsonStr = "{\"status\":\"-1\", \"msg\":\"error\"}";
byte[] bytes = jsonStr.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
}
return chain.filter(exchange);
}
}
案例2:获取微服务的状态信息,如果非200,进行统一的异常处理
@Component
@Order(2)
public class TokenGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String host = exchange.getRequest().getURI().getHost();
List<String> tokens = exchange.getRequest().getHeaders().get("token");
if (tokens != null) {
if (tokens.get(0).equals("qqq")) {
return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() {
@Override
public void run() {
HttpStatus statusCode = exchange.getResponse().getStatusCode();
if (statusCode.value() == 500) {
throw new RuntimeException("{\"status\":\"500\", \"msg\":\"服务器内部报错\"}");
} else if (statusCode.value() == 404) {
throw new RuntimeException("{\"status\":\"404\", \"msg\":\"url地址不合法\"}");
}else if (statusCode.value() == 400) {
throw new RuntimeException("{\"status\":\"400\", \"msg\":\"请求参数错误\"}");
}
}
}));
}else{
String jsonStr = "{\"status\":\"400\", \"msg\":\"token非法\"}";
byte[] bytes = jsonStr.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
return exchange.getResponse().writeWith(Flux.just(buffer));
}
} else {
String jsonStr = "{\"status\":\"400\", \"msg\":\"token无效|token为空\"}";
byte[] bytes = jsonStr.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
return exchange.getResponse().writeWith(Flux.just(buffer));
}
}
public static class Config {
}
}
定义异常部分在前面的局部异常定义过,这里不再讲解
需要说明的是:如果网关转发的微服务宕机或者没有启动,那么全局过滤器是不会执行的。
13.跨域配置
通过自定义 GatewayFilter 定义过滤器,拦截请求,统一设置请求允许跨域
@Component
public class CrossGatewayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
HttpHeaders headers = response.getHeaders();
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS,"POST,GET,PUT,DELETE");
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "*");
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS,"*");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -100;
}
}
采用配置的方式(推荐)
spring:
application:
name: hospital-gateway1
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
# 允许携带认证信息
allow-credentials: true
# 允许跨域的源(网站域名/ip),设置*为全部
allowedOrigins: "*"
# 允许跨域的method, 默认为GET和OPTIONS,设置*为全部
allowedMethods: "*"
# 允许跨域请求里的head字段,设置*为全部
allowedHeaders: "*"
routes:
- id: sickroom微服务
uri: lb://sickroom-service
predicates:
- Path=/sickroom/**
filters:
- StripPrefix=1
- id: finance微服务
uri: lb://finance-service
predicates:
- Path=/finance/**
filters:
- StripPrefix=1
14.gateway网关的熔断降级
1.添加springcloud的hystrix启动器
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2.在gateway网关已经内置了局部的HystrixGatewayFilterFactory过滤器类,直接在要转发的某个微服务上面配置即可
spring:
application:
name: spring-service-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
globalcors:
corsConfigurations:
'[/**]':
# 允许携带认证信息
allow-credentials: true
# 允许跨域的源(网站域名/ip),设置*为全部
allowedOrigins: "*"
# 允许跨域的method, 默认为GET和OPTIONS,设置*为全部
allowedMethods: "*"
# 允许跨域请求里的head字段,设置*为全部
allowedHeaders: "*"
routes:
- id: b服务
uri: lb://spring-service-b
predicates:
- Path=/port/**
filters:
- name: Xxx
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: forward:/myfallback
#hystrix配置
hystrix:
command:
fallbackcmd:
execution:
isolation:
thread:
#断路器的超时时间ms,默认1000
timeoutInMilliseconds: 5000
circuitBreaker:
#是否启动熔断器,默认为true,false表示不要引入Hystrix。
enabled: true
#当在配置时间窗口内达到此数量的失败后,进行短路
requestVolumeThreshold: 20
#出错百分比阈值,当达到此阈值后,开始短路。默认50%)
errorThresholdPercentage: 50%
#短路多久以后开始尝试是否恢复,默认5s)-单位ms
sleepWindowInMilliseconds: 30000
3.请求接口定义
在gateway微服务中编写controller请求
@RestController
public class FallBackController {
@RequestMapping("/myfallback") //和上面的fallbackUri要对应
public Map<String,String> myFallback(){
Map<String,String> map = new HashMap<>();
map.put("Code","fail");
map.put("Message","服务暂时不可用,请稍候访问.");
return map;
}
}
说明:
1.HystrixGatewayFilterFactory过滤器内置配置类有很多参数,name: fallbackcmd(固定写法)
2.fallbackUri表示熔断后的降级请求地址
3.另外考虑到所有的接口都需要熔断降级,可以配置默认的全局过滤器
spring: cloud: gateway: default-filters: - name: Hystrix args: name: fallbackcmd fallbackUri: forward:/myfallback
4.token的校验也应该定义全局的过滤器
5.如果目前微服务正常响应,则不会有问题,如果微服务超时则执行熔断降级逻辑,然后响应,如果微服务直接报错,则熔断器HystrixGatewayFilterFactory不会管,ErrorWebHandlerException不会捕捉到这个异常。如果目标微服务报的异常做了处理,如
return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() { @SneakyThrows @Override public void run() { HttpStatus statusCode = exchange.getResponse().getStatusCode(); if (statusCode.value() == 500) { ErrorStatus error = new ErrorStatus(); error.setStatus(500); error.setMsg("服务器内部报错"); String errorMsg = new ObjectMapper().writeValueAsString(error); throw new RuntimeException(errorMsg); //网关直接抛异常 } else if (statusCode.value() == 404) { ErrorStatus error = new ErrorStatus(); error.setStatus(404); error.setMsg("url地址不合法"); String errorMsg = new ObjectMapper().writeValueAsString(error); throw new RuntimeException(errorMsg); ......
直接抛异常,则会引发HystrixGatewayFilterFactory的异常,因此ErrorWebHandlerException捕获到的异常就是HystrixGatewayFilterFactory抛出的,而不是我们定义的。需要单独处理。
spring: cloud: gateway: default-filters: - name: Hystrix args: name: fallbackcmd fallbackUri: forward:/myfallback
4.token的校验也应该定义全局的过滤器
5.如果目前微服务正常响应,则不会有问题,如果微服务超时则执行熔断降级逻辑,然后响应,如果微服务直接报错,则熔断器HystrixGatewayFilterFactory不会管,ErrorWebHandlerException不会捕捉到这个异常。如果目标微服务报的异常做了处理,如
return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() { @SneakyThrows @Override public void run() { HttpStatus statusCode = exchange.getResponse().getStatusCode(); if (statusCode.value() == 500) { ErrorStatus error = new ErrorStatus(); error.setStatus(500); error.setMsg("服务器内部报错"); String errorMsg = new ObjectMapper().writeValueAsString(error); throw new RuntimeException(errorMsg); //网关直接抛异常 } else if (statusCode.value() == 404) { ErrorStatus error = new ErrorStatus(); error.setStatus(404); error.setMsg("url地址不合法"); String errorMsg = new ObjectMapper().writeValueAsString(error); throw new RuntimeException(errorMsg); ......
直接抛异常,则会引发HystrixGatewayFilterFactory的异常,因此ErrorWebHandlerException捕获到的异常就是HystrixGatewayFilterFactory抛出的,而不是我们定义的。需要单独处理。