关于云计算:Spring-Cloud-Gateway修改请求和响应body的内容

44次阅读

共计 9585 个字符,预计需要花费 24 分钟才能阅读完成。

欢送拜访我的 GitHub

https://github.com/zq2599/blog_demos

内容:所有原创文章分类汇总及配套源码,波及 Java、Docker、Kubernetes、DevOPS 等;

本篇概览

  • 作为《Spring Cloud Gateway 实战》系列的第九篇,咱们聊聊如何用 Spring Cloud Gateway 批改原始申请和响应内容,以及批改过程中遇到的问题
  • 首先是批改申请 body,如下图,浏览器是申请发起方,实在参数只有 <font color=”blue”>user-id</font>,通过网关时被塞入字段 <font color=”blue”>user-name</font>,于是,后盾服务收到的申请就带有 <font color=”blue”>user-name</font> 字段了

  • 其次是批改响应,如下图,服务提供方 <font color=”blue”>provider-hello</font> 的原始响应只有 <font color=”red”>response-tag</font> 字段,通过网关时被塞入了 <font color=”blue”>gateway-response-tag</font> 字段,最终浏览器收到的响应就是 <font color=”red”>response-tag</font> 和 <font color=”blue”>gateway-response-tag</font> 两个字段:

  • 总的来说,明天要做具体事件如下:
  1. 筹备工作:在服务提供者的代码中新增一个 web 接口,用于验证 Gateway 的操作是否无效
  2. 介绍批改申请 body 和响应 body 的套路
  3. 按套路开发一个过滤器(filter),用于批改申请的 body
  4. 按套路开发一个过滤器(filter),用于批改响应的 body
  5. 思考和尝试:如何从 Gateway 返回谬误?
  • 在实战过程中,咱们顺便搞清楚两个问题:
  1. 代码配置路由时,如何给一个路由增加多个 filter?
  2. 代码配置路由和 yml 配置是否能够混搭,两者有抵触吗?

源码下载

  • 本篇实战中的残缺源码可在 GitHub 下载到,地址和链接信息如下表所示(https://github.com/zq2599/blo…):
名称 链接 备注
我的项目主页 https://github.com/zq2599/blo… 该我的项目在 GitHub 上的主页
git 仓库地址(https) https://github.com/zq2599/blo… 该我的项目源码的仓库地址,https 协定
git 仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该我的项目源码的仓库地址,ssh 协定
  • 这个 git 我的项目中有多个文件夹,本篇的源码在 <font color=”blue”>spring-cloud-tutorials</font> 文件夹下,如下图红框所示:

  • <font color=”blue”>spring-cloud-tutorials</font> 文件夹下有多个子工程,本篇的代码是 <font color=”red”>gateway-change-body</font>,如下图红框所示:

筹备工作

  • 为了察看 Gateway 是否按预期去批改申请和响应的 body,咱们给服务提供者 <font color=”blue”>provider-hello</font> 减少一个接口,代码在 Hello.java 中,如下:
    @PostMapping("/change")
    public Map<String, Object> change(@RequestBody Map<String, Object> map) {map.put("response-tag", dateStr());
        return map;
    }
  • 可见新增的 web 接口很简略:将收到的申请数据作为返回值,在外面增加了一个键值对,而后返回给申请方,有了这个接口,咱们就能通过观察返回值来判断 Gateway 对申请和响应的操作是否失效
  • 来试一下,先启动 nacos(provider-hello 须要的)
  • 再运行 <font color=”blue”>provider-hello</font> 利用,用 Postman 向其发申请试试,如下图,合乎预期:

  • 筹备工作已实现,开始开发吧

批改申请 body 的套路

  • 如何用 Spring Cloud Gateway 批改申请的 body?来看看其中的套路:
  1. 批改申请 body 是通过自定义 filter 实现的
  2. 配置路由及其 filter 的时候,有 yml 配置文件和代码配置两种形式能够配置路由,官网文档给出的 demo 是代码配置的,因而明天咱们也参考官网做法,通过代码来配置路由和过滤器
  3. 在代码配置路由的时候,调用 <font color=”blue”>filters</font> 办法,该办法的入参是个 lambda 表达式
  4. 此 lambda 表达式固定调用 modifyRequestBody 办法,咱们只有定义好 modifyRequestBody 办法的三个入参即可
  5. modifyRequestBody 办法的第一个入参是输出类型
  6. 第二个入参是返回类型
  7. 第三个是 RewriteFunction 接口的实现,这个代码须要您本人写,内容是将输出数据转换为返回类型数据具体逻辑,咱们来看官网 Demo,也就是上述套路了:
@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 一模一样
  1. 通过代码来配置路由和过滤器
  2. 在代码配置路由的时候,调用 <font color=”blue”>filters</font> 办法,该办法的入参是个 lambda 表达式
  3. 此 lambda 表达式固定调用 modifyResponseBody 办法,咱们只有定义好 modifyResponseBody 办法的三个入参即可
  4. modifyRequestBody 办法的第一个入参是输出类型
  5. 第二个入参是返回类型
  6. 第三个是 RewriteFunction 接口的实现,这个代码要您本人写,内容是将输出数据转换为返回类型数据具体逻辑,咱们来看官网 Demo,其实就是上述套路:
@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)

  • 废话不说,在父工程 <font color=”blue”>spring-cloud-tutorials</font> 下新建子工程 <font color=”red”>gateway-change-body</font>,pom.xml 无任何非凡之处,留神依赖 <font color=”blue”>spring-cloud-starter-gateway</font> 即可
  • 启动类毫无新意:
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
  • 而后是外围逻辑:批改申请 body 的代码,既 RewriteFunction 的实现类,代码很简略,将原始的申请 body 解析成 Map 对象,取出 user-id 字段,生成 user-name 字段放回 map,apply 办法返回的是个 Mono:
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));
        }
    }
}
  • 而后是循序渐进的基于代码实现路由配置,重点是 lambda 表达式执行 modifyRequestBody 办法,并且将 RequestBodyRewrite 作为参数传入:
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();}
}
  • 代码写完了,运行工程 <font color=”blue”>gateway-change-body</font>,在 postman 发动申请,失去响应如下图,红框中可见 Gateway 增加的内容已胜利:

  • 当初批改申请 body 曾经胜利,接下来再来批改服务提供者响应的 body

批改响应 body

  • 接下来开发批改响应 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));
        }
    }
}
  • 路由配置代码中,lambda 表达式外面,filters 办法外部调用 modifyResponseBody,第三个入参是 ResponseBodyRewrite:
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();}
}
  • 还记得咱们的第一个问题吗?通过下面的代码,您应该曾经看到了答案:用代码配置路由时,多个过滤器的配置办法就是在 filters 办法中重复调用内置的过滤器相干 API,下图红框中的都能够:

  • 运行服务,用 Postman 验证成果,如下图红框,Gateway 在响应 body 中胜利增加了一个 key&value:

代码配置路由和 yml 配置是否能够混搭?

  • 后面有两个问题,接下来答复第二个,咱们在 application.yml 中减少一个路由配置:
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
  • 把 <font color=”blue”>gateway-change-body</font> 服务启动起来,此时曾经有了两个路由配置,一个在代码中,一个在 yml 中,先试试 yml 中的这个,如下图没问题:

  • 再试试代码配置的路由,如下图,论断是 <font color=”red”> 代码配置路由和 yml 配置 能够 混搭 </font>

如何解决异样

  • 还有个问题必须要面对:批改申请或者响应 body 的过程中,如果发现问题须要提前返回谬误(例如必要的字段不存在),代码该怎么写?
  • 咱们批改申请 body 的代码集中在 RequestBodyRewrite.java,减少下图红框内容:

  • 再来试试,这次申请参数中不蕴含 <font color=”blue”>user-id</font>,收到 Gateway 返回的错误信息如下图:

  • 看看控制台,能看到代码中抛出的异样信息:

  • 此时,聪慧的您应该发现问题所在了:咱们想通知客户端具体的谬误,但实际上客户端收到的是被 Gateway 框架解决后的内容
  • 篇幅所限,上述问题从剖析到解决的过程,就留给下一篇文章吧
  • 本篇的最初,请答应欣宸唠叨两句,聊聊为何要网关来批改申请和响应 body 的内容,如果您没趣味还请疏忽

网关(Gateway)为什么要做这些?

  • 看过开篇的两个图,聪慧的您肯定发现了问题:为什么要毁坏原始数据,一旦零碎出了问题如何定位是服务提供方还是网关?
  • 依照欣宸之前的教训,只管网关会毁坏原始数据,但只做一些简略固定的解决,个别以增加数据为主,网关不理解业务,最常见的就是鉴权、增加身份或标签等操作
  • 后面的图中的确感触不到网关的作用,但如果网关前面有多个服务提供者,如下图,这时候诸如鉴权、获取账号信息等操作由网关对立实现,比每个后盾别离实现一次更有效率,后盾能够更加专一于本身业务:

  • 经验丰富的您可能会对我的诡辩等闲视之:网关对立鉴权、获取身份,个别会把身份信息放入申请的 header 中,也不会批改申请和响应的内容啊,欣宸后面的一堆解释还是没说分明为啥要在网关地位批改申请和响应的内容!
  • 好吧,面对聪慧的您,我摊牌了:本篇只是从技术上演示 Spring Cloud Gateway 如何批改申请和响应内容,请不要将此技术与理论后盾业务耦合;

你不孤独,欣宸原创一路相伴

  1. Java 系列
  2. Spring 系列
  3. Docker 系列
  4. kubernetes 系列
  5. 数据库 + 中间件系列
  6. DevOps 系列

欢送关注公众号:程序员欣宸

微信搜寻「程序员欣宸」,我是欣宸,期待与您一起畅游 Java 世界 …
https://github.com/zq2599/blog_demos

正文完
 0