SpringCloudGateway dynamic forwarding backend service

The core function of the API gateway is to unify traffic entry and realize routing and forwarding. Spring Cloud Gateway is one of the technologies developed by the API gateway. In addition, Kong and ApiSix are more popular, both of which are based on the OpenResty technology stack.

Simple routing and forwarding can be implemented through the configuration file of Spring Cloud Gateway. In some business scenarios, it is necessary to dynamically replace the backend service address in the routing configuration. Configuration files alone cannot meet this requirement.

This article introduces a way to save the routing configuration in the database, and dynamically read the backend service address from the database according to the specific conditions of the interface request to achieve flexible forwarding.

For specific code, refer to the sample project https://github.com/qihaiyan/springcamp/tree/master/spring-cloud-gateway

I. Overview

By saving the relevant routing configuration rules of Spring Cloud Gateway to the database, the routing can be dynamically and flexibly adjusted. In the implementation of this article, we dynamically select the corresponding backend service address by requesting a specific value in the header.

2. Add dependencies to the project

Add dependencies in the project's gradle.

build.gradle:

plugins {
    
    
    id 'org.springframework.boot' version '3.0.2'
    id 'io.spring.dependency-management' version '1.1.0'
    id 'java'
}

group = 'cn.springcamp'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    
    
    compileOnly {
    
    
        extendsFrom annotationProcessor
    }
    testCompileOnly {
    
    
        extendsFrom testAnnotationProcessor
    }
}

repositories {
    
    
    mavenCentral()
}

dependencies {
    
    
    implementation "org.springframework.boot:spring-boot-starter-json"
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'io.r2dbc:r2dbc-h2'
    annotationProcessor 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
    testImplementation "org.springframework.boot:spring-boot-starter-test"
    testImplementation 'org.junit.vintage:junit-vintage-engine'
    testImplementation 'io.projectreactor:reactor-test'
    testImplementation 'com.h2database:h2'
    testImplementation 'io.r2dbc:r2dbc-h2'
    testImplementation 'org.junit.vintage:junit-vintage-engine'
}

dependencyManagement {
    
    
    imports {
    
    
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:2022.0.1"
    }
}

test {
    
    
    useJUnitPlatform()
}

Since Spring Cloud Gateway is built based on Spring Web Flux technology, the database configuration in dependencies needs to use r2dbc.

3. Configuration file

The sample program prefers to configure the basic routing through the configuration file, the configuration file code:

spring:
  r2dbc:
    url: r2dbc:h2:mem:///testdb?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    username: sa
    password:
  cloud:
    gateway:
      routes:
        - id: routeOne
          predicates:
            - Path=/route1/**
          uri: no://op
          filters:
            - UriHostPlaceholderFilter=10001
        - id: routeTwo
          predicates:
            - Path=/route2/**
          uri: no://op
          filters:
            - UriHostPlaceholderFilter=10001

Two routes are configured in the configuration file, and the corresponding interface address paths are /route1/**and Path=/route2/**. The in the path ***means fuzzy matching, as long as /route1/the path is prefixed with , it can be accessed.

The backend service address is configured with an unintentional address: uri: no://op, because our processing logic will dynamically replace the backend service address by reading the configuration from the database.

3. Dynamic routing data storage format

We use ROUTE_FILTER_ENTITYthis database table to store interface backend service configuration data. The table structure is:

CREATE TABLE "ROUTE_FILTER_ENTITY"
(
   id VARCHAR(255) PRIMARY KEY,
   route_id VARCHAR(255),  -- 路由ID,对应配置文件中的 ```id```配置项
   code VARCHAR(255), -- 接口请求header中的code参数的值
   url VARCHAR(255) -- 后端服务地址
);

When the client accesses /route1/testthe interface , according to the routing configuration of the configuration file, Spring Cloud Gateway will hit id: routeOnethis routing rule. The back-end service address corresponding to this rule is uri: no://opnot the real back-end service address we expect.

Therefore, we need to read the real backend service address and forward the request to this address. According to the routeId and the value of the code parameter in the header of the interface request, the value of urlthe .

We have read the backend service address, and we need to forward the request to this address. The forwarding method is introduced below.

4. Backend service dynamic forwarding

Dynamic forwarding is implemented by customizing the filter. The custom filter code is as follows:

@Component
public class UriHostPlaceholderFilter extends AbstractGatewayFilterFactory<UriHostPlaceholderFilter.Config> {
    
    
    @Autowired
    private RouteFilterRepository routeFilterRepository;

    public UriHostPlaceholderFilter() {
    
    
        super(Config.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {
    
    
        return Collections.singletonList("order");
    }

    @Override
    public GatewayFilter apply(Config config) {
    
    
        return new OrderedGatewayFilter((exchange, chain) -> {
    
    
            String code = exchange.getRequest().getHeaders().getOrDefault("code", new ArrayList<>()).stream().findFirst().orElse("");
            String routeId = exchange.getAttribute(GATEWAY_PREDICATE_MATCHED_PATH_ROUTE_ID_ATTR);
            if (StringUtils.hasText(code)) {
    
    
                String newurl;
                try {
    
    
                    newurl = routeFilterRepository.findByRouteIdAndCode(routeId, code).toFuture().get().getUrl();
                } catch (InterruptedException | ExecutionException e) {
    
    
                    throw new RuntimeException(e);
                }
                if (StringUtils.hasText(exchange.getRequest().getURI().getQuery())) {
    
    
                    newurl = newurl + "?" + exchange.getRequest().getURI().getQuery();
                }
                URI newUri = null;
                try {
    
    
                    newUri = new URI(newurl);
                } catch (URISyntaxException e) {
    
    
                    log.error("uri error", e);
                }

                exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newUri);
            }
            return chain.filter(exchange);
        }, config.getOrder());
    }

    @Data
    @NoArgsConstructor
    public static class Config {
    
    
        private int order;

        public Config(int order) {
    
    
            this.order = order;
        }
    }
}

By extending the AbstractGatewayFilterFactory class, we have customized the UriHostPlaceholderFilter filter.

The core logic of the code is in the apply method.

First, String code = exchange.getRequest().getHeaders().getOrDefault("code", new ArrayList<>()).stream().findFirst().orElse("")you can .

Then String routeId = exchange.getAttribute(GATEWAY_PREDICATE_MATCHED_PATH_ROUTE_ID_ATTR)you can .

Finally, you newurl = routeFilterRepository.findByRouteIdAndCode(routeId, code).toFuture().get().getUrl()can .

After getting the backend service address, exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newUri);forward .

4. Unit testing

In the unit test code, we preset a piece of backend service dynamic configuration data:

insert into ROUTE_FILTER_ENTITY values('1','routeOne','alpha','http://httpbin.org/anything')

Then simulate the request /route1/test?a=testto this interface, according to our configuration, the request will be forwarded to http://httpbin.org/anything.

After executing the unit test, it can be found from the log that the data returned by the interface is the data returned by the backend service http://httpbin.org/anything.

When we want to adjust the backend service address, we only need to change the url field in this piece of configuration data in the ROUTE_FILTER_ENTITY table to any other service address, which greatly increases the flexibility of the program.

Guess you like

Origin blog.csdn.net/haiyan_qi/article/details/129112234