前言
-
咱们聊聊如何用
Spring Cloud Gateway
修改原始请求和响应内容,以及修改过程中遇到的问题 -
首先是修改请求
body
,如下图,浏览器是请求发起方,真实参数只有user-id
,经过网关时被塞入字段user-name
,于是,后台服务收到的请求就带有user-name
字段了 -
其次是修改响应,如下图,服务提供方
nacos-provider
的原始响应只有response-tag
字段,经过网关时被塞入了gateway-response-tag
字段,最终浏览器收到的响应就是response-tag
和gateway-response-tag
两个字段: -
总的来说,要做具体事情如下:
- 准备工作:在服务提供者的代码中新增一个
web
接口,用于验证Gateway
的操作是否有效 - 介绍修改请求
body
和响应body
的套路 - 按套路开发一个过滤器(
filter
),用于修改请求的body
- 按套路开发一个过滤器(
filter
),用于修改响应的body
- 思考和尝试:如何从
Gateway
返回错误?
- 在实战过程中,顺便搞清楚两个问题:
- 代码配置路由时,如何给一个路由添加多个
filter
? - 代码配置路由和
yml
配置是否可以混搭,两者有冲突吗?
准备工作
- 为了观察
Gateway
能否按预期去修改请求和响应的body
,咱们给服务提供者nacos-provider
增加一个接口,代码在NacosController.java
中,如下:
java
@PostMapping("/change")
public Map<String, Object> change(@RequestBody Map<String, Object> map) {
map.put("response-tag", dateStr());
return map;
}
- 新增的
web
接口很简单:将收到的请求数据作为返回值,在里面添加了一个键值对,然后返回给请求方,有了这个接口,咱们就能通过观察返回值来判断Gateway
对请求和响应的操作是否生效 - 先启动
nacos
(nacos-provider
需要的),再运行nacos-provider
应用,用Postman
向其发请求试试,如下图,符合预期: - 准备工作已完成,开始开发吧
修改请求body的套路
- 如何用
Spring Cloud Gateway
修改请求的body
?来看看其中的套路:
- 修改请求
body
是通过自定义filter
实现的 - 配置路由及其
filter
的时候,有yml
配置文件和代码配置两种方式可以配置路由,官方文档给出的demo是代码配置的,因此今天咱们也参考官方做法,通过代码来配置路由和过滤器 - 在代码配置路由的时候,调用
filters
方法,该方法的入参是个lambda
表达式 - 此
lambda
表达式固定调用modifyRequestBody
方法,咱们只要定义好modifyRequestBody
方法的三个入参即可 5.modifyRequestBody
方法的第一个入参是输入类型;第二个入参是返回类型;第三个是RewriteFunction
接口的实现,这个代码需要您自己写,内容是将输入数据转换为返回类型数据具体逻辑,咱们来看官方Demo,也就是上述套路了:
java
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("rewrite_request_obj", r -> r.host("*.rewriterequestobj.org")
.filters(f -> f.prefixPath("/httpbin")
.modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE,
(exchange, s) -> return Mono.just(new Hello(s.toUpperCase())))).uri(uri))
.build();
}
修改响应body的套路
- 用
Spring Cloud Gateway
修改响应body
的套路和前面的请求body
如出一辙
- 通过代码来配置路由和过滤器
- 在代码配置路由的时候,调用
filters
方法,该方法的入参是个lambda
表达式 - 此
lambda
表达式固定调用modifyResponseBody
方法,咱们只要定义好modifyResponseBody
方法的三个入参即可 modifyRequestBody
方法的第一个入参是输入类型;第二个入参是返回类型;第三个是RewriteFunction接口的实现,这个代码要您自己写,内容是将输入数据转换为返回类型数据具体逻辑,咱们来看官方Demo,其实就是上述套路:
java
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("rewrite_response_upper", r -> r.host("*.rewriteresponseupper.org")
.filters(f -> f.prefixPath("/httpbin")
.modifyResponseBody(String.class, String.class,
(exchange, s) -> Mono.just(s.toUpperCase()))).uri(uri))
.build();
}
- 套路总结出来了,接下来开发?
按套路开发一个修改请求body的过滤器(filter)
- 在父工程
spring-cloud-learning
下新建子工程gateway-change-body
,pom.xml
无任何特殊之处,注意依赖spring-cloud-starter-gateway
即可
pom
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 启动类
GatewayChangeBodyApplication
:
java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayChangeBodyApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayChangeBodyApplication.class, args);
}
}
- 配置文件
application.yml
:
yml
server:
#服务端口
port: 10014
spring:
application:
name: gateway-change-body
- 然后是核心逻辑:修改请求
body
的代码,即RewriteFunction
的实现类,代码很简单,将原始的请求body
解析成Map
对象,取出user-id
字段,生成user-name
字段放回map
,apply
方法返回的是个Mono
:
java
package com.example.function;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Map;
@Slf4j
public class RequestBodyRewrite implements RewriteFunction<String, String> {
private ObjectMapper objectMapper;
public RequestBodyRewrite(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
/**
* 根据用户ID获取用户名称的方法,可以按实际情况来内部实现,例如查库或缓存,或者远程调用
* @param userId
* @return
*/
private String mockUserName(int userId) {
return "user-" + userId;
}
@Override
public Publisher<String> apply(ServerWebExchange exchange, String body) {
try {
Map<String, Object> map = objectMapper.readValue(body, Map.class);
// 取得id
int userId = (Integer)map.get("user-id");
// 得到nanme后写入map
map.put("user-name", mockUserName(userId));
// 添加一个key/value
map.put("gateway-request-tag", userId + "-" + System.currentTimeMillis());
return Mono.just(objectMapper.writeValueAsString(map));
} catch (Exception ex) {
log.error("1. json process fail", ex);
// json操作出现异常时的处理
return Mono.error(new Exception("1. json process fail", ex));
}
}
}
- 然后是按部就班的基于代码实现路由配置,重点是
lambda
表达式执行modifyRequestBody
方法,并且将RequestBodyRewrite
作为参数传入:
java
package com.example.config;
import com.example.function.RequestBodyRewrite;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public RouteLocator routes(RouteLocatorBuilder builder, ObjectMapper objectMapper) {
return builder
.routes()
.route("path_route_change",
r -> r.path("/nacos/change")
.filters(f -> f
.modifyRequestBody(String.class,String.class,new RequestBodyRewrite(objectMapper))
)
.uri("http://localhost:9001"))
.build();
}
}
- 代码写完了,运行工程
gateway-change-body
,在postman
发起请求,得到响应如下图,红框中可见Gateway
添加的内容已成功: - 现在修改请求
body
已经成功,接下来再来修改服务提供者响应的body
修改响应body
- 新增
RewriteFunction
接口的实现类ResponseBodyRewrite.java
java
package com.example.function;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Map;
@Slf4j
public class ResponseBodyRewrite implements RewriteFunction<String, String> {
private ObjectMapper objectMapper;
public ResponseBodyRewrite(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public Publisher<String> apply(ServerWebExchange exchange, String body) {
try {
Map<String, Object> map = objectMapper.readValue(body, Map.class);
// 取得id
int userId = (Integer)map.get("user-id");
// 添加一个key/value
map.put("gateway-response-tag", userId + "-" + System.currentTimeMillis());
return Mono.just(objectMapper.writeValueAsString(map));
} catch (Exception ex) {
log.error("2. json process fail", ex);
return Mono.error(new Exception("2. json process fail", ex));
}
}
}
- 路由配置代码中,
lambda
表达式里面,filters
方法内部调用modifyResponseBody
,第三个入参是ResponseBodyRewrite
:
java
package com.example.config;
import com.example.function.RequestBodyRewrite;
import com.example.function.ResponseBodyRewrite;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public RouteLocator routes(RouteLocatorBuilder builder, ObjectMapper objectMapper) {
return builder
.routes()
.route("path_route_change",
r -> r.path("/nacos/change")
.filters(f -> f
.modifyRequestBody(String.class,String.class,new RequestBodyRewrite(objectMapper))
.modifyResponseBody(String.class, String.class, new ResponseBodyRewrite(objectMapper))
)
.uri("http://localhost:9001"))
.build();
}
}
- 还记得咱们的第一个问题吗?通过上面的代码,您应该已经看到了答案:用代码配置路由时,多个过滤器的配置方法就是在
filters
方法中反复调用内置的过滤器相关API
,下图红框中的都可以: - 运行服务,用
Postman
验证效果,如下图红框,Gateway
在响应body中成功添加了一个key&value
:
代码配置路由和yml配置是否可以混搭?
- 前面有两个问题,接下来回答第二个,咱们在
application.yml
中增加一个路由配置:
yml
server:
#服务端口
port: 10014
spring:
application:
name: gateway-change-body
cloud:
gateway:
routes:
- id: path_route_str
uri: http://localhost:9001
predicates:
- Path=/nacos/test
- 重启
gateway-change-body
服务,此时已经有了两个路由配置,一个在代码中,一个在yml
中,先试试yml中的这个,如下图没问题: - 再试试代码配置的路由,如下图,结论是代码配置路由和yml配置可以 混搭
如何处理异常
- 还有个问题必须要面对:修改请求或者响应
body
的过程中,如果发现问题需要提前返回错误(例如必要的字段不存在),代码该怎么写? - 咱们修改请求body的代码集中在
RequestBodyRewrite.java
,增加下图红框内容: - 再来试试,这次请求参数中不包含
user-id
,收到Gateway
返回的错误信息如下图: - 看看控制台,能看到代码中抛出的异常信息:
- 此时,聪明的您应该发现问题所在了:咱们想告诉客户端具体的错误,但实际上客户端收到的是被
Gateway
框架处理后的内容
网关(Gateway)为什么要做这些?
- 为什么要破坏原始数据,一旦系统出了问题如何定位是服务提供方还是网关?
- 尽管网关会破坏原始数据,但只做一些简单固定的处理,一般以添加数据为主,网关不了解业务,最常见的就是鉴权、添加身份或标签等操作
- 前面的图中确实感受不到网关的作用,但如果网关后面有多个服务提供者,如下图,这时候诸如鉴权、获取账号信息等操作由网关统一完成,比每个后台分别实现一次更有效率,后台可以更加专注于自身业务:
- 网关统一鉴权、获取身份,一般会把身份信息放入请求的
header
中,也不会修改请求和响应的内容,本篇只是从技术上演示Spring Cloud Gateway
如何修改请求和响应内容,请不要将此技术与实际后台业务耦合;