小白学习spring-cloud(十二): Spring Cloud Gateway修改请求和响应body的内容

前言

  • 咱们聊聊如何用Spring Cloud Gateway修改原始请求和响应内容,以及修改过程中遇到的问题

  • 首先是修改请求body,如下图,浏览器是请求发起方,真实参数只有user-id,经过网关时被塞入字段user-name,于是,后台服务收到的请求就带有user-name字段了

  • 其次是修改响应,如下图,服务提供方nacos-provider的原始响应只有response-tag字段,经过网关时被塞入了gateway-response-tag字段,最终浏览器收到的响应就是response-taggateway-response-tag两个字段:

  • 总的来说,要做具体事情如下:

  1. 准备工作:在服务提供者的代码中新增一个web接口,用于验证Gateway的操作是否有效
  2. 介绍修改请求body和响应body的套路
  3. 按套路开发一个过滤器(filter),用于修改请求的body
  4. 按套路开发一个过滤器(filter),用于修改响应的body
  5. 思考和尝试:如何从Gateway返回错误?
  • 在实战过程中,顺便搞清楚两个问题:
  1. 代码配置路由时,如何给一个路由添加多个filter
  2. 代码配置路由和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对请求和响应的操作是否生效
  • 先启动nacosnacos-provider需要的),再运行nacos-provider应用,用Postman向其发请求试试,如下图,符合预期:
  • 准备工作已完成,开始开发吧

修改请求body的套路

  • 如何用Spring Cloud Gateway修改请求的body?来看看其中的套路:
  1. 修改请求body是通过自定义filter实现的
  2. 配置路由及其filter的时候,有yml配置文件和代码配置两种方式可以配置路由,官方文档给出的demo是代码配置的,因此今天咱们也参考官方做法,通过代码来配置路由和过滤器
  3. 在代码配置路由的时候,调用filters方法,该方法的入参是个lambda表达式
  4. 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如出一辙
  1. 通过代码来配置路由和过滤器
  2. 在代码配置路由的时候,调用filters方法,该方法的入参是个lambda表达式
  3. lambda表达式固定调用modifyResponseBody方法,咱们只要定义好modifyResponseBody方法的三个入参即可
  4. 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-bodypom.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字段放回mapapply方法返回的是个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如何修改请求和响应内容,请不要将此技术与实际后台业务耦合;
相关推荐
wno70419 小时前
Spring Cloud Hystrix Dashboard仪表盘
spring·spring cloud·hystrix
BUG指挥官1 天前
Redis已接入AI
数据库·人工智能·spring boot·redis·spring cloud
wno7041 天前
Spring Cloud Hystrix服务容错
spring·spring cloud·hystrix
苏渡苇2 天前
Spring Insight 里上报 Span 为什么用 JDK HttpClient 而不是 RestTemplate
java·后端·spring·spring cloud·springboot·性能监控
YOU OU3 天前
Spring Cloud Consul
spring cloud·consul·java-consul
重生之我是Java开发战士3 天前
【Spring Cloud】Eureka服务注册发现与LoadBalancer负载均衡
spring cloud·eureka·负载均衡
谢亮_vipxieliang3 天前
ValidX vs Google Guava Preconditions:验证 vs 断言
java·spring boot·后端·spring cloud·hibernate·guava
夏天拐跑了西瓜4 天前
Spring Cloud 微服务实战(八):Hystrix熔断降级——从服务雪崩到断路器模式
spring cloud·hystrix·微服务
重生之我是Java开发战士5 天前
【Spring Cloud】认识微服务与工程搭建
spring·spring cloud·微服务
墨着染霜华6 天前
SpringCloud Gateway线上Nginx代理后自定义下划线请求头丢失(app_name/app_version获取不到)完美解决
nginx·spring cloud·gateway