The Spring Cloud Gateway modifies the content of the request and response body

Welcome to my GitHub

https://github.com/zq2599/blog_demos

Content: classification and summary of all original articles and supporting source code, involving Java, Docker, Kubernetes, DevOPS, etc;

Overview of this article

  • As the ninth part of the Spring Cloud Gateway practice series, let's talk about how to use Spring Cloud Gateway to modify the original request and response content, as well as the problems encountered in the modification process
  • First, modify the request body. As shown in the figure below, the browser is the initiator of the request, and the real parameters are only < font color = "blue" > User ID < / font >, which is inserted into the field < font color = "blue" > user name < / font > when passing through the gateway. Therefore, the request received by the background service has a < font color = "blue" > user name < / font > field

  • The second is the modification response. As shown in the figure below, the original response of the service provider < font color = "blue" > provider Hello < / font > has only the < font color = "red" > response tag < / font > field, which is inserted into the < font color = "blue" > gateway response tag < / font > field when passing through the gateway. The final responses received by the browser are < font color = "red" > response tag < / font > and < font color = "blue" >Gateway response tag < / font > two fields:

  • In general, the specific things to be done today are as follows:
  1. Preparation: add a new web interface in the service provider's code to verify whether the Gateway operation is valid
  2. Introduce the routine of modifying the request body and the response body
  3. Develop a filter according to the routine to modify the requested body
  4. Develop a filter according to the routine to modify the body of the response
  5. Think and try: how to return errors from the Gateway?
  • In the course of actual combat, let's clarify two questions:
  1. How to add multiple filter s to a route when the code configures a route?
  2. Can code configuration routing and yml configuration be mixed and matched? Do they conflict?

Source download

  • The complete source code in this actual combat can be downloaded from GitHub. The address and link information are shown in the table below( https://github.com/zq2599/blo...):
namelinkremarks
Project Home https://github.com/zq2599/blo...The project is on the GitHub home page
git warehouse address (https)https://github.com/zq2599/blo...The warehouse address of the source code of the project, https protocol
git warehouse address (ssh)git@github.com:zq2599/blog_demos.gitThe project source code warehouse address, ssh protocol
  • There are multiple folders in this git project. The source code of this article is in the < font color = "blue" > spring cloud tutorials < / font > folder, as shown in the red box below:

  • There are several sub projects under the < font color = "blue" > spring cloud tutorials < / font > folder. The code of this chapter is < font color = "red" > gateway change body < / font >, as shown in the red box below:

preparation

  • In order to observe whether the Gateway can modify the request and response body as expected, we add an interface to the service provider < font color = "blue" > provider Hello < / font >, and the code is in Hello.java, as follows:
    @PostMapping("/change")
    public Map<String, Object> change(@RequestBody Map<String, Object> map) {
        map.put("response-tag", dateStr());
        return map;
    }
  • It can be seen that the new web interface is very simple: take the received request data as the return value, add a key value pair in it, and then return it to the requester. With this interface, we can judge whether the Gateway's operation on the request and response is effective by observing the return value
  • Let's try it. First start nacos (required by provider Hello)
  • Then run the < font color = "blue" > provider Hello < / font > Application and try sending a request to it with Postman, as shown in the figure below, which meets the expectation:

  • The preparatory work has been completed. Let's start development

Modify the routine of the request body

  • How to modify the body of a request with Spring Cloud Gateway? Let's take a look at the routine:
  1. The modification request body is implemented through a custom filter
  2. When configuring the route and its filter, there are two ways to configure the route: yml configuration file and code configuration. The demo given in the official document is code configuration. Therefore, today we also refer to the official practice to configure the route and filter through code
  3. When the code configures the route, call the < font color = "blue" > filters < / font > method. The input parameter of this method is a lambda expression
  4. This lambda expression calls the modifyRequestBody method. We just need to define the three input parameters of the modifyRequestBody method
  5. The first input parameter to the modifyRequestBody method is the input type
  6. The second input parameter is the return type
  7. The third is the implementation of the RewriteFunction interface. This code needs to be written by yourself. The content is the specific logic of converting input data into return type data. Let's look at the official Demo, which is the above routine:
@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();
}

Modify the routine of response body

  • The routine of modifying the response body with Spring Cloud Gateway is the same as the previous request body
  1. Configure routing and filters through code
  2. When the code configures the route, call the < font color = "blue" > filters < / font > method. The input parameter of this method is a lambda expression
  3. This lambda expression calls the modifyResponseBody method. We just need to define the three input parameters of the modifyResponseBody method
  4. The first input parameter to the modifyRequestBody method is the input type
  5. The second input parameter is the return type
  6. The third is the implementation of the RewriteFunction interface. You should write this code yourself. The content is to convert the input data into return type data. Let's look at the official Demo, which is actually the above routine:
@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();
}
  • The routine is summarized. Next, let's roll out the code together?

Develop a filter to modify the request body according to the routine

  • Needless to say, create a new sub project < font color = "red" > gateway change body < / font > under the parent project < font color = "blue" > spring cloud tutorials < / font >, pom.xml has nothing special, and pay attention to relying on < font color = "blue" > spring cloud starter gateway < / font >
  • The startup class is nothing new:
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);
    }
}
  • Configuration files are the same:
server:
  #Service port
  port: 8081
spring:
  application:
    name: gateway-change-body
  • Then comes the core logic: modify the code of the request body, that is, the implementation class of RewriteFunction. The code is very simple. Parse the original request body into a map object, take out the user ID field, generate the user name field and put it back into the map. The apply method returns a 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;
    }

    /**
     * The method of obtaining the user name according to the user ID can be implemented internally according to the actual situation, such as library lookup or cache, or remote call
     * @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);

            // Get id
            int userId = (Integer)map.get("user-id");

            // Write the map after obtaining the nanme
            map.put("user-name", mockUserName(userId));

            // Add a 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);
            // Handling of exception in json operation
            return Mono.error(new Exception("1. json process fail", ex));
        }
    }
}
  • Then, the routing configuration is implemented step by step based on the code. The key point is that the lambda expression executes the modifyRequestBody method and passes the RequestBodyRewrite as a parameter:
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();
    }
}
  • After the code is written, run the project < font color = "blue" > Gateway change body < / font >, initiate a request in postman and get a response, as shown in the figure below. It can be seen in the red box that the content added by Gateway has been successful:

  • Now that the modification of the request body has been successful, the next step is to modify the body responded by the service provider

Modify response body

  • Next, develop the code to modify the response body
  • Added the implementation class ResponseBodyRewrite.java of the RewriteFunction interface
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);

            // Get id
            int userId = (Integer)map.get("user-id");

            // Add a 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));
        }
    }
}
  • In the routing configuration code, in the lambda expression, the filters method calls modifyResponseBody internally, and the third input parameter is 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();
    }
}
  • Remember our first question? Through the above code, you should have seen the answer: when configuring routes with code, the configuration method of multiple filters is to repeatedly call the built-in filter related API s in the filters method. All in the red box in the following figure can be:

  • Run the service and verify the effect with Postman, as shown in the red box below. Gateway successfully added a key & value in the response body:

Can code configuration routing and yml configuration be mixed?

  • To answer the second question, let's add a routing configuration in application.yml:
server:
  #Service port
  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
  • Start the < font color = "blue" > gateway change body < / font > service. At this time, there are already two routing configurations, one in the code and the other in yml. Try this in yml first, as shown in the figure below

  • Try the code configured route again, as shown in the figure below. The conclusion is that < font color = "red" > code configured route and yml configuration can be mixed and matched < / font >

How to handle exceptions

  • There is another problem that must be faced: in the process of modifying the request or response body, if an error needs to be returned in advance (for example, the necessary field does not exist), how should the code be written?
  • Let's focus on request body rewrite.java and add the following red box content:

  • Try again. The request parameters do not contain < font color = "blue" > User ID < / font >, and the error message returned by the Gateway is as follows:

  • Look at the console and you can see the exception information thrown in the code:

  • At this point, smart you should find the problem: we want to tell the client about the specific error, but in fact, the client receives the content processed by the Gateway framework
  • Due to space constraints, the process from analysis to solution of the above problems will be left to the next article
  • At the end of this article, please allow Xinchen to nag about why the gateway is used to modify the content of the request and response body. If you are not interested, please ignore it

Why does the Gateway do this?

  • After reading the two figures at the beginning, you must have found the problem: why destroy the original data? Once the system has a problem, how to locate the service provider or gateway?
  • According to Xinchen's previous experience, although the gateway will destroy the original data, it only does some simple and fixed processing. Generally, it mainly adds data. The gateway does not understand the business. The most common operations are authentication, adding identity or label
  • The role of the gateway is really not felt in the previous figure, but if there are multiple service providers behind the gateway, as shown in the following figure, at this time, operations such as authentication and obtaining account information are uniformly completed by the gateway, which is more efficient than each background, and the background can pay more attention to its own business:

  • Experienced you may despise my sophistry: the gateway uniformly authenticates and obtains identity. Generally, it will put the identity information into the request header and will not modify the content of the request and response. The pile of explanations in front of Xinchen still don't make it clear why it is necessary to modify the content of the request and response at the gateway position!
  • Well, in the face of smart you, I have a showdown: This article is only a technical demonstration of how Spring Cloud Gateway modifies the request and response content. Please do not couple this technology with the actual background business;

You're not alone. Xinchen's original accompanies you all the way

  1. Java series
  2. Spring collection
  3. Docker series
  4. kubernetes series
  5. Database + middleware series
  6. DevOps series

Welcome to the official account: programmer Xin Chen

Wechat search "programmer Xinchen", I'm Xinchen, looking forward to traveling with you in the Java World
https://github.com/zq2599/blog_demos

Keywords: cloud computing

Added by realjumper on Thu, 25 Nov 2021 00:32:55 +0200