https://github.com/zq2599/blog_demos
内容:所有原创文章分类汇总及配套源码,涉及Java、Docker、Kubernetes、DevOPS等;
作为《Spring Cloud Gateway实战》系列的第九篇,咱们聊聊如何用Spring Cloud Gateway修改原始请求和响应内容,以及修改过程中遇到的问题
首先是修改请求body,如下图,浏览器是请求发起方,真实参数只有user-id,经过网关时被塞入字段user-name,于是,后台服务收到的请求就带有user-name字段了
名称 | 链接 | 备注 |
---|---|---|
项目主页 | https://github.com/zq2599/blog_demos | 该项目在GitHub上的主页 |
git仓库地址(https) | https://github.com/zq2599/blog_demos.git | 该项目源码的仓库地址,https协议 |
git仓库地址(ssh) | git@github.com:zq2599/blog_demos.git | 该项目源码的仓库地址,ssh协议 |
@PostMapping("/change") public Map<String, Object> change(@RequestBody Map<String, Object> map) { map.put("response-tag", dateStr()); return map; }
可见新增的web接口很简单:将收到的请求数据作为返回值,在里面添加了一个键值对,然后返回给请求方,有了这个接口,咱们就能通过观察返回值来判断Gateway对请求和响应的操作是否生效
来试一下,先启动nacos(provider-hello需要的)
再运行provider-hello应用,用Postman向其发请求试试,如下图,符合预期:
@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(); }
@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(); }
废话不说,在父工程spring-cloud-tutorials下新建子工程gateway-change-body,pom.xml无任何特殊之处,注意依赖spring-cloud-starter-gateway即可
启动类毫无新意:
package com.bolingcavalry.changebody; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ChangeBodyApplication { public static void main(String[] args) { SpringApplication.run(ChangeBodyApplication.class,args); } }
server: #服务端口 port: 8081 spring: application: name: gateway-change-body
package com.bolingcavalry.changebody.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)); } } }
package com.bolingcavalry.changebody.config; import com.bolingcavalry.changebody.function.RequestBodyRewrite; import com.bolingcavalry.changebody.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; import org.springframework.http.MediaType; import reactor.core.publisher.Mono; @Configuration public class FilterConfig { @Bean public RouteLocator routes(RouteLocatorBuilder builder, ObjectMapper objectMapper) { return builder .routes() .route("path_route_change", r -> r.path("/hello/change") .filters(f -> f .modifyRequestBody(String.class,String.class,new RequestBodyRewrite(objectMapper)) ) .uri("http://127.0.0.1:8082")) .build(); } }
接下来开发修改响应body的代码
新增RewriteFunction接口的实现类ResponseBodyRewrite.java
package com.bolingcavalry.changebody.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)); } } }
package com.bolingcavalry.changebody.config; import com.bolingcavalry.changebody.function.RequestBodyRewrite; import com.bolingcavalry.changebody.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; import org.springframework.http.MediaType; import reactor.core.publisher.Mono; @Configuration public class FilterConfig { @Bean public RouteLocator routes(RouteLocatorBuilder builder, ObjectMapper objectMapper) { return builder .routes() .route("path_route_change", r -> r.path("/hello/change") .filters(f -> f .modifyRequestBody(String.class,String.class,new RequestBodyRewrite(objectMapper)) .modifyResponseBody(String.class, String.class, new ResponseBodyRewrite(objectMapper)) ) .uri("http://127.0.0.1:8082")) .build(); } }
server: #服务端口 port: 8081 spring: application: name: gateway-change-body cloud: gateway: routes: - id: path_route_str uri: http://127.0.0.1:8082 predicates: - Path=/hello/str
还有个问题必须要面对:修改请求或者响应body的过程中,如果发现问题需要提前返回错误(例如必要的字段不存在),代码该怎么写?
咱们修改请求body的代码集中在RequestBodyRewrite.java,增加下图红框内容:
此时,聪明的您应该发现问题所在了:咱们想告诉客户端具体的错误,但实际上客户端收到的是被Gateway框架处理后的内容
篇幅所限,上述问题从分析到解决的过程,就留给下一篇文章吧
本篇的最后,请容许欣宸唠叨两句,聊聊为何要网关来修改请求和响应body的内容,如果您没兴趣还请忽略
看过开篇的两个图,聪明的您一定发现了问题:为什么要破坏原始数据,一旦系统出了问题如何定位是服务提供方还是网关?
按照欣宸之前的经验,尽管网关会破坏原始数据,但只做一些简单固定的处理,一般以添加数据为主,网关不了解业务,最常见的就是鉴权、添加身份或标签等操作
前面的图中确实感受不到网关的作用,但如果网关后面有多个服务提供者,如下图,这时候诸如鉴权、获取账号信息等操作由网关统一完成,比每个后台分别实现一次更有效率,后台可以更加专注于自身业务:
经验丰富的您可能会对我的狡辩不屑一顾:网关统一鉴权、获取身份,一般会把身份信息放入请求的header中,也不会修改请求和响应的内容啊,欣宸前面的一堆解释还是没说清楚为啥要在网关位置修改请求和响应的内容!
好吧,面对聪明的您,我摊牌了:本篇只是从技术上演示Spring Cloud Gateway如何修改请求和响应内容,请不要将此技术与实际后台业务耦合;
我是欣宸,期待与您一同畅游Java世界…
https://github.com/zq2599/blog_demos