3W words to understand: the underlying principles and practical operations of the microservice gateway SpringCloud gateway

40-year-old architect Nien's heartfelt heart:

It is very difficult to get an offer now , and I can't even get a call for an interview.

In Nien's technical community (50+), many small partners have obtained offers by virtue of the three unique skills of "left-handed cloud native + right-handed big data + SpringCloud Alibaba micro-services", and they are very high-quality offers. It is said that the year-end bonus is enough . 18 months , very enviable.

The problem is: "left-handed cloud native + right-handed big data + SpringCloud Alibaba microservices" has a lot of content, the actual operation environment is very complex, and the underlying principles are very deep .

The rice should be eaten one bite at a time, not in a hurry . Here, from the perspective of an architect, Nien will give a macro introduction to the core principles of left-handed cloud native + right-handed big data + SpringCloud Alibaba microservices.

Since the content is indeed too much, write multiple pdf e-books:

(1) "Docker Study Bible" PDF (V1 has been completed)

(2) "SpringCloud Alibaba Microservice Study Bible" PDF (V4 has been completed)

(3) "K8S Study Bible" PDF (V1 has been completed)

(4) "Go Study Bible" PDF (coding ...)

The above-mentioned Bible study will continue to be upgraded in the future, and it will be released iteratively from the V1 version. Just like our " Nin Java Interview Collection ", it has been iterated to V68.

40-year-old veteran architect Nien's heartfelt heart: Through a series of study bibles, we will lead you through "left-handed cloud native + right-handed big data + SpringCloud Alibaba micro-services", to achieve technological freedom, to subvert life, so that everyone will not get lost.

The basic knowledge of the above study Bible is Nien's "High Concurrency Trilogy". It is recommended that before reading the study Bible, you must go through Nien's Java High Concurrency Trilogy, remember, remember.

Version Upgrade Instructions

This article is an upgraded version of the gateway part of "SpringCloud Alibaba Study Bible".

On the basis of the original book, about 3W words have been improved and revised.

The latest content has been integrated into the "SpringCloud Alibaba Study Bible" V4 version PDF.

The latest and complete PDF can be obtained from the official account [Technical Freedom Circle] at the end of the article.

This article directory

Article directory

1. Introduction to Spring Cloud Gateway

The gateway is a key role in the microservice architecture, used to protect, enhance and control access to microservices.

The gateway is a system before the application or service, which is used to manage authorization, access control and traffic restriction, etc., so that the microservice will be protected by the microservice gateway and transparent to all callers. Therefore, the business system hidden behind the microservice gateway can focus more on the business itself. At the same time, the microservice gateway can also provide and deposit more additional functions for services.

The following are the main functions of the microservice gateway:

Examples of such API gateways include:

  • Spring Cloud Gateway
  • Solo.io Glow
  • Netflix Zuul
  • IBM-Strongloop Loopback/Microgateway

SpringCloud Gateway is a brand new project of Spring Cloud. The project is a gateway developed based on technologies such as Spring 5.0, Spring Boot 2.0 and Project Reactor. It aims to provide a simple and effective unified API routing management method for microservice architecture.

As a gateway in the Spring Cloud ecosystem, SpringCloud Gateway aims to replace Zuul. In Spring Cloud 2.0 and above, it does not integrate the latest high-performance versions of Zuul 2.0 and above, and still uses the non-Reactor mode before Zuul 2.0. old version of . In order to improve the performance of the gateway, SpringCloud Gateway is implemented based on the WebFlux framework, and the bottom layer of the WebFlux framework uses the high-performance Reactor mode communication framework Netty.

The goal of Spring Cloud Gateway is not only to provide a unified routing method, but also to provide basic functions of the gateway based on the Filter chain, such as: security, monitoring/indicators, and current limiting.

Special note :
The bottom layer of Spring Cloud Gateway uses Netty, a high-performance communication framework.

Netty is the communication base of high-performance middleware. Rocketmq, seata, nacos, sentinel, redission, dubbo and many other famous middleware are all based on netty without exception.

It is no exaggeration to say: netty is a necessary skill to enter a large factory and go to the high end .

If you want to understand springcloud gateway in depth, it is best to master netty programming .

For details about netty learning, please refer to the best-selling book published by Jigongshe and Nien: " Java High Concurrency Core Programming Volume 1 Enhanced Edition "

1.1 This article's companion article "Flux and Mono, reactor actual combat (the most complete in history)"

In addition, if you want to master the filter programming of spring cloud gateway, or master the development of spring cloud gateway, you must also:
have the actual combat ability of reactor responsive programming

For the actual combat of reactor responsive programming, please refer to the companion article of this article:

"Flux and Mono, reactor actual combat (the most complete in history)"

1.2 Spring Cloud Gateway Features

SpringCloud officially introduces the features of SpringCloud Gateway as follows:

(1) Based on Spring Framework 5, Project Reactor and Spring Boot 2.0

(2) Integrated Hystrix circuit breaker

(3) Integrate Spring Cloud Discovery Client

(4) Predicates and Filters act on specific routes, easy to write Predicates and Filters

(5) With some advanced functions of the gateway: dynamic routing, current limiting, path rewriting

From the above characteristics, it is not much different from Zuul's characteristics. The main difference between SpringCloud Gateway and Zuul lies in the underlying communication framework.

A brief explanation of the three terms above:

(1) Filter (filter) :

Similar in concept to Zuul's filter, it can be used to intercept and modify requests, and perform secondary processing on upstream responses. Filters are instances of the org.springframework.cloud.gateway.filter.GatewayFilter class.

(2) Route (routing) :

The basic component module of gateway configuration is similar to Zuul's routing configuration module. A Route module is defined by an ID, a target URI, a set of assertions and a set of filters. If the assertion is true, the route matches and the target URI is accessed.

(3) Predicate :

This is a Java 8 Predicate that can be used to match anything from an HTTP request, such as headers or parameters. Assert that the input parameter type is a ServerWebExchange.

1.3 Spring Cloud Gateway and architecture

Spring ushered in Webflux in the second half of 2017. The emergence of Webflux filled the gap in Spring's responsive programming. Webflux's responsive programming is not only a change in programming style, but also provides a response to a series of well-known frameworks. Access-based development kits, such as Netty, Redis, etc.

The reactor-netty responsive programming component in Webflux used by SpringCloud Gateway uses the Netty communication framework at the bottom.

1.3.1 IO model of Spring Cloud Zuul

The Zuul version integrated in Springcloud uses the Tomcat container and uses the traditional Servlet IO processing model.

As we all know, servlets are managed by the servlet container for life cycle. When the container starts, construct the servlet object and call servlet init() to initialize; when the container is closed, call servlet destory() to destroy the servlet; when the container runs, it accepts the request and allocates a thread for each request (usually obtains an idle thread from the thread pool) Then call service().

Disadvantages: servlet is a simple network IO model. When a request enters the servlet container, the servlet container will bind a thread to it. This model is applicable in scenarios where the concurrency is not high, but once the concurrency increases, the number of threads It will rise, and the cost of thread resources is expensive (online text switching, large memory consumption) which seriously affects the processing time of requests. In some simple business scenarios, you don't want to allocate a thread for each request, and only need one or a few threads to deal with extremely concurrent requests. In this business scenario, the servlet model has no advantage.

So Springcloud Zuul is a blocking processing model based on servlets, that is, spring implements a servlet (DispatcherServlet) that handles all requests, and is processed by the servlet blocking processing. So Springcloud Zuul cannot get rid of the disadvantages of the servlet model. Although Zuul 2.0 started using Netty, and there are already mature cases of large-scale Zuul 2.0 cluster deployment, Springcloud has no official plan to integrate and change the version.

1.3.2 Webflux server

The Webflux pattern replaces the old Servlet threading model. Use a small number of threads to process request and response io operations. These threads are called Loop threads, and the business is handed over to the responsive programming framework. Responsive programming is very flexible. Users can submit blocked operations in the business to the responsive framework. The non-blocking operations can still be processed in the Loop thread, which greatly improves the utilization of the Loop thread. Official structure diagram:

Although Webflux is compatible with multiple underlying communication frameworks, in general, the underlying layer uses Netty. After all, Netty is currently the highest performance communication framework recognized by the industry. The Loop thread of Webflux happens to be the Reactor thread of the famous Reactor mode IO processing model. If the high-performance communication framework Netty is used, this is the EventLoop thread of Netty.

The knowledge about the Reactor threading model and the Netty communication framework is an important and necessary internal skill for Java programmers. For details, please refer to the book "Netty, Zookeeper, Redis High Concurrency Practical Combat" edited by Nien, here Do not repeat too much.

1.3.3 Processing flow of Spring Cloud Gateway

The client makes a request to Spring Cloud Gateway. Then find the route that matches the request in Gateway Handler Mapping and send it to Gateway Web Handler. Handler then sends the request to our actual service to execute business logic through the specified filter chain, and then returns. Filters are separated by dotted lines because filters may perform business logic before ("pre") or after ("post") the proxy request is sent.

2. Routing configuration method

2.1 Basic URI routing configuration method

If the target address of the request is a single URI resource path, the configuration file example is as follows:

server:
  port: 8080
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: url-proxy-1
          uri: https://blog.csdn.net
          predicates:
            - Path=/csdn

The meaning of each field is as follows:

id: our custom routing ID, keep it unique

uri: target service address

predicates: Routing conditions, Predicate accepts an input parameter and returns a boolean result. This interface contains various default methods to combine Predicates into other complex logic (such as: AND, OR, NOT).

The above configuration means that a URI proxy rule with an id of url-proxy-1 is configured, and the routing rule is:

When accessing the address http://localhost:8080/csdn/1.jsp,

will be routed to the upstream address https://blog.csdn.net/1.jsp.

2.2 Code-based routing configuration

The forwarding function can also be implemented through code, we can add the method customRouteLocator() in the startup class GateWayApplication to customize the forwarding rules.

package com.springcloud.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class GatewayApplication {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(GatewayApplication.class, args);
    }

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    
    
        return builder.routes()
                .route("path_route", r -> r.path("/csdn")
                        .uri("https://blog.csdn.net"))
                .build();
    }
}

We log out the relevant routing configuration in the yaml configuration file, restart the service, visit the link: http://localhost:8080/csdn, and you can see the same page as above, which proves that our test is successful.

In the above two examples, the uri points to my CSDN blog. In actual project use, the uri can be pointed to the address of the project that provides external services, and the external output interface is unified.

2.3 Routing configuration combined with the registration center

The schema protocol part of uri is a custom lb: type, which means subscribing to a service from a microservice registry (such as Eureka) and routing the service.

A typical example is as follows:

server:
  port: 8084
spring:
  cloud:
    gateway:
      routes:
      - id: seckill-provider-route
        uri: lb://seckill-provider
        predicates:
        - Path=/seckill-provider/**

      - id: message-provider-route
        uri: lb://message-provider
        predicates:
        - Path=/message-provider/**

application:
  name: cloud-gateway

eureka:
  instance:
    prefer-ip-address: true
  client:
    service-url:
      defaultZone: http://localhost:8888/eureka/

The routing configuration combined with the registration center is actually very different from the routing configuration of a single URI, except that the schema protocol of the URI is different. The schema protocol of the address of a single URI, usually http or https protocol.

3. Routing matching rules

The function of Spring Cloud Gateway is very powerful. We can see it only through the design of Predicates. We just used predicates for simple condition matching. In fact, Spring Cloud Gateway has built many Predicates functions for us.

Spring Cloud Gateway uses Spring WebFlux's HandlerMapping as the underlying support to match forwarding routes. Spring Cloud Gateway has many built-in Predicates factories. These Predicates factories are matched by different HTTP request parameters, and multiple Predicates factories can be used in combination.

One of the main functions of gateWay is to forward requests, and the definition of forwarding rules mainly includes three parts

Route Routing is the basic unit of a gateway, consisting of ID, URI, a set of Predicates, and a set of Filters, and is matched and forwarded according to Predicates.
Predicate (predicate, assertion) Judgment conditions for routing and forwarding. Currently SpringCloud Gateway supports a variety of methods, such as: Path, Query, Method, Header, etc. The writing method must follow the form of key=vlue
Filter The filter is the filtering logic that the route passes through when forwarding the request, which can be used to modify the content of the request and response

Among them, Route and Predicate must be declared at the same time

example:

//通过配置文件配置
spring:
  cloud:
    gateway:
      routes:
        - id: gate_route
          uri: http://localhost:9023
          predicates:
          ## 当请求的路径为gate、rule开头的时,转发到http://localhost:9023服务器上
            - Path=/gate/**,/rule/**
        ### 请求路径前加上/app
          filters:
          - PrefixPath=/app

Most of the configurations in this article have been verified in the demo project, and there may be format errors during the writing process

If you have a problem, you can find Nin

3.1 Predicate assertion condition (forwarding rule) introduction

Predicate comes from Java 8 and is a function introduced in Java 8. Predicate accepts an input parameter and returns a Boolean result. This interface contains various default methods to combine Predicates into other complex logic (such as: AND, OR, NOT). It can be used for interface request parameter verification, and to determine whether there are changes in old and new data that need to be updated.

In Spring Cloud Gateway, Spring utilizes the characteristics of Predicate to implement various routing matching rules, and different conditions such as Header and request parameters are used as conditions to match to the corresponding routing. There is a picture on the Internet that summarizes the implementation of several Predicates built into Spring Cloud.

Spring Cloud Gateway

To put it bluntly, Predicate is to implement a set of matching rules, so that the request can be found to find the corresponding Route for processing. Next, we will take over the use of several built-in Predicates in Spring Cloud GateWay.

  • Forwarding rules (predicates), assuming that the forwarding uri is set tohttp://localhost:9023
rule example illustrate
Path - Path=/gate/,/rule/ ## When the requested path starts with gate and rule, forward it to http://localhost:9023 server
Before - Before=2017-01-20T17:42:47.789-07:00[America/Denver] Requests before a certain time will be forwarded to the http://localhost:9023 server
After - After=2017-01-20T17:42:47.789-07:00[America/Denver] Requests after a certain time will be forwarded
Between - Between=2017-01-20T17:42:47.789-07:00[America/Denver],2017-01-21T17:42:47.789-07:00[America/Denver] Only between a certain period of time will be forwarded
Cookie - Cookie=chocolate, ch.p A form named chocolate or a form that satisfies the regular ch.p will be matched for request forwarding
Header - Header=X-Request-Id, \d+ The request header that carries the parameter X-Request-Id or satisfies \d+ will match
Host - Host=www.hd123.com When the host name is www.hd123.com, it is directly forwarded to the http://localhost:9023 server
Method - Method=GET Only the GET method will match the forwarding request, and you can also limit the request methods such as POST and PUT

3.1.1 Matching by request parameters

Query Route Predicate supports passing in two parameters, one is the attribute name and the other is the attribute value, and the attribute value can be a regular expression.

server:
  port: 8080
spring:
  cloud:
    gateway:
      routes:
      - id: query_route
        uri: https://example.org
        predicates:
        - Query=smile

With this configuration, as long as the request contains parameters of the smile attribute, the route can be matched.

Use curl to test, command line input:

curl localhost:8080?smile=x&id=2

After testing, it is found that as long as the request summary has the smile parameter, the route will be matched, and the route will not be matched without the smile parameter.

You can also configure the value of Query in the form of key-value pairs, so that when the request comes in, the attribute value and the regular expression will be matched, and the route will be followed if the match is made.

server:
  port: 8080
spring:
  cloud:
    gateway:
      routes:
      - id: query_route
        uri: https://example.org
        predicates:
            - Query=keep, pu.

In this way, only when the request contains the keep attribute and the parameter value is a three-digit string starting with pu will it be matched and routed.

Use curl to test, command line input:

curl localhost:8080?keep=pub

The test can return the page code, change the attribute value of keep to pubx, and it will report 404 when it is accessed again, which proves that the routing needs to match the regular expression before routing.

3.1.2 Matching by Header attribute

The Header Route Predicate is the same as the Cookie Route Predicate, it also receives 2 parameters, a header attribute name and a regular expression, and the attribute value matches the regular expression and is executed.

server:
  port: 8080
spring:
  cloud:
    gateway:
      routes:
      - id: query_route
        uri: https://example.org
        predicates:
            - Header=X-Request-Id, \d+

Use curl to test, command line input:

curl http://localhost:8080 -H "X-Request-Id:88"

Then return the page code to prove that the matching is successful. Change the parameter -H "X-Request-Id:88" to -H "X-Request-Id:spring" and return 404 to prove that there is no match when executed again.

3.1.3 Matching via cookies

Cookie Route Predicate can receive two parameters, one is Cookie name, and the other is regular expression. The routing rule will match by obtaining the corresponding Cookie name value and regular expression. If there is a match, the route will be executed. If there is no match, then Not implemented.

server:
  port: 8080
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        -id: gateway-service
          uri: https://www.baidu.com
          order: 0
          predicates:
            - Cookie=sessionId, test

Use curl to test, command line input:

curl http://localhost:8080 --cookie "sessionId=test"

The page code will be returned. If the –cookie “sessionId=test” is removed, a 404 error will be reported in the background.

3.1.4 Matching by Host

Host Route Predicate receives a set of parameters, a list of matching domain names, this template is an ant-separated template, with . as the separator. It passes the host address in the parameter as a matching rule.

server:
  port: 8080
spring:
  cloud:
    gateway:
      routes:
      - id: query_route
        uri: https://example.org
        predicates:
          - Host=**.baidu.com

Use curl to test, command line input:

curl http://localhost:8080 -H "Host: www.baidu.com"

curl http://localhost:8080 -H "Host: md.baidu.com"

After testing, the above two hosts can match the host_route route, and if the host parameter is removed, a 404 error will be reported.

3.1.5 Match by request

Routing can be done through different request methods such as POST, GET, PUT, DELETE, etc.

server:
  port: 8080
spring:
  cloud:
    gateway:
      routes:
      - id: query_route
        uri: https://example.org
        predicates:
            - Method=GET

Use curl to test, command line input:

# curl defaults to GET to request

curl http://localhost:8080

The test returns the page code, which proves that the route is matched, and we request the test in the form of POST.

# curl defaults to GET to request

curl -X POST http://localhost:8080

Return 404 not found, which proves that there is no matching route

3.1.6 Matching by request path

Path Route Predicate receives a matching path parameter to determine whether to follow the route.

server:
  port: 8080
spring:
  cloud:
    gateway:
      routes:
      - id: query_route
        uri: https://example.org
        predicates:
            -Path=/foo/{
    
    segment}

This route will match if the request path matches, eg: /foo/1 or /foo/bar.

Use curl to test, command line input:

curl http://localhost:8080/foo/1

curl http://localhost:8080/foo/xx

curl http://localhost:8080/boo/xx

After testing, the first and second commands can get the return value of the page normally, and the last command reports 404, which proves that the route is matched by the specified route.

3.1.7 Match by requesting ip address

Predicate also supports routing by setting a certain ip interval number segment request. RemoteAddr Route Predicate accepts a list of cidr symbol (IPv4 or IPv6) strings (minimum size is 1), such as 192.168.0.1/16 (where 192.168.0.1 is the IP address and 16 is the subnet mask).

server:
  port: 8080
spring:
  cloud:
    gateway:
      routes:
      - id: query_route
        uri: https://example.org
        predicates:
            - RemoteAddr=192.168.1.1/24

You can set this address as the ip address of the machine for testing.

curl localhost:8080

This route will match if the requested remote address is 192.168.1.10.

3.1.8 Combined use

server:
  port: 8080
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: gateway-service
          uri: https://www.baidu.com
          order: 0
          predicates:
            - Host=**.foo.org
            - Path=/headers
            - Method=GET
            - Header=X-Request-Id, \d+
            - Query=foo, ba.
            - Query=baz
            - Cookie=chocolate, ch.p

When various Predicates exist in the same route at the same time, the request must meet all the conditions at the same time to be matched by this route.

When a request meets the assertion conditions of multiple routes, the request will only be forwarded by the first successfully matched route

3.2 Filter rules (Filter)

Filter Rules (Filter)

filter rules example illustrate
PrefixPath - PrefixPath=/app Add app before the request path
RewritePath - RewritePath=/test, /app/test Visit localhost:9022/test, the request will be forwarded to localhost:8001/app/test
SetPath SetPath=/app/{path} Set the path through the template, the forwarding rule will add app before the path, {path} means the original request path
RedirectTo redirect
RemoveRequestHeader Remove a certain request header information

Note: When configuring multiple filters, the one defined first will be called, and the remaining filters will not take effect

PrefixPath

Prefix all request paths:

spring:
  cloud:
    gateway:
      routes:
      - id: prefixpath_route
        uri: https://example.org
        filters:
        - PrefixPath=/mypath

A request to visit /hello is sent to https://example.org/mypath/hello.

RedirectTo

Redirection, the configuration contains the return code and address of redirection:

spring:
  cloud:
    gateway:
      routes:
      - id: prefixpath_route
        uri: https://example.org
        filters:
        - RedirectTo=302, https://acme.org

RemoveRequestHeader

Remove a certain request header information:

spring:
  cloud:
    gateway:
      routes:
      - id: removerequestheader_route
        uri: https://example.org
        filters:
        - RemoveRequestHeader=X-Request-Foo

Remove the request header information X-Request-Foo

RemoveResponseHeader

Remove a certain receipt header information:

spring:
  cloud:
    gateway:
      routes:
      - id: removerequestheader_route
        uri: https://example.org
        filters:
        - RemoveResponseHeader=X-Request-Foo

RemoveRequestParameter

Remove a request parameter information:

spring:
  cloud:
    gateway:
      routes:
      - id: removerequestparameter_route
        uri: https://example.org
        filters:
        - RemoveRequestParameter=red

RewritePath

Rewrite path:

spring:
  cloud:
    gateway:
      routes:
      - id: rewrite_filter
        uri: http://localhost:8081
        predicates:
        - Path=/test/**
        filters:
        - RewritePath=/where(?<segment>/?.*), /test(?<segment>/?.*)

SetPath

Set the request path, similar to RewritePath.

spring:
  cloud:
    gateway:
      routes:
      - id: setpath_route
        uri: https://example.org
        predicates:
        - Path=/red/{
    
    segment}
        filters:
        - SetPath=/{
    
    segment}

For example, requests for /red/blue are forwarded to /blue.

SetRequestHeader

Set request header information.

spring:
  cloud:
    gateway:
      routes:
      - id: setrequestheader_route
        uri: https://example.org
        filters:
        - SetRequestHeader=X-Request-Red, Blue

SetStatus

Set the receipt status code.

spring:
  cloud:
    gateway:
      routes:
      - id: setstatusint_route
        uri: https://example.org
        filters:
        - SetStatus=401

StripPrefix

Skip the specified path.

spring:
  cloud:
    gateway:
      routes:
      - id: nameRoot
        uri: https://nameservice
        predicates:
        - Path=/name/**
        filters:
        - StripPrefix=2

A request for /name/blue/red will be forwarded to /red.

RequestSize

request size.

spring:
  cloud:
    gateway:
      routes:
      - id: request_size_route
        uri: http://localhost:8080/upload
        predicates:
        - Path=/upload
        filters:
        - name: RequestSize
          args:
            maxSize: 5000000

Requests exceeding 5M will return a 413 error.

Default-filters

Add filters to all requests.

spring:
  cloud:
    gateway:
      default-filters:
      - AddResponseHeader=X-Response-Default-Red, Default-Blue
      - PrefixPath=/httpbin

3.3 Configuration via code

To configure through code, set the routing rule as a Bean:

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    
    
        return builder.routes()
            .route("path_route", r -> r.path("/get")
                .uri("http://httpbin.org"))
            .route("host_route", r -> r.host("*.myhost.org")
                .uri("http://httpbin.org"))
            .route("rewrite_route", r -> r.host("*.rewrite.org")
                .filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${
    
    segment}"))
                .uri("http://httpbin.org"))
            .route("hystrix_route", r -> r.host("*.hystrix.org")
                .filters(f -> f.hystrix(c -> c.setName("slowcmd")))
                .uri("http://httpbin.org"))
            .route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
                .filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
                .uri("http://httpbin.org"))
            .route("limit_route", r -> r
                .host("*.limited.org").and().path("/anything/**")
                .filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
                .uri("http://httpbin.org"))
            .build();
    }

3.4 Realize fuse downgrade

Why implement circuit breaker downgrade?

In a distributed system, the gateway is the entrance of traffic, so a large number of requests will enter the gateway and initiate calls to other services. Other services will inevitably fail to call (timeouts, exceptions). When failures occur, requests cannot be accumulated on the gateway. On the Internet, it needs to fail quickly and return to the client. To achieve this requirement, it is necessary to perform fuse and downgrade operations on the gateway.

Why does request failure on the gateway need to be returned quickly to the client?

Because when a client request fails, the request will always be piled up on the gateway. Of course, there is only one such request, and the gateway must have no problem (if one request can cause the entire system to paralyze, then the system can be taken off the shelves) , but if there is too much accumulation on the gateway, it will cause huge pressure on the gateway and even the entire service, and even the entire service will go down. Therefore, some services and pages should be downgraded strategically to relieve the pressure on server resources to ensure the normal operation of the core business, and at the same time maintain the correct response of customers and most customers, so the request on the gateway is required Failures need to be returned quickly to the client.

server.port: 8082

spring:
  application:
    name: gateway
  redis:
      host: localhost
      port: 6379
      password: 123456
  cloud:
    gateway:
      routes:
        - id: rateLimit_route
          uri: http://localhost:8000
          order: 0
          predicates:
            - Path=/test/**
          filters:
            - StripPrefix=1
            - name: Hystrix
              args:
                name: fallbackCmdA
                fallbackUri: forward:/fallbackA

  hystrix.command.fallbackCmdA.execution.isolation.thread.timeoutInMilliseconds: 5000

In the configuration here, two filters are used:

(1) The filter StripPrefix is ​​used to remove the first n parts of the request path and intercept them.

StripPrefix=1 means that the number of intercepted paths is 1. For example, if the front-end comes to request /test/good/1/view, after the matching is successful, the request path routed to the back-end will become http://localhost:8888/good/1/view.

(2) Filter Hystrix, the function is to fuse and downgrade through Hystrix

When the upstream request enters the Hystrix fuse downgrade mechanism, the downgrade address configured by fallbackUri will be called. It should be noted that the timeout period of Hystrix's commandKey also needs to be set separately

The code of the fallback address configured by fallbackUri is as follows:

@RestController
public class FallbackController {
    
    

  @GetMapping("/fallbackA")
  public Mono<Void> fallbackA(ServerWebExchange exchange) {
    
    
    exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
    ServerHttpResponse serverHttpResponse = exchange.getResponse();
    serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
    RestOut<String> stringMasResponse = RestOut.error("服务不可用");
    byte[] jsonBytes = JsonUtil.object2JsonBytes(stringMasResponse);
    DataBuffer buffer = serverHttpResponse.bufferFactory().wrap(jsonBytes);
    return serverHttpResponse.writeWith(Mono.just(buffer));
  }
}

4. Detailed explanation of core classes

ServerWebExchange

Notes to look at first ServerWebExchange:

Contract for an HTTP request-response interaction.

Provides access to the HTTP request and response and also exposes additional server-side processing related properties and features such as request attributes.

The translation is probably:

ServerWebExchange is a contract for HTTP request-response interactions.

Provides access to HTTP requests and responses, and exposes additional server-side processing-related properties and features, such as request attributes.

In fact, ServerWebExchangeit is named as a service network switch , which stores important request-response attributes, request instances and response instances, etc., which is a bit like Contexta role.

ServerWebExchange interface

ServerWebExchangeAll methods of the interface:

public interface ServerWebExchange {
    
    

    // 日志前缀属性的KEY,值为org.springframework.web.server.ServerWebExchange.LOG_ID
    // 可以理解为 attributes.set("org.springframework.web.server.ServerWebExchange.LOG_ID","日志前缀的具体值");
    // 作用是打印日志的时候会拼接这个KEY对饮的前缀值,默认值为""
    String LOG_ID_ATTRIBUTE = ServerWebExchange.class.getName() + ".LOG_ID";
    String getLogPrefix();

    // 获取ServerHttpRequest对象
    ServerHttpRequest getRequest();

    // 获取ServerHttpResponse对象
    ServerHttpResponse getResponse();

    // 返回当前exchange的请求属性,返回结果是一个可变的Map
    Map<String, Object> getAttributes();

    // 根据KEY获取请求属性
    @Nullable
    default <T> T getAttribute(String name) {
    
    
        return (T) getAttributes().get(name);
    }

    // 根据KEY获取请求属性,做了非空判断
    @SuppressWarnings("unchecked")
    default <T> T getRequiredAttribute(String name) {
    
    
        T value = getAttribute(name);
        Assert.notNull(value, () -> "Required attribute '" + name + "' is missing");
        return value;
    }

     // 根据KEY获取请求属性,需要提供默认值
    @SuppressWarnings("unchecked")
    default <T> T getAttributeOrDefault(String name, T defaultValue) {
    
    
        return (T) getAttributes().getOrDefault(name, defaultValue);
    } 

    // 返回当前请求的网络会话
    Mono<WebSession> getSession();

    // 返回当前请求的认证用户,如果存在的话
    <T extends Principal> Mono<T> getPrincipal();  

    // 返回请求的表单数据或者一个空的Map,只有Content-Type为application/x-www-form-urlencoded的时候这个方法才会返回一个非空的Map -- 这个一般是表单数据提交用到
    Mono<MultiValueMap<String, String>> getFormData();   

    // 返回multipart请求的part数据或者一个空的Map,只有Content-Type为multipart/form-data的时候这个方法才会返回一个非空的Map  -- 这个一般是文件上传用到
    Mono<MultiValueMap<String, Part>> getMultipartData();

    // 返回Spring的上下文
    @Nullable
    ApplicationContext getApplicationContext();   

    // 这几个方法和lastModified属性相关
    boolean isNotModified();
    boolean checkNotModified(Instant lastModified);
    boolean checkNotModified(String etag);
    boolean checkNotModified(@Nullable String etag, Instant lastModified);

    // URL转换
    String transformUrl(String url);    

    // URL转换映射
    void addUrlTransformer(Function<String, String> transformer); 

    // 注意这个方法,方法名是:改变,这个是修改ServerWebExchange属性的方法,返回的是一个Builder实例,Builder是ServerWebExchange的内部类
    default Builder mutate() {
    
    
         return new DefaultServerWebExchangeBuilder(this);
    }

    interface Builder {
    
          

        // 覆盖ServerHttpRequest
        Builder request(Consumer<ServerHttpRequest.Builder> requestBuilderConsumer);
        Builder request(ServerHttpRequest request);

        // 覆盖ServerHttpResponse
        Builder response(ServerHttpResponse response);

        // 覆盖当前请求的认证用户
        Builder principal(Mono<Principal> principalMono);

        // 构建新的ServerWebExchange实例
        ServerWebExchange build();
    }
}    

ServerWebExchange#mutate() method

Noting ServerWebExchange#mutate()methods, ServerWebExchangeinstances can be understood as immutable instances,

If we want to modify it, we need to mutate()generate a new instance through the method, for example:

public class CustomGlobalFilter implements GlobalFilter {
    
    

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
        ServerHttpRequest request = exchange.getRequest();
        // 这里可以修改ServerHttpRequest实例
        ServerHttpRequest newRequest = ...
        ServerHttpResponse response = exchange.getResponse();
        // 这里可以修改ServerHttpResponse实例
        ServerHttpResponse newResponse = ...
        // 构建新的ServerWebExchange实例
        ServerWebExchange newExchange = exchange.mutate().request(newRequest).response(newResponse).build();
        return chain.filter(newExchange);
    }
}

ServerHttpRequest interface

ServerHttpRequestThe instance is used to carry the attributes and request body related to the request.

Spring Cloud GatewayThe middle and lower layers are used Nettyto process network requests, and by tracing the source code,

You can learn ReactorHttpHandlerAdapterfrom it what the specific implementation of the instance ServerWebExchangeheld in the instance is .ServerHttpRequestReactorServerHttpRequest

The relationship between these instances is listed because it is easier to clarify some hidden problems, such as:

  • ReactorServerHttpRequestAbstractServerHttpRequestWhen initializing the internal attribute headers in the parent class , the HTTP header of the request is encapsulated as a read-only instance :
public AbstractServerHttpRequest(URI uri, @Nullable String contextPath, HttpHeaders headers) {
    
    
    this.uri = uri;
    this.path = RequestPath.parse(uri, contextPath);
    this.headers = HttpHeaders.readOnlyHttpHeaders(headers);
}

// HttpHeaders类中的readOnlyHttpHeaders方法,
// ReadOnlyHttpHeaders屏蔽了所有修改请求头的方法,直接抛出UnsupportedOperationException
public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) {
    
    
    Assert.notNull(headers, "HttpHeaders must not be null");
    if (headers instanceof ReadOnlyHttpHeaders) {
    
    
        return headers;
    }
    else {
    
    
        return new ReadOnlyHttpHeaders(headers);
    }
}

Therefore, the request header instance cannot be ServerHttpRequestobtained directly from the instance HttpHeadersand modified .

ServerHttpRequestThe interface is as follows:

public interface HttpMessage {
    
    

    // 获取请求头,目前的实现中返回的是ReadOnlyHttpHeaders实例,只读
    HttpHeaders getHeaders();
}    

public interface ReactiveHttpInputMessage extends HttpMessage {
    
    

    // 返回请求体的Flux封装
    Flux<DataBuffer> getBody();
}

public interface HttpRequest extends HttpMessage {
    
    

    // 返回HTTP请求方法,解析为HttpMethod实例
    @Nullable
    default HttpMethod getMethod() {
    
    
        return HttpMethod.resolve(getMethodValue());
    }

    // 返回HTTP请求方法,字符串
    String getMethodValue();    

    // 请求的URI
    URI getURI();
}    

public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage {
    
    

    // 连接的唯一标识或者用于日志处理标识
    String getId();   

    // 获取请求路径,封装为RequestPath对象
    RequestPath getPath();

    // 返回查询参数,是只读的MultiValueMap实例
    MultiValueMap<String, String> getQueryParams();

    // 返回Cookie集合,是只读的MultiValueMap实例
    MultiValueMap<String, HttpCookie> getCookies();  

    // 远程服务器地址信息
    @Nullable
    default InetSocketAddress getRemoteAddress() {
    
    
       return null;
    }

    // SSL会话实现的相关信息
    @Nullable
    default SslInfo getSslInfo() {
    
    
       return null;
    }  

    // 修改请求的方法,返回一个建造器实例Builder,Builder是内部类
    default ServerHttpRequest.Builder mutate() {
    
    
        return new DefaultServerHttpRequestBuilder(this);
    } 

    interface Builder {
    
    

        // 覆盖请求方法
        Builder method(HttpMethod httpMethod);

        // 覆盖请求的URI、请求路径或者上下文,这三者相互有制约关系,具体可以参考API注释
        Builder uri(URI uri);
        Builder path(String path);
        Builder contextPath(String contextPath);

        // 覆盖请求头
        Builder header(String key, String value);
        Builder headers(Consumer<HttpHeaders> headersConsumer);

        // 覆盖SslInfo
        Builder sslInfo(SslInfo sslInfo);

        // 构建一个新的ServerHttpRequest实例
        ServerHttpRequest build();
    }         
}    

Notice:

ServerHttpRequestOr HttpMessagethe method of obtaining the request header provided by the interfaceHttpHeaders getHeaders();

The returned result is a read-only instance, specifically ReadOnlyHttpHeadersthe type,

If you want to modify ServerHttpRequestthe instance, then you need to do this:

ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest newRequest = request.mutate().header("key","value").path("/myPath").build();

ServerHttpResponse interface

ServerHttpResponseInstances are used to carry response-related attributes and response bodies.

Spring Cloud GatewayThe middle and lower layers are used to Nettyprocess network requests. By tracing the source code, ReactorHttpHandlerAdapteryou know the specific implementation of the instance ServerWebExchangeheld in the instance .ServerHttpResponseReactorServerHttpResponse

The relationship between these instances is listed because it is easier to clarify some hidden problems, such as:

// ReactorServerHttpResponse的父类
public AbstractServerHttpResponse(DataBufferFactory dataBufferFactory, HttpHeaders headers) {
    
    
    Assert.notNull(dataBufferFactory, "DataBufferFactory must not be null");
    Assert.notNull(headers, "HttpHeaders must not be null");
    this.dataBufferFactory = dataBufferFactory;
    this.headers = headers;
    this.cookies = new LinkedMultiValueMap<>();
}

public ReactorServerHttpResponse(HttpServerResponse response, DataBufferFactory bufferFactory) {
    
    
    super(bufferFactory, new HttpHeaders(new NettyHeadersAdapter(response.responseHeaders())));
    Assert.notNull(response, "HttpServerResponse must not be null");
    this.response = response;
}

It can be seen that ReactorServerHttpResponsewhen the constructor initializes the instance, it is HttpHeadersthe instance that stores the response Header, that is, the response Header can be directly modified.

ServerHttpResponseThe interface is as follows:

public interface HttpMessage {
    
    

    // 获取响应Header,目前的实现中返回的是HttpHeaders实例,可以直接修改
    HttpHeaders getHeaders();
}  

public interface ReactiveHttpOutputMessage extends HttpMessage {
    
    

    // 获取DataBufferFactory实例,用于包装或者生成数据缓冲区DataBuffer实例(创建响应体)
    DataBufferFactory bufferFactory();

    // 注册一个动作,在HttpOutputMessage提交之前此动作会进行回调
    void beforeCommit(Supplier<? extends Mono<Void>> action);

    // 判断HttpOutputMessage是否已经提交
    boolean isCommitted();

    // 写入消息体到HTTP协议层
    Mono<Void> writeWith(Publisher<? extends DataBuffer> body);

    // 写入消息体到HTTP协议层并且刷新缓冲区
    Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body);

    // 指明消息处理已经结束,一般在消息处理结束自动调用此方法,多次调用不会产生副作用
    Mono<Void> setComplete();
}

public interface ServerHttpResponse extends ReactiveHttpOutputMessage {
    
    

    // 设置响应状态码
    boolean setStatusCode(@Nullable HttpStatus status);

    // 获取响应状态码
    @Nullable
    HttpStatus getStatusCode();

    // 获取响应Cookie,封装为MultiValueMap实例,可以修改
    MultiValueMap<String, ResponseCookie> getCookies();  

    // 添加响应Cookie
    void addCookie(ResponseCookie cookie);  
}    

Here you can see that except for the response body which is difficult to modify, other attributes are variable.

ServerWebExchangeUtils and context properties

ServerWebExchangeUtilsIt stores a lot of static public string KEY values

( The actual value of these string KEYs isorg.springframework.cloud.gateway.support.ServerWebExchangeUtils. + any static public KEY below),

These string KEY values ​​are generally used for the KEY of ServerWebExchangeattributes ( Attribute, see the method above ServerWebExchange#getAttributes()). These attribute values ​​have special meanings. When using filters, they can be taken out and used directly if the timing is appropriate. Let’s analyze them one by one .

  • PRESERVE_HOST_HEADER_ATTRIBUTE: Whether to save the Host attribute, the value is a Boolean value type, the writing position is PreserveHostHeaderGatewayFilterFactory, the used position is NettyRoutingFilter, the effect is that if it is set to true, the Host property in the HTTP request header will be written to the request Header property of the underlying Reactor-Netty.
  • CLIENT_RESPONSE_ATTR: Save the response object of the underlying Reactor-Netty, the type is reactor.netty.http.client.HttpClientResponse.
  • CLIENT_RESPONSE_CONN_ATTR: Save the connection object of the underlying Reactor-Netty, the type is reactor.netty.Connection.
  • URI_TEMPLATE_VARIABLES_ATTRIBUTE: PathRoutePredicateFactoryAfter parsing the path parameters is completed, store the placeholder KEY-path Path mapping after parsing in ServerWebExchangethe attribute, KEY is URI_TEMPLATE_VARIABLES_ATTRIBUTE.
  • CLIENT_RESPONSE_HEADER_NAMES: Save the name collection of the response Header of the underlying Reactor-Netty.
  • GATEWAY_ROUTE_ATTR: It is used to store RoutePredicateHandlerMappingthe specific routing ( ) instance matched in the process org.springframework.cloud.gateway.route.Route. Through this routing instance, you can know which downstream service the current request will be routed to.
  • GATEWAY_REQUEST_URL_ATTR: java.net.URIAn instance of the type, which represents the real URI that needs to be requested to the downstream service after direct request or load balancing processing.
  • GATEWAY_ORIGINAL_REQUEST_URL_ATTR: java.net.URIAn instance of the type, when the request URI needs to be rewritten, the original request URI is saved.
  • GATEWAY_HANDLER_MAPPER_ATTR: Save the type abbreviation of the currently used HandlerMappingconcrete instance (usually the string "RoutePredicateHandlerMapping").
  • GATEWAY_SCHEME_PREFIX_ATTR: Determine if there is a schemeSpecificPart attribute in the destination routing URI, then save the scheme of the URI in this attribute, and the routing URI will be reconstructed, see RouteToRequestUrlFilter.
  • GATEWAY_PREDICATE_ROUTE_ATTR: It is used to store the ID of RoutePredicateHandlerMappingthe specific routing ( ) instance matched in .org.springframework.cloud.gateway.route.Route
  • WEIGHT_ATTR: An experimental function (this version is not recommended to be used in the official version) to store group weight related attributes, see WeightCalculatorWebFilter.
  • ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR: Store the value of ContentType in the response header.
  • HYSTRIX_EXECUTION_EXCEPTION_ATTR: ThrowableAn instance, which stores the exception instance when Hystrix executes an exception, see HystrixGatewayFilterFactory.
  • GATEWAY_ALREADY_ROUTED_ATTR: Boolean value, used to determine whether routing has been performed, see NettyRoutingFilter.
  • GATEWAY_ALREADY_PREFIXED_ATTR: Boolean value, used to determine whether the request path has been prepended, see PrefixPathGatewayFilterFactory.

ServerWebExchangeUtilsThe provided context attributes are used for the secure transmission and use of some important internal instances or identification attributes when components process requests and responses. There may be certain risks in using them Spring Cloud Gateway.ServerWebExchange

Because no one can be sure whether the original attribute KEY or VALUE will change after the version upgrade. If the risk is assessed or avoided, you can use it with peace of mind.

For example, we can rely on it when doing request and response logs (similar to Nginx's Access Log), GATEWAY_ROUTE_ATTRbecause we want to print the target information of the route. **Give a simple example:

@Slf4j
@Component
public class AccessLogFilter implements GlobalFilter {
    
    

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().pathWithinApplication().value();
        HttpMethod method = request.getMethod();
        // 获取路由的目标URI
        URI targetUri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        InetSocketAddress remoteAddress = request.getRemoteAddress();
        return chain.filter(exchange.mutate().build()).then(Mono.fromRunnable(() -> {
    
    
            ServerHttpResponse response = exchange.getResponse();
            HttpStatus statusCode = response.getStatusCode();
            log.info("请求路径:{},客户端远程IP地址:{},请求方法:{},目标URI:{},响应码:{}",
                    path, remoteAddress, method, targetUri, statusCode);
        }));
    }
}

Gateway filter

Relationship between ServerWebExchange and filter:

Similar to zuul, Spring Cloud Gateway has two types of filters: "pre" and "post".

The client's request first passes through the "pre" type filter, and then forwards the request to a specific business service. After receiving the response from the business service, it is processed by the "post" type filter, and finally returns the response to the client.

Quoting a picture on the official website of Spring Cloud Gateway:

Different from zuul, in addition to filters divided into "pre" and "post", in Spring Cloud Gateway, filters can be divided into two other types from the scope of action.

One is a gateway filter for a single route , which is written in the configuration file similarly to predict;

One is the global gateway filer for all routes .

Now explain these two filters from the dimension of scope division.

When we use Spring Cloud Gatewayit, we pay attention to the filter (including GatewayFilter, GlobalFilterand filter chain GatewayFilterChain).

Spring Cloud Gateway is divided into GatewayFilter and GlobalFilter according to the scope of action. The differences between the two are as follows:

  • GatewayFilter : It needs to be configured under specific routes through spring.cloud.routes.filters, and only acts on the current route or configured globally through spring.cloud.default-filters, acting on all routes
  • GlobalFilter : The global filter does not need to be configured in the configuration file. It acts on all routes and is finally packaged into a filter recognizable by GatewayFilterChain through GatewayFilterAdapter. It converts the URI of the request business and the route into the request address of the real business service The core filter does not need to be configured. It is loaded when the system is initialized and acts on each route.

GlobalFilter

The GlobalFilter built into the Spring Cloud Gateway framework is as follows:

Each GlobalFilter in the above figure acts on each router, which can meet most requirements.

But if you encounter business customization, you may need to write a GlobalFilter that meets your needs.

Filters all depend on ServerWebExchange:

public interface GlobalFilter {
    
    

    Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}

public interface GatewayFilter extends ShortcutConfigurable {
    
    

    Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}

public interface GatewayFilterChain {
    
    

    Mono<Void> filter(ServerWebExchange exchange);
}    

The design here is similar to Servletthe one in ,Filter

The current filter can decide whether to execute the logic of the next filter, depending on GatewayFilterChain#filter()whether it is called.

It ServerWebExchangeis equivalent to the context of the current request and response.

ServerWebExchangeThe instance 不单stores Requestand Responseobjects, and also provides some extension methods. If you want to realize the modification of request parameters or response parameters, you must have a deep understanding ServerWebExchange.

Forward Routing Filter

To put it simply, this Filter is used for local forwarding. After translating the description of the official document, it is roughly as follows:

When a request comes in, ForwardRoutingFilterit will check a URL, which is the value ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTRof . If the scheme of the URL is forward(for example: forward://localendpoint), then the Filter will use Spirng's DispatcherHandlerto process the request. The URL path part of the request will be overwritten by the path in the forward URL. The original URL, which has not been modified, will be appended to ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTRthe attribute

Note: The so-called url scheme is simply the protocol part of the url, such as http, https, ws, etc. A custom scheme is usually used to identify the behavior of the url, for example, the url scheme is usually used to jump to the page in app development

Tips:

  • This document actually describes the implementation principle of the Filter, which is of little significance to users. If you are interested in the implementation principle, you can directly view the source code. The source code is easier to understand than the document description:org.springframework.cloud.gateway.filter.ForwardRoutingFilter
LoadBlancerClientFilter

This Filter is used to integrate the Ribbon. Its core is to parse lbthe url of the scheme to obtain the name of the microservice, and then obtain the actual calling address through the Ribbon. The translation of the official document description is roughly as follows:

When a request comes in, LoadBalancerClientFilterit will look at a URL, which is the value ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTRof . If the scheme of the URL is lb, (for example: lb://myservice), then the Filter will use Spring Cloud LoadBalancerClientto myserviceresolve to the actual host and port, and Replaces the value of the original ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTRproperty . Instead, the original url is appended to ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTRthe attribute . The filter also looks at ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTRthe attribute , and if it finds that the value of the attribute is lb, it executes the same logic.

Configuration example:

spring:
  cloud:
    gateway:
      routes:
      - id: myRoute
        uri: lb://service
        predicates:
        - Path=/service/**

By default, if the instance of the specified service cannot be LoadBalancerfound , then 503 will be returned (as in the above configuration example, if LoadBalancerno instance named service can be found, 503 will be returned); configuration can be used to spring.cloud.gateway.loadbalancer.use404=truemake it return 404.

LoadBalancerThe ServiceInstancereturned isSecurevalue of will override the requested scheme. For example, if the request hits the Gateway using HTTPS, ServiceInstancebut isSecurethe falseHTTP request is received by the downstream microservice, and vice versa. In addition, if the route specifies GATEWAY_SCHEME_PREFIX_ATTRthe attribute , the prefix will be stripped, and the scheme in the route URL will override ServiceInstancethe configuration of

Tips:

  • This document actually describes the implementation principle of the Filter, which is of little significance to users. If you are interested in the implementation principle, you can directly view the source code. The source code is easier to understand than the document description:org.springframework.cloud.gateway.filter.LoadBalancerClientFilter
NettyRoutingFilter

When a request comes in, NettyRoutingFilterit will check a URL, which is the value ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTRof . If the scheme of the URL is httpor https, then the Filter will use Netty's to send a proxy request HttpClientto the downstream service. The obtained response will be placed in ServerWebExchangeUtils.CLIENT_RESPONSE_ATTRthe property for use in the following Filter. (There is an experimental filter: WebClientHttpRoutingFilterdoes the same thing, but without Netty)

//必须保证是最后一个执行的过滤器
@Override
public int getOrder() {
    
    
    return Ordered.LOWEST_PRECEDENCE;
}

@Override
@SuppressWarnings("Duplicates")
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
    //获取请求url
    URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
    //获取请求模式
    String scheme = requestUrl.getScheme();
    //判断是否已经被路由过了,被路由过会添加标记
    if (isAlreadyRouted(exchange)//如果已经被路由过或者不是http和https就走其他过滤器
            || (!"http".equals(scheme) && !"https".equals(scheme))) {
    
    
        return chain.filter(exchange);
    }
    //标记为已经路由过,防止再次被其他过滤器使用
    setAlreadyRouted(exchange);
    //拿到请求
    ServerHttpRequest request = exchange.getRequest();
    //获取请求类型
    final HttpMethod method = HttpMethod.valueOf(request.getMethodValue());
    //处理URL地址编码,拿到想要的url
    final String url = requestUrl.toASCIIString();
    //拿到前置过滤器添加的header
    HttpHeaders filtered = filterRequest(getHeadersFilters(), exchange);
    //创建一个HttpHeaders对象,将所有的Header全都添加进去
    final DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders();
    filtered.forEach(httpHeaders::set);
    //根据exchange里的属性,判断是否要保留header里的host属性
    boolean preserveHost = exchange
            .getAttributeOrDefault(PRESERVE_HOST_HEADER_ATTRIBUTE, false);//PreserveHostHeader是否发送原始主机信息
    //拿到请求(路由)
    Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
    /*开始发送请求*/
    //使用HttpClient发送请求

    Flux<HttpClientResponse> responseFlux = getHttpClient(route, exchange)                .headers(headers -> {
    
    
            //将前置的Header带入之后的请求
                headers.add(httpHeaders);
                // Will either be set below, or later by Netty
                //删除Host
                headers.remove(HttpHeaders.HOST);
                //判断是否要保留Host
                if (preserveHost) {
    
    
                    //拿到Host
                    String host = request.getHeaders().getFirst(HttpHeaders.HOST);
                    //将Host添加到Header中
                    headers.add(HttpHeaders.HOST, host);
                }
            }).request(method).uri(url).send((req, nettyOutbound) -> {
    
    
                //判断Trace是否激活
                if (log.isTraceEnabled()) {
    
                            
                    nettyOutbound
                            .withConnection(connection -> log.trace("outbound route: "
                                    + connection.channel().id().asShortText()
                                    + ", inbound: " + exchange.getLogPrefix()));
                }
                //得到具体调用发送请求的位置
                return nettyOutbound.send(request.getBody().map(this::getByteBuf));
                //配置Response连接
            }).responseConnection((res, connection) -> {
    
    
                //NettyWriteResponseFilter就是通过这个连接拿到的数据
                // Defer committing the response until all route filters have run
                // Put client response as ServerWebExchange attribute and write
                // response later NettyWriteResponseFilter
                //将请求给到NettyWriteResponseFilter和连接
                exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res);                
                exchange.getAttributes().put(CLIENT_RESPONSE_CONN_ATTR, connection);
                //得到Response
                ServerHttpResponse response = exchange.getResponse();

                /*
                * 下边的代码就是发送请求了,Gateway的开发者已经给好注释了感觉没必要再去扒了
                */

                // put headers and status so filters can modify the response
                HttpHeaders headers = new HttpHeaders();

                res.responseHeaders().forEach(
                        entry -> headers.add(entry.getKey(), entry.getValue()));

                String contentTypeValue = headers.getFirst(HttpHeaders.CONTENT_TYPE);
                if (StringUtils.hasLength(contentTypeValue)) {
    
    
                    exchange.getAttributes().put(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR,
                            contentTypeValue);
                }

                setResponseStatus(res, response);

                // make sure headers filters run after setting status so it is
                // available in response
                HttpHeaders filteredResponseHeaders = HttpHeadersFilter.filter(
                        getHeadersFilters(), headers, exchange, Type.RESPONSE);

                if (!filteredResponseHeaders
                        .containsKey(HttpHeaders.TRANSFER_ENCODING)
                        && filteredResponseHeaders
                                .containsKey(HttpHeaders.CONTENT_LENGTH)) {
    
    
                    // It is not valid to have both the transfer-encoding header and
                    // the content-length header.
                    // Remove the transfer-encoding header in the response if the
                    // content-length header is present.
                    response.getHeaders().remove(HttpHeaders.TRANSFER_ENCODING);
                }

                exchange.getAttributes().put(CLIENT_RESPONSE_HEADER_NAMES,
                        filteredResponseHeaders.keySet());

                response.getHeaders().putAll(filteredResponseHeaders);

                return Mono.just(res);
            });

    Duration responseTimeout = getResponseTimeout(route);
    if (responseTimeout != null) {
    
    
        responseFlux = responseFlux
                .timeout(responseTimeout, Mono.error(new TimeoutException(
                        "Response took longer than timeout: " + responseTimeout)))
                .onErrorMap(TimeoutException.class,
                        th -> new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT,
                                th.getMessage(), th));
    }

    return responseFlux.then(chain.filter(exchange));
}
NettyWriteResponseFilter

NettyWriteResponseFilterIt is used to write the proxy response back to the client side of the gateway, so this filter will be executed after all other filters are executed, and the execution condition is that the value of ServerWebExchangeUtils.CLIENT_RESPONSE_CONN_ATTRthe attribute empty, and the value is Connectionan instance of Netty. (There is an experimental filter: WebClientWriteResponseFilterdoes the same thing, but without Netty)

RouteToRequestUrl Filter

This filter is used to convert the original url obtained from the request into the url used by Gateway for request forwarding. When a request comes in, RouteToRequestUrlFilterit will get the value of ServerWebExchangeUtils.GATEWAY_ROUTE_ATTRthe property , which is an Routeobject. If the object is not empty, a new URL RouteToRequestUrlFilterwill be created based on the request URL and the URL in Routethe object . The new URL will be placed in the exchange's ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTRproperties .

It may be a bit abstract, let's debug it and look at the values ​​​​of these three urls to understand, as shown in the following figure:

img

If the URL has a scheme prefix, for example lb:ws://serviceid, the lbscheme will be stripped from the URL and put ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTRin for later use by filters.

Websocket Routing Filter

This filter works NettyRoutingFiltersimilarly . When a request comes in, WebsocketRoutingFilterit will check a URL, which is the value of ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTRthe attribute . If the scheme of the URL is wsor wss, then the Filter will use Spring Web Socket to forward the Websocket request to the downstream.

Also, if Websocket requests need to be load balanced, URLs can be lbprefixed for load balancing, eg lb:ws://serviceid.

If you're using SockJS as a fallback to plain http, you should configure the normal HTTP route as well as the Websocket route.

Configuration example:

spring:
  cloud:
    gateway:
      routes:
      # SockJS route
      - id: websocket_sockjs_route
        uri: http://localhost:3001
        predicates:
        - Path=/websocket/info/**
      # Normwal Websocket route
      - id: websocket_route
        uri: ws://localhost:3001
        predicates:
        - Path=/websocket/**
Gateway Metrics Filter

To enable Gateway Metrics Filter, you need to add spring-boot-starter-actuatordependencies , and then configure spring.cloud.gateway.metrics.enabledthe value of in the configuration file true. This filter adds a timer metric gateway.requestsnamed with the following flags:

  • routeId: Route ID
  • routeUri: The URI the API will route to
  • outcome: Classified by HttpStatus.Series
  • status: Http Status returned to the client
  • httpStatusCode: Http Status of the request returned to the client
  • httpMethod: The Http method used by the request

These metrics are exposed in /actuator/metrics/gateway.requestsendpoints and can be easily integrated with Prometheus to create a Grafana dashboard .

Note: Prometheus is a monitoring tool, and Grafana is a monitoring visualization tool; Spring Boot Actuator can be integrated with these two tools.

Marking An Exchange As Routed

When a request goes through the entire filter chain, the filter responsible for forwarding the request to the downstream will add an gatewayAlreadyRoutedattribute , thus marking the exchange as routed(routed). Once a request is marked routed, other routing filters will not route the request again, but simply skip it.

After understanding all the built-in global filters above, we know that requests of different protocols will be forwarded downstream by different filters. gatewayAlreadyRoutedSo the filter responsible for adding this attribute is the one responsible for ultimately forwarding the request:

  • http, https requests will be added by NettyRoutingFilteror WebClientHttpRoutingFilterby this attribute
  • forward request will ForwardRoutingFilteradd this attribute by
  • websocket requests will be added by WebsocketRoutingFilteradding this property

These filters call the following methods to mark an exchange as routed, or check if an exchange is routed:

  • ServerWebExchangeUtils.isAlreadyRouted: Check whether the exchange is routed
  • ServerWebExchangeUtils.setAlreadyRouted: Set the exchange to routed state

To put it simply, Gateway uses gatewayAlreadyRoutedthe attribute to indicate that the request has been forwarded, without the need for other filters to repeat the route, thereby preventing repeated route forwarding.

These global filters have corresponding configuration classes. If you are interested, you can view the relevant source code:

  • org.springframework.cloud.gateway.config.GatewayAutoConfiguration
  • org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration
  • org.springframework.cloud.gateway.config.GatewayLoadBalancerClientAutoConfiguration

custom filter

For example, we can rely on it when doing request and response logs (similar to Nginx's Access Log), GATEWAY_ROUTE_ATTRbecause we want to print the target information of the route. **Give a simple example:

@Slf4j
@Component
public class AccessLogFilter implements GlobalFilter {
    
    

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().pathWithinApplication().value();
        HttpMethod method = request.getMethod();
        // 获取路由的目标URI
        URI targetUri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        InetSocketAddress remoteAddress = request.getRemoteAddress();
        return chain.filter(exchange.mutate().build()).then(Mono.fromRunnable(() -> {
    
    
            ServerHttpResponse response = exchange.getResponse();
            HttpStatus statusCode = response.getStatusCode();
            log.info("请求路径:{},客户端远程IP地址:{},请求方法:{},目标URI:{},响应码:{}",
                    path, remoteAddress, method, targetUri, statusCode);
        }));
    }
}
Modify the request body and response body
modify request body

Modifying the request body is a relatively common requirement.

For example, when we use Spring Cloud Gatewaythe implementation gateway, we need to implement a function:

After parsing the JWT stored in the request header, extract the user ID inside, and then write it into the request body.

Let's simplify this scenario, assuming that we store the userId plaintext in the accessToken in the request header, and the request body is a JSON structure:

{
    
    
    "serialNumber": "请求流水号",
    "payload" : {
    
    
        // ... 这里是有效载荷,存放具体的数据
    }
}

We need to extract the accessToken, which is userId, and insert it into the request body JSON as follows:

{
    
    
    "userId": "用户ID",
    "serialNumber": "请求流水号",
    "payload" : {
    
    
        // ... 这里是有效载荷,存放具体的数据
    }
}

GlobalFilterIn order to simplify the design here, it is implemented with a global filter , which actually needs to be considered in combination with specific scenarios:

@Slf4j
@Component
public class ModifyRequestBodyGlobalFilter implements GlobalFilter {
    
    

    private final DataBufferFactory dataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
        ServerHttpRequest request = exchange.getRequest();
        String accessToken = request.getHeaders().getFirst("accessToken");
        if (!StringUtils.hasLength(accessToken)) {
    
    
            throw new IllegalArgumentException("accessToken");
        }
        // 新建一个ServerHttpRequest装饰器,覆盖需要装饰的方法
        ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(request) {
    
    

            @Override
            public Flux<DataBuffer> getBody() {
    
    
                Flux<DataBuffer> body = super.getBody();
                InputStreamHolder holder = new InputStreamHolder();
                body.subscribe(buffer -> holder.inputStream = buffer.asInputStream());
                if (null != holder.inputStream) {
    
    
                    try {
    
    

                        // 解析JSON的节点
                        JsonNode jsonNode = objectMapper.readTree(holder.inputStream);
                        Assert.isTrue(jsonNode instanceof ObjectNode, "JSON格式异常");
                        ObjectNode objectNode = (ObjectNode) jsonNode;

                        // JSON节点最外层写入新的属性
                        objectNode.put("userId", accessToken);
                        DataBuffer dataBuffer = dataBufferFactory.allocateBuffer();
                        String json = objectNode.toString();
                        log.info("最终的JSON数据为:{}", json);
                        dataBuffer.write(json.getBytes(StandardCharsets.UTF_8));

                        return Flux.just(dataBuffer);
                    } catch (Exception e) {
    
    
                        throw new IllegalStateException(e);
                    }
                } else {
    
    
                    return super.getBody();
                }
            }
        };
        // 使用修改后的ServerHttpRequestDecorator重新生成一个新的ServerWebExchange
        return chain.filter(exchange.mutate().request(decorator).build());
    }

    private class InputStreamHolder {
    
    

        InputStream inputStream;
    }
}

have a test:

// HTTP
POST /order/json HTTP/1.1
Host: localhost:9090
Content-Type: application/json
accessToken: 10086
Accept: */*
Cache-Control: no-cache
Host: localhost:9090
accept-encoding: gzip, deflate
content-length: 94
Connection: keep-alive
cache-control: no-cache

{
    
    
    "serialNumber": "请求流水号",
    "payload": {
    
    
        "name": "doge"
    }
}

// 日志输出
最终的JSON数据为:{
    
    "serialNumber":"请求流水号","payload":{
    
    "name":"doge"},"userId":"10086"}

The most important thing is to use ServerHttpRequesta decorator ServerHttpRequestDecorator, which mainly covers the method of obtaining the request body data buffer. As for how to deal with other logic, you need to consider it yourself. Here is just a simple demonstration.

The general code logic is as follows:

ServerHttpRequest request = exchange.getRequest();
ServerHttpRequestDecorator requestDecorator = new ServerHttpRequestDecorator(request) {
    
    

     @Override
     public Flux<DataBuffer> getBody() {
    
    
         // 拿到承载原始请求体的Flux
         Flux<DataBuffer> body = super.getBody();
         // 这里通过自定义方式生成新的承载请求体的Flux
         Flux<DataBuffer> newBody = ...
         return newBody;
     }            
}
return chain.filter(exchange.mutate().request(requestDecorator).build());    
Modify the response body

The need to modify the response body is also relatively common, and the specific method is similar to modifying the request body.

For example, we want to implement the following functions: a third-party service request passes through the gateway, and the original message is ciphertext. We need to decrypt the ciphertext at the gateway, and then route the decrypted plaintext to the downstream service, and the downstream service responds to the plaintext successfully. The plaintext needs to be encrypted into ciphertext at the gateway and then returned to the third-party service.

Now simplify the whole process, use the AES encryption algorithm, and the unified password is the string "throwable", assuming that the plain text of the request message and the response message is as follows:

// 请求密文
{
    
    
    "serialNumber": "请求流水号",
    "payload" : "加密后的请求消息载荷"
}

// 请求明文(仅仅作为提示)
{
    
    
    "serialNumber": "请求流水号",
    "payload" : "{\"name:\":\"doge\"}"
}

// 响应密文
{
    
    
    "code": 200,
    "message":"ok",
    "payload" : "加密后的响应消息载荷"
}

// 响应明文(仅仅作为提示)
{
    
    
    "code": 200,
    "message":"ok",
    "payload" : "{\"name:\":\"doge\",\"age\":26}"
}

In order to facilitate the implementation of some encryption and decryption or encoding and decoding, the class library that needs to be Apacheintroduced commons-codec:

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.12</version>
</dependency>

A global filter is defined here to deal with encryption and decryption. In fact, it is best to combine the real scene to determine whether the global filter is suitable. Here is just an example:

// AES加解密工具类
public enum AesUtils {
    
    

    // 单例
    X;

    private static final String PASSWORD = "throwable";
    private static final String KEY_ALGORITHM = "AES";
    private static final String SECURE_RANDOM_ALGORITHM = "SHA1PRNG";
    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";

    public String encrypt(String content) {
    
    
        try {
    
    
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, provideSecretKey());
            return Hex.encodeHexString(cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) {
    
    
            throw new IllegalArgumentException(e);
        }
    }

    public byte[] decrypt(String content) {
    
    
        try {
    
    
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, provideSecretKey());
            return cipher.doFinal(Hex.decodeHex(content));
        } catch (Exception e) {
    
    
            throw new IllegalArgumentException(e);
        }
    }

    private SecretKey provideSecretKey() {
    
    
        try {
    
    
            KeyGenerator keyGen = KeyGenerator.getInstance(KEY_ALGORITHM);
            SecureRandom secureRandom = SecureRandom.getInstance(SECURE_RANDOM_ALGORITHM);
            secureRandom.setSeed(PASSWORD.getBytes(StandardCharsets.UTF_8));
            keyGen.init(128, secureRandom);
            return new SecretKeySpec(keyGen.generateKey().getEncoded(), KEY_ALGORITHM);
        } catch (Exception e) {
    
    
            throw new IllegalArgumentException(e);
        }
    }
}

// EncryptionGlobalFilter
@Slf4j
@Component
public class EncryptionGlobalFilter implements GlobalFilter, Ordered {
    
    

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public int getOrder() {
    
    
        return -2;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
        ServerHttpRequest request = exchange.getRequest();
        // 响应体
        ServerHttpResponse response = exchange.getResponse();
        DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
        ServerHttpRequestDecorator requestDecorator = processRequest(request, bufferFactory);
        ServerHttpResponseDecorator responseDecorator = processResponse(response, bufferFactory);
        return chain.filter(exchange.mutate().request(requestDecorator).response(responseDecorator).build());
    }

    private ServerHttpRequestDecorator processRequest(ServerHttpRequest request, DataBufferFactory bufferFactory) {
    
    
        Flux<DataBuffer> body = request.getBody();
        DataBufferHolder holder = new DataBufferHolder();
        body.subscribe(dataBuffer -> {
    
    
            int len = dataBuffer.readableByteCount();
            holder.length = len;
            byte[] bytes = new byte[len];
            dataBuffer.read(bytes);
            DataBufferUtils.release(dataBuffer);
            String text = new String(bytes, StandardCharsets.UTF_8);
            JsonNode jsonNode = readNode(text);
            JsonNode payload = jsonNode.get("payload");
            String payloadText = payload.asText();
            byte[] content = AesUtils.X.decrypt(payloadText);
            String requestBody = new String(content, StandardCharsets.UTF_8);
            log.info("修改请求体payload,修改前:{},修改后:{}", payloadText, requestBody);
            rewritePayloadNode(requestBody, jsonNode);
            DataBuffer data = bufferFactory.allocateBuffer();
            data.write(jsonNode.toString().getBytes(StandardCharsets.UTF_8));
            holder.dataBuffer = data;
        });
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(request.getHeaders());
        headers.remove(HttpHeaders.CONTENT_LENGTH);
        return new ServerHttpRequestDecorator(request) {
    
    

            @Override
            public HttpHeaders getHeaders() {
    
    
                int contentLength = holder.length;
                if (contentLength > 0) {
    
    
                    headers.setContentLength(contentLength);
                } else {
    
    
                    headers.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                }
                return headers;
            }

            @Override
            public Flux<DataBuffer> getBody() {
    
    
                return Flux.just(holder.dataBuffer);
            }
        };
    }

    private ServerHttpResponseDecorator processResponse(ServerHttpResponse response, DataBufferFactory bufferFactory) {
    
    
        return new ServerHttpResponseDecorator(response) {
    
    

            @SuppressWarnings("unchecked")
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
    
    
                if (body instanceof Flux) {
    
    
                    Flux<? extends DataBuffer> flux = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(flux.map(buffer -> {
    
    
                        CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
                        DataBufferUtils.release(buffer);
                        JsonNode jsonNode = readNode(charBuffer.toString());
                        JsonNode payload = jsonNode.get("payload");
                        String text = payload.toString();
                        String content = AesUtils.X.encrypt(text);
                        log.info("修改响应体payload,修改前:{},修改后:{}", text, content);
                        setPayloadTextNode(content, jsonNode);
                        return bufferFactory.wrap(jsonNode.toString().getBytes(StandardCharsets.UTF_8));
                    }));
                }
                return super.writeWith(body);
            }
        };
    }

    private void rewritePayloadNode(String text, JsonNode root) {
    
    
        try {
    
    
            JsonNode node = objectMapper.readTree(text);
            ObjectNode objectNode = (ObjectNode) root;
            objectNode.set("payload", node);
        } catch (Exception e) {
    
    
            throw new IllegalStateException(e);
        }
    }

    private void setPayloadTextNode(String text, JsonNode root) {
    
    
        try {
    
    
            ObjectNode objectNode = (ObjectNode) root;
            objectNode.set("payload", new TextNode(text));
        } catch (Exception e) {
    
    
            throw new IllegalStateException(e);
        }
    }

    private JsonNode readNode(String in) {
    
    
        try {
    
    
            return objectMapper.readTree(in);
        } catch (Exception e) {
    
    
            throw new IllegalStateException(e);
        }
    }

    private class DataBufferHolder {
    
    

        DataBuffer dataBuffer;
        int length;
    }
}  

First prepare a ciphertext:

Map<String, Object> json = new HashMap<>(8);
json.put("serialNumber", "请求流水号");
String content = "{\"name\": \"doge\"}";
json.put("payload", AesUtils.X.encrypt(content));
System.out.println(new ObjectMapper().writeValueAsString(json));

// 输出
{
    
    "serialNumber":"请求流水号","payload":"144e3dc734743f5709f1adf857bca473da683246fd612f86ac70edeb5f2d2729"}

Mock request:

POST /order/json HTTP/1.1
Host: localhost:9090
accessToken: 10086
Content-Type: application/json
User-Agent: PostmanRuntime/7.13.0
Accept: */*
Cache-Control: no-cache
Postman-Token: bda07fc3-ea1a-478c-b4d7-754fe6f37200,634734d9-feed-4fc9-ba20-7618bd986e1c
Host: localhost:9090
cookie: customCookieName=customCookieValue
accept-encoding: gzip, deflate
content-length: 104
Connection: keep-alive
cache-control: no-cache

{
    
    
    "serialNumber": "请求流水号",
    "payload": "FE49xzR0P1cJ8a34V7ykc9poMkb9YS+GrHDt618tJyk="
}

// 响应结果
{
    
    
    "serialNumber": "请求流水号",
    "payload": "oo/K1igg2t/S8EExkBVGWOfI1gAh5pBpZ0wyjNPW6e8="   # <--- 解密后:{"name":"doge","age":26}
}

Problems encountered:

  • The interface must be implemented Orderedto return an order value less than -1. This is because NettyWriteResponseFilterthe order value is -1 . We need to override the logic of returning the response body, and the custom one GlobalFiltermust be NettyWriteResponseFilterexecuted first.
  • After each restart of the gateway, the first request always fails ServerHttpRequestto read the valid body from the original one. To be precise, the phenomenon that occurs is that an empty object is obtained when calling, resulting in a null pointer; the strange thing is that from the NettyRoutingFiltersecond ServerHttpRequest#getBody()It can be called normally from the beginning of a request. The author Spring Cloud Gatewaylowered the version of Finchley.SR3, the problem no longer Spring Bootoccurs 2.0.8.RELEASE, and it is initially determined that it is Spring Cloud Gatewaya compatibility problem or a bug caused by the version upgrade .

The most important thing is to use ServerHttpResponsea decorator ServerHttpResponseDecorator, which mainly covers the part written to the response body data buffer. As for how to deal with other logic, you need to consider it yourself. Here is just a simple demonstration. The general code logic is as follows:

ServerHttpResponse response = exchange.getResponse();
ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response) {
    
    

            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
    
    
                if (body instanceof Flux) {
    
    
                    Flux<? extends DataBuffer> flux = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(flux.map(buffer -> {
    
    
                        // buffer就是原始的响应数据的缓冲区
                        // 下面处理完毕之后返回新的响应数据的缓冲区即可
                        return bufferFactory.wrap(...);
                    }));
                }
                return super.writeWith(body);
            }
        };
return chain.filter(exchange.mutate().response(responseDecorator).build());    ###  

5. Advanced configuration

5.1 Unified configuration of cross-domain requests

When the current request passes through the gateWay gateway, cross-domain requests need to be uniformly configured on the gateway, and all requests are required to pass

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origins: "*"
            allowed-headers: "*"
            allow-credentials: true
            allowed-methods:
              - GET
              - POST
              - DELETE
              - PUT
              - OPTION

5.2 Health check configuration

The admin-client and actuator health check configurations provide support for future functions. This part is relatively simple and will not be described in detail. Add the following maven dependencies and configurations

maven dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-client</artifactId>
    <version>2.1.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

configuration file

spring:
  application:
    name: mas-cloud-gateway
  boot:
    admin:
      client:
      ### 本地搭建的admin-server
        url: http://localhost:8011
eureka:
  client:
    registerWithEureka: true
    fetchRegistry: true
    healthcheck:
      enabled: true
    serviceUrl:
      defaultZone: http://localhost:6887/eureka/
    enabled: true
feign:
  sentinel:
    enabled: true
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: ALWAYS

If the forwarded target address is a component in the microservice, it should be written in the form of lb://mas-openapi-service if it is not in the form of specific ip:port, and the target address will be directly pulled from the registration center

5.3 Distributed current limiting

When the flow entrance cannot be controlled, we need to consider the flow control. We can control the flow of the system, and also independently control the flow of the system access users. We need to choose the appropriate flow control strategy based on the actual business scenario.

Several common flow control algorithms are as follows:

  • fixed window algorithm
  • sliding window algorithm
  • Leaky Bucket Algorithm
  • token bucket algorithm

1. The fixed window algorithm refers to the algorithm that performs flow control according to the threshold within a fixed time window. This algorithm has the following characteristics:

  • The principle is simple and the implementation is easy
  • When there is a burst of traffic in the time window, blocking requests flood in at the same time when the time window is switched, resulting in traffic stampede, and the system stability is tested

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-obVodGQk-1685123974010)(null)]

2. Sliding window algorithm . In order to solve the problem of traffic impact when the fixed window algorithm switches time windows, we can divide the time window into multiple small time segments. Each small time segment has an independent counter. When the request When the time point is greater than the maximum time point of the time window, the entire window is moved to the right by a small time segment (the first time segment is discarded, and the request is placed on the latest time segment), which is the sliding window. The more time segments are divided, the smoother the sliding window will slide, and the more accurate the flow control will be.

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-6jmLMZV7-1685123974911)(null)]

3. Leaky bucket algorithm , define a bucket with a certain capacity, if the capacity of the bucket is not full, the new request will be put into the bucket, if the capacity of the bucket is full, the new request will be discarded, the leaky bucket algorithm passes Control the output rate, smooth the network traffic, and play the role of eliminating peaks and filling valleys.

Since the leakage rate of the leaky bucket algorithm is fixed, it cannot effectively use network resources in the case of burst traffic. In this case, the processing of requests is inefficient.

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-8cHvhHQR-1685123974480)(null)]

Token bucket algorithm , in a sense, the token bucket algorithm is an improvement on the leaky bucket algorithm, the bucket algorithm can limit the rate of request calls, and the token bucket algorithm can limit the average rate of calls A certain degree of bursty calls is allowed. In the token bucket algorithm, there is a bucket used to store a fixed number of tokens. There is a mechanism in the algorithm to put tokens into the bucket at a certain rate. Each request call needs to obtain a token first. Only when the token is obtained, can the execution continue. Otherwise, choose to wait for the available token, or directly reject it. The action of putting tokens is carried out continuously. If the number of tokens in the bucket reaches the upper limit, the tokens will be discarded, so there is such a situation. There are always a large number of available tokens in the bucket. At this time, incoming requests can be directly Get the token to execute, for example, set qps to 100, then one second after the current limiter is initialized, there are already 100 tokens in the bucket, and the service has not been fully started at this time. The limiter can withstand 100 requests instantaneously. Therefore, only when there is no token in the bucket, the request will wait, and finally it is equivalent to executing at a certain rate.

In Spring Cloud Gateway, there are Filter filters, so the above three filters can be implemented in the "pre" type Filter by itself. However, current limiting is the most basic function of the gateway. Spring Cloud Gateway officially provides the RequestRateLimiterGatewayFilterFactory class, which is applicable to the way of implementing token buckets in Redis by executing Lua scripts. The specific implementation logic is in the RequestRateLimiterGatewayFilterFactory class, and the lua script is in the folder shown in the figure below:

First, introduce the gateway's starting dependency and redis's reactive dependency in the pom file of the project. The code is as follows:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
 
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

The configuration is as follows:

server:
  port: 8081
spring:
  cloud:
    gateway:
      routes:
      - id: limit_route
        uri: http://httpbin.org:80/get
        predicates:
        - After=2017-01-20T17:42:47.789-07:00[America/Denver]
        filters:
        # 限流过滤器
        - name: RequestRateLimiter
          args:
            #用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象
            key-resolver: '#{@userKeyResolver}'
            #令牌桶每秒填充平均速率
            redis-rate-limiter.replenishRate: 1
            #令牌桶总容量
            redis-rate-limiter.burstCapacity: 3
  application:
    name: cloud-gateway
  redis:
    host: localhost
    port: 6379
    database: 0

In the above configuration file, the port of the specified program is 8081, the information of redis is configured, and the current limiting filter of RequestRateLimiter is configured. The filter needs to be configured with three parameters:

  • burstCapacity, the total capacity of the token bucket.
  • replenishRate, the average rate at which the token bucket is filled per second.
  • key-resolver, the name of the Bean object of the resolver for the key used for throttling. It uses the SpEL expression to get the Bean object from the Spring container according to #{@beanName}.

Here, the flow is limited according to the user ID, and the userId parameter must be carried in the request path

@Bean
KeyResolver userKeyResolver() {
    
    
  return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}

KeyResolver needs to implement the resolve method. For example, to limit the current based on userid, you need to use userid to judge. After implementing the KeyResolver, the Bean of this class needs to be registered in the Ioc container.

If the current limit needs to be based on IP, the bean defined to obtain the current limit Key is:

@Bean
public KeyResolver ipKeyResolver() {
    
    
  return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}

The request information can be obtained through the exchange object. HostName is used here. If you want to limit the flow according to the user, you can get the user ID or username of the current request here, for example:

If you need to limit the current based on the URI of the interface, you need to obtain the URI of the request address as the current limiting key. The defined Bean object is:

@Bean
KeyResolver apiKeyResolver() {
    
    
  return exchange -> Mono.just(exchange.getRequest().getPath().value());
}

The request information can be obtained through the exchange object. HostName is used here. If you want to limit the flow according to the user, you can get the user ID or username of the current request here, for example:

If you need to limit the current based on the URI of the interface, you need to obtain the URI of the request address as the current limiting key. The defined Bean object is:

@Bean
KeyResolver apiKeyResolver() {
    
    
    return exchange -> Mono.just(exchange.getRequest().getPath().value());
}

6. Integrate Nacos

maven dependencies

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>nacos_gateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>nacos_gateway</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR3</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--gateway-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--nacos dicovery-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>0.2.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Note that this jar does not exist in the pom.xml file in the Gateway service

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>

Otherwise, the following error will be reported when calling the interface, because the gateway uses webflux, and netty is used by default, so tomcat-related dependencies are excluded from the dependencies

java.lang.ClassCastException: org.springframework.core.io.buffer.DefaultDataBufferFactory cannot be cast to org.springframework.core.io.buffer.NettyDataBufferFactory
    at org.springframework.cloud.gateway.filter.NettyWriteResponseFilter.lambda$filter$1(NettyWriteResponseFilter.java:82) ~[spring-cloud-gateway-core-2.1.3.RELEASE.jar:2.1.3.RELEASE]
    at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:44) [reactor-core-3.2.12.RELEASE.jar:3.2.12.RELEASE]

Error 2 is caused by spring-boot-starter-web

Service discovery configuration: Get the list of microservice providers from Nacos

server:
  port: 9999

spring:
  application:
    name: springcloud-gateway
  profiles:
    active: dev
  cloud:
    nacos:
      discovery:
        server-addr: ${
    
    NACOS_SERVER:cdh1:8848}
      config:
        server-addr: ${
    
    NACOS_SERVER:cdh1:8848}
        prefix: springcloud-gateway
        group: DEFAULT_GROUP
        file-extension: yml
        ext-config:
          - data-id: crazymaker-db-dev.yml
            group: DEFAULT_GROUP
            refresh: true
          - data-id: crazymaker-redis-dev.yml
            group: DEFAULT_GROUP
            refresh: true
          - data-id: crazymaker-common-dev.yml
            group: DEFAULT_GROUP
            refresh: true
          - data-id: some.properties
            group: DEFAULT_GROUP
            refresh: true
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: true  #开启从注册中心动态创建路由的功能,利用微服务名进行路由
          lower-case-service-id: true
          filters:
            - args[name]: serviceId
              name: Hystrix
          predicates:
            - args[pattern]: '"''/''+serviceId+''/**''"'
              name: Path
      routes:
        - id: blog
          uri: https://blog.csdn.net/
          predicates:
            - Path=/csdn
        - id: blog1
          uri: https://blog.csdn.net/
          predicates:
            - Path=/blog1/**
          filters:
            - RewritePath=/blog1/(?<segment>.*), /$\{
    
    segment}
        # 代理前  http://192.168.68.1:9999/blog1/crazymakercircle/article/details/80208650
        #  代理后  https://blog.csdn.net/crazymakercircle/article/details/80208650
        - id: service_provider_demo_route
          uri: lb://service-provider-demo
          predicates:
            - Path=/provider/**
        - id: service_provider_demo_route_filter
          uri: lb://service-provider-demo
          predicates:
            - Path=/filter/**
          filters:
            - RewritePath=/filter/(?<segment>.*), /provider/$\{
    
    segment}
            - UserIdCheck
        - id: service_consumer_demo_route
          uri: lb://service-consumer-demo
          predicates:
            - Path=/consumer/**
        - id: sentinel_demo_provider_route
          uri: lb://sentinel-demo-provider
          predicates:
            - Path=/sentinel-demo/**
        - id: uaa-provider_route
          uri: lb://uaa-provider
          predicates:
            - Path=/uaa-provider/**
    sentinel:
      transport:
        dashboard: cdh1:8849 #配置Sentinel dashboard地址
        port: 8719   #这里配置的是本地端口
      eager: true
    inetutils:
      timeout-seconds: 10
      preferred-networks: ${
    
    SCAFFOLD_PREFERRED_NETWORKS:192.168.68.}
      prefer-ip-address: true  #访问路径可以显示IP地址


ribbon:
  eager-load:
    enabled: true # 开启Ribbon的饥饿加载模式,启动时创建 RibbonClient
  MaxAutoRetries: 1 # 同一台实例的最大重试次数,但是不包括首次调用,默认为1次
  MaxAutoRetriesNextServer: 2  # 重试负载均衡其他实例的最大重试次数,不包括首次调用,默认为0次
  OkToRetryOnAllOperations: true  # 是否对所有操作都重试,默认false
  ServerListRefreshInterval: 2000 # 从注册中心刷新服务器列表信息的时间间隔,默认为2000毫秒,即2秒
  retryableStatusCodes: 400,401,403,404,500,502,504
  NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RetryRule #配置规则 重试
  ConnectTimeout: 3000  #连接建立的超时时长,默认1秒
  ReadTimeout: 3000 #处理请求的超时时间,默认为1秒
  MaxTotalConnections: 1000  # 最大连接数
  MaxConnectionsPerHost: 1000  # 每个host最大连接数
  restclient:
    enabled: true


hystrix:
  threadpool:
    default:
      coreSize: 10 # 线程池核心线程数
      maximumSize: 20  # 线程池最大线程数
      allowMaximumSizeToDivergeFromCoreSize: true   # 线程池最大线程数是否有效
      keepAliveTimeMinutes: 10  # 设置可空闲时间,单位分钟
    demo-provider:
      coreSize: 20   # 线程池核心线程数
      maximumSize: 100   # 线程池最大线程数
      allowMaximumSizeToDivergeFromCoreSize: true   # 线程池最大线程数是否有效
      keepAliveTimeMinutes: 20  # 设置可空闲时间,单位分钟
  propagate:
    request-attribute:
      enabled: true
  command:
    default:  #全局默认配置
      execution:  #线程隔离相关配置
        timeout:
          enabled: true   #是否给方法执行设置超时时间,默认为true。一般我们不要改。
        isolation:
          strategy: THREAD    #配置请求隔离的方式,这里是默认的线程池方式。还有一种信号量的方式semaphore,使用比较少。
          thread:
            timeoutInMilliseconds: 100000  #方式执行的超时时间,默认为1000毫秒,在实际场景中需要根据情况设置
            interruptOnTimeout: true   #发生超时时是否中断方法的执行,默认值为true。不要改。
            interruptOnCancel: false  #是否在方法执行被取消时中断方法,默认值为false。没有实际意义,默认就好!
      circuitBreaker:   #熔断器相关配置
        enabled: true   #是否启动熔断器,默认为true,false表示不要引入 Hystrix。
        requestVolumeThreshold: 20     #启用熔断器功能窗口时间内的最小请求数,假设我们设置的窗口时间为10秒,
        sleepWindowInMilliseconds: 5000    #此配置的作用是指定熔断器打开后多长时间内允许一次请求尝试执行,官方默认配置为5秒。
        errorThresholdPercentage: 50   #窗口时间内超过50%的请求失败后就会打开熔断器将后续请求快速失败掉,默认配置为50
      metrics:
        rollingStats:
          timeInMilliseconds: 10000
          numBuckets: 10

# 暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: '*'

Nacos implements dynamic configuration

Use nacos to implement dynamic routing. The above two methods are static configuration paths, which can only deal with some scenarios. Next, configure nacos to implement dynamic configuration and configuration storage. Since gateWay is not adapted to nacos, you need to customize the listener:

@Component
@Slf4j
public class NacosDynamicRouteService implements ApplicationEventPublisherAware {
    
    
    private String dataId = "gateway-router";
    private String group = "DEFAULT_GROUP";
    @Value("${spring.cloud.nacos.config.server-addr}")
    private String serverAddr;
    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;
    private ApplicationEventPublisher applicationEventPublisher;
    private static final List<String> ROUTE_LIST = new ArrayList<>();
    @PostConstruct
    public void dynamicRouteByNacosListener() {
    
    
        try {
    
    
            ConfigService configService = NacosFactory.createConfigService(serverAddr);
            configService.getConfig(dataId, group, 5000);
            configService.addListener(dataId, group, new Listener() {
    
    
                @Override
                public void receiveConfigInfo(String configInfo) {
    
    
                    clearRoute();
                    try {
    
    
                        if (StringUtil.isNullOrEmpty(configInfo)) {
    
    //配置被删除
                            return;
                        }
                        List<RouteDefinition> gatewayRouteDefinitions = JSONObject.parseArray(configInfo, RouteDefinition.class);
                        for (RouteDefinition routeDefinition : gatewayRouteDefinitions) {
    
    
                            addRoute(routeDefinition);
                        }
                        publish();
                    } catch (Exception e) {
    
    
                        log.error("receiveConfigInfo error" + e);
                    }
                }
                @Override
                public Executor getExecutor() {
    
    
                    return null;
                }
            });
        } catch (NacosException e) {
    
    
            log.error("dynamicRouteByNacosListener error" + e);
        }
    }
    private void clearRoute() {
    
    
        for (String id : ROUTE_LIST) {
    
    
            this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
        }
        ROUTE_LIST.clear();
    }
    private void addRoute(RouteDefinition definition) {
    
    
        try {
    
    
            routeDefinitionWriter.save(Mono.just(definition)).subscribe();
            ROUTE_LIST.add(definition.getId());
        } catch (Exception e) {
    
    
            log.error("addRoute error" + e);
        }
    }
    private void publish() {
    
    
        this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this.routeDefinitionWriter));
    }
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
    
    
        this.applicationEventPublisher = applicationEventPublisher;
    }

Add a rule in nacos:

[{
    
    
    "filters": [],
    "id": "baidu_route",
    "order": 0,
    "predicates": [{
    
    
        "args": {
    
    
            "pattern": "/baidu"
        },
        "name": "Path"
    }],
    "uri": "https://www.baidu.com"
}]

Access the routing rules of the gateway, and you can see the rules you just added. When you visit http://localhost:9022/baidu, the request is directly forwarded to Baidu's homepage.

effective path

Custom definition of service discovery routing predicates and filters

Gateways can be configured to discover routes based on services registered with the DiscoveryClient registry.

To enable this, set spring.cloud.gateway.discovery.locator.enabled=true, and make sure the DiscoveryClient implementation is on the classpath and enabled (like netflix eureka, consul, or zookeeper).

Configure assertions and filters for registry routes

By default, the gateway defines a single assertion and filter for routes created via DiscoveryClient.

The default assertion is to use /serviceId/**the defined path assertion, where serviceId is the ID of the service in DiscoveryClient.
Default filters /serviceId/(?.*)are /${remaining}overridden using regular expressions and substitutions. This simply strips the service id from the path before the request is sent downstream.

Notice

This default filter, the old version does not have it, this is fatal

Nien upgraded the springcloud gateway when he was working on the push platform architecture in the past few days, which is going to kill him

All requests are 404

And because reactive programming is not very easy to debug, however, Nien doesn’t like this set, and likes to go deep behind enemy lines. After entering the source code, he probably found the assertion processing iteration place, and saw more than 10 filters

One of the filters, rewrite, did an incredible thing, removing the prefix of the backend service, if the prefix is ​​serviceId

This is where our problem lies: Coincidentally, our backend microservice needs a path prefix, and the prefix of the path is serviceId

Now, this upgrade operation of springcloud gateway has lost the route prefix. Of course, the route can’t pass, which made me blind for 2 hours.

Read the official documentation and follow the official instructions:

You can customize the assertions and filters used by DiscoveryClient routes by setting spring.cloud.gateway.discovery.locator.predicates[x] and spring.cloud.gateway.discovery.locator.filters[y].

After customization, the default ones are gone. If you want to keep the default functions, you need to manually add default assertions and filters.

The following is such an example.

Example 69. application.properties

spring.cloud.gateway.discovery.locator.predicates[0].name: Path
spring.cloud.gateway.discovery.locator.predicates[0].args[pattern]: "'/'+serviceId+'/**'"
spring.cloud.gateway.discovery.locator.predicates[1].name: Host
spring.cloud.gateway.discovery.locator.predicates[1].args[pattern]: "'**.foo.com'"
spring.cloud.gateway.discovery.locator.filters[0].name: Hystrix
spring.cloud.gateway.discovery.locator.filters[0].args[name]: serviceId
spring.cloud.gateway.discovery.locator.filters[1].name: RewritePath
spring.cloud.gateway.discovery.locator.filters[1].args[regexp]: "'/' + serviceId + '/(?<remaining>.*)'"
spring.cloud.gateway.discovery.locator.filters[1].args[replacement]: "'/${remaining}'"

Therefore, this RewritePath filter needs to be removed, and the following is a custom client filter for nacos

    gateway:
      enabled: true
      discovery:
        locator:
          enabled: true  #开启从注册中心动态创建路由的功能,利用微服务名进行路由
          lower-case-service-id: true
          filters:
            - args[name]: serviceId
              name: Hystrix
          predicates:
            - args[pattern]: '"''/''+serviceId+''/**''"'
              name: Path

In essence, whether to remove the prefix of the url, under the configuration file, there are completely rules that can be configured by yourself.

Of course, it is understandable that the official hopes to add some filters to the route of the microservice DiscoveryClient

Anyway, in our scenario, we can only override the official default discovery.filters configuration, so we made the above modification

After modification, restart,

Then look at the filter through the breakpoint, the rewrite filter is gone

Finally it's not a 404 error

It seems that the official upgraded version of springcloud did not do anything meaningful, and a lot of it was vase work

7. Integrate Swagger aggregation microservice system API documents

For the source code, please refer to the crazy-springcloud scaffolding in Crazy Maker Circle

maven dependencies

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud-center-alibaba</artifactId>
        <groupId>com.crazymaker.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.crazymaker.springcloud</groupId>
    <artifactId>springcloud-gateway-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>springcloud-gateway-demo</name>
    <packaging>jar</packaging>


    <dependencies>
        <!--gateway 网关依赖,内置webflux 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--新增sentinel-->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-transport-simple-http</artifactId>
        </dependency>

        <!-- nacos服务注册发现依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>com.google.guava</groupId>
                    <artifactId>guava</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.connector.version}</version>
        </dependency>
        <!-- nacos配置服务依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>com.google.guava</groupId>
                    <artifactId>guava</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${swagger.version}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-common</artifactId>
            <version>${swagger.version}</version>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>${swagger-ui.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-commons</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.crazymaker.cloud.nacos.demo.gateway.starter.GatewayProviderApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>


            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.4.1</version>
                <configuration>
                    <descriptors>
                        <descriptor>src/main/assembly/assembly.xml</descriptor>
                    </descriptors>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <!-- 添加docker-maven插件 -->

            <plugin>
                <groupId>com.spotify</groupId>
                <artifactId>docker-maven-plugin</artifactId>
                <version>1.1.1</version>
                <configuration>
                    <imageName>dockerlocal:5000/${project.artifactId}:${project.version}</imageName>
                    <baseImage>dockerlocal:5000/java</baseImage>
                    <entryPoint>["java", "-jar", "/${project.build.finalName}.jar"]</entryPoint>
                    <dockerDirectory>docker</dockerDirectory>
                    <resources>
                        <resource>
                            <targetPath>/</targetPath>
                            <directory>${project.build.directory}</directory>
                            <include>${project.build.finalName}.jar</include>
                        </resource>
                    </resources>
                </configuration>
            </plugin>

        </plugins>
    </build>
</project>

configuration file

package com.crazymaker.cloud.nacos.demo.gateway.config;

import lombok.AllArgsConstructor;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;

import java.util.ArrayList;
import java.util.List;

/**
 * @ClassName SwaggerProvider
 * @PackageName com.ruoyi.gateway.config
 * @Description
 * @Author daiz
 * @Date 2019/8/16 10:04
 * @Version 1.0
 */
@Component
@Primary
@AllArgsConstructor
public class SwaggerConfig implements SwaggerResourcesProvider
{
    
    
    public static final String API_URI = "/v2/api-docs";

    private final RouteLocator routeLocator;

    private final GatewayProperties gatewayProperties;

    @Override
    public List<SwaggerResource> get()
    {
    
    
        /**
         * 网关应用名称,不需要在网关的swagger 上展示
         */
        String appName = "springcloud-gateway";

        List<SwaggerResource> resources = new ArrayList<>();
        List<String> routes = new ArrayList<>();
        // 取出gateway的route
        routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
        // 结合配置的route-路径(Path),和route过滤,只获取有效的route节点
        // 打开下面注释可以自动扫描接入gateway的服务,为了演示,只扫描system
        // gatewayProperties.getRoutes().stream().filter(routeDefinition ->
        // routes.contains(routeDefinition.getId()))
        gatewayProperties.getRoutes().stream()
                .filter(route -> route.getUri().getHost() != null)
                .filter(route -> !appName.equals(route.getUri().getHost()))
                .forEach(routeDefinition -> routeDefinition.getPredicates().stream()
                        .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
                        .forEach(predicateDefinition -> resources
                                .add(swaggerResource(routeDefinition.getId(), predicateDefinition.getArgs()
                                        .get(NameUtils.GENERATED_NAME_PREFIX + "0").replace("/**", API_URI)))));
        return resources;
    }

    private SwaggerResource swaggerResource(String name, String location)
    {
    
    
        SwaggerResource swaggerResource = new SwaggerResource();
        swaggerResource.setName(name);
        swaggerResource.setLocation(location);
        swaggerResource.setSwaggerVersion("2.0");
        return swaggerResource;
    }
}

Effect:

8. Integrate Sentinel to complete flow control and downgrade

maven dependencies

Use Sentinel as gateWay's current limiting, downgrading, and system protection tools

<!--alibaba 流量卫士-->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>${sentinel.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>

configuration file

Client configuration: Add the following configuration in the configuration file, and the dashboard can easily manage the client. Another way is to add it at startup

spring:
  cloud:
    sentinel:
      transport:
        ## VM
        ##-Djava.net.preferIPv4Stack=true -Dcsp.sentinel.dashboard.server=localhost:8080 -Dcsp.sentinel.api.port=8666 -Dproject.name=gateway -Dcsp.sentinel.app.type=1
        dashboard: localhost:8880
        port: 8880

General configuration of current limiting rules

Since the working principle of sentinel actually uses the global filter to intercept requests and calculate whether to perform operations such as current limiting and fusing, increase the SentinelGateWayFilter configuration

@Bean//拦截请求
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
    
    
    return new SentinelGatewayFilter();
}

Sentinel not only supports the declaration of resources through hard code, but also declares through annotations. In order for the annotations to take effect, it is also necessary to configure the aspect class SentinelResourceAspect

@Bean
public SentinelResourceAspect sentinelResourceAspect() {
    
    
    return new SentinelResourceAspect();
}

Sentinel interception includes views, static resources, etc. It is necessary to configure viewResolvers and exceptions after interception. We can also customize the prompts for throwing exceptions

public SentinelConfig(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                      ServerCodecConfigurer serverCodecConfigurer) {
    
    
    this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
    this.serverCodecConfigurer = serverCodecConfigurer;
}

@Bean//自定义异常
@Order(Ordered.HIGHEST_PRECEDENCE)
public ExceptionHandler sentinelGatewayBlockExceptionHandler() {
    
    
    // Register the block exception handler for Spring Cloud Gateway.
    return new ExceptionHandler(viewResolvers, serverCodecConfigurer);
}

Custom exception prompt: When a current limit or fuse exception occurs, the defined prompt information will be returned.

/**
 * 配置限流的异常处理器:SentinelGatewayBlockExceptionHandler
 */
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
    
    
    return new SentinelGatewayBlockExceptionHandlerEX(viewResolvers, serverCodecConfigurer);
}

No additional configuration is required, sentinel can already work normally

Limiting rule settings

1 Resource definition: define the API group

private void initCustomizedApis() {
    
    
    Set<ApiDefinition> definitions = new HashSet<>();

    ApiDefinition api3 = new ApiDefinition("filter_api_group")
        .setPredicateItems(new HashSet<ApiPredicateItem>() {
    
    {
    
    
            add(new ApiPathPredicateItem().setPattern("/filter/**")
                .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
        }});

    ApiDefinition api4 = new ApiDefinition("service-provider-demo_api_group")
        .setPredicateItems(new HashSet<ApiPredicateItem>() {
    
    {
    
    
            add(new ApiPathPredicateItem().setPattern("/service-provider-demo/**")
                .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
        }});

    definitions.add(api3);
    definitions.add(api4);

    GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}

2 Define current limiting rules

private void initGatewayRules() {
    
    
    Set<GatewayFlowRule> rules = new HashSet<>();
    /*设置限流规则
        count: QPS即每秒钟允许的调用次数
        intervalSec: 每隔多少时间统计一次汇总数据,统计时间窗口,单位是秒,默认是 1 秒。
        */
    GatewayFlowRule rule3 = new GatewayFlowRule("filter_api_group")
        .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME)
        .setCount(1) //qps 1
        .setIntervalSec(1)  //1 s
        ;

    GatewayFlowRule rule4 = new GatewayFlowRule("service-provider-demo_api_group")
        .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME)
        .setCount(1) //qps 1
        .setIntervalSec(1)  //10 s
        ;
    rules.add(rule4);
    rules.add(rule3);
    GatewayRuleManager.loadRules(rules);

}

For details, please refer to the learning video

Gateway current limiting parameters

The fields of GatewayFlowRule are explained as follows:

  • resource: Resource name, which can be a route name in the gateway or a user-defined API group name.
  • resourceMode: Whether the rule is for the route of API Gateway (RESOURCE_MODE_ROUTE_ID) or the API group defined by the user in Sentinel (RESOURCE_MODE_CUSTOM_API_NAME), the default is route.
  • grade: current limit index dimension, the same as the grade field of the current limit rule.
  • count: current limit threshold
  • intervalSec: Statistics time window, the unit is second, the default is 1 second.
  • controlBehavior: The control effect of traffic shaping, the same as the controlBehavior field of the current limiting rule. Currently, it supports two modes: fast fail and uniform queue. The default is fast fail.
  • burst: The number of additional requests allowed when dealing with burst requests.
  • maxQueueingTimeoutMs: The maximum queuing time in the uniform queuing mode, in milliseconds, only valid in the uniform queuing mode.
  • paramItem
    parameter current limit configuration. If not provided, it means that the parameters are not limited, and the gateway rule will be converted into a normal flow control rule; otherwise, it will be converted into a hotspot rule. The fields in it:
    • parseStrategy: The strategy for extracting parameters from the request. Currently, it supports four modes for extracting source IP (PARAM_PARSE_STRATEGY_CLIENT_IP), Host (PARAM_PARSE_STRATEGY_HOST), arbitrary Header (PARAM_PARSE_STRATEGY_HEADER) and arbitrary URL parameters (PARAM_PARSE_STRATEGY_URL_PARAM).
    • fieldName: If the extraction strategy selects Header mode or URL parameter mode, you need to specify the corresponding header name or URL parameter name.
    • pattern: The matching pattern of the parameter value, only the request attribute value matching this pattern will be included in the statistics and flow control; if it is empty, all the values ​​of the request attribute will be counted. (Supported from version 1.6.2)
    • matchStrategy: The matching strategy of the parameter value, which currently supports exact matching (PARAM_MATCH_STRATEGY_EXACT), substring matching (PARAM_MATCH_STRATEGY_CONTAINS) and regular matching (PARAM_MATCH_STRATEGY_REGEX). (Supported from version 1.6.2)

Users can manually load gateway rules through GatewayRuleManager.loadRules(rules), or register dynamic rule source dynamic push through GatewayRuleManager.register2Property(property) (recommended method).

10. The principle of sentinel gateway flow control:

After knowing how to use the sentinel-dashboard to control the flow of the gateway, we will introduce the implementation principle of the flow control of the sentinel gateway

When the gateway flow control rule (GatewayFlowRule) is loaded through the GatewayRuleManager, regardless of whether the request attribute is restricted or not, the bottom layer of Sentinel will convert the gateway flow control rule into a hot parameter rule (ParamFlowRule) and store it in the GatewayRuleManager, which is the same as the normal hot parameter rule isolated. During the conversion, Sentinel will set the parameter index (idx) for the gateway flow control rule according to the configuration of the request attribute, and synchronize it to the generated hotspot parameter rule.

When an external request enters the API Gateway, it will pass through the filter implemented by Sentinel, in which "routing/API group matching -> request attribute parsing and parameter assembly" will be performed in sequence. Sentinel will parse the request attributes according to the configured gateway flow control rules, assemble the parameter array according to the parameter index order, and finally pass it into SphU.entry(res, args). The Sentinel API Gateway Adapter Common module adds a GatewayFlowSlot to the Slot Chain, which is specially used for checking gateway rules. GatewayFlowSlot will extract the generated hotspot parameter rules from GatewayRuleManager, and check the rules in turn according to the incoming parameters. If a rule does not target the request attribute, a preset constant will be placed in the last position of the parameter to achieve the effect of ordinary flow control.

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-JpBDiyBm-1685123973442)(null)]

Operation:

  1. Combined with Jwt to realize the gateway authentication operation.
  2. Customize the debt balance strategy to achieve priority in calling local microservices.

references

Official documentation
https://cloud.spring.io/spring-cloud-gateway/reference/html
https://cloud.spring.io/spring-cloud-gateway/reference/html/#configuring-predicates-and-filters- for-discoveryclient-routes
https://www.cnblogs.com/satire/p/15092894.html

The realization path of technical freedom PDF:

Realize your architectural freedom:

" Have a thorough understanding of the 8-figure-1 template, everyone can do the architecture "

" 10Wqps review platform, how to structure it? This is what station B does! ! ! "

" Alibaba Two Sides: How to optimize the performance of tens of millions and billions of data?" Textbook-level answers are coming "

" Peak 21WQps, 100 million DAU, how is the small game "Sheep a Sheep" structured? "

" How to Scheduling 10 Billion-Level Orders, Come to a Big Factory's Superb Solution "

" Two Big Factory 10 Billion-Level Red Envelope Architecture Scheme "

… more architecture articles, being added

Realize your responsive freedom:

" Responsive Bible: 10W Words, Realize Spring Responsive Programming Freedom "

This is the old version of " Flux, Mono, Reactor Combat (the most complete in history) "

Realize your spring cloud freedom:

" Spring cloud Alibaba Study Bible "

" Sharding-JDBC underlying principle and core practice (the most complete in history) "

" Get it done in one article: the chaotic relationship between SpringBoot, SLF4j, Log4j, Logback, and Netty (the most complete in history) "

Realize your linux freedom:

" Linux Commands Encyclopedia: 2W More Words, One Time to Realize Linux Freedom "

Realize your online freedom:

" Detailed explanation of TCP protocol (the most complete in history) "

" Three Network Tables: ARP Table, MAC Table, Routing Table, Realize Your Network Freedom!" ! "

Realize your distributed lock freedom:

" Redis Distributed Lock (Illustration - Second Understanding - The Most Complete in History) "

" Zookeeper Distributed Lock - Diagram - Second Understanding "

Realize your king component freedom:

" King of the Queue: Disruptor Principles, Architecture, and Source Code Penetration "

" The King of Cache: Caffeine Source Code, Architecture, and Principles (the most complete in history, 10W super long text) "

" The King of Cache: The Use of Caffeine (The Most Complete in History) "

" Java Agent probe, bytecode enhanced ByteBuddy (the most complete in history) "

Realize your interview questions freely:

4000 pages of "Nin's Java Interview Collection" 40 topics

The PDF file update of the above Nien architecture notes and interview questions, ▼Please go to the following [Technical Freedom Circle] official account to get it▼

Guess you like

Origin blog.csdn.net/crazymakercircle/article/details/130896500