Welcome to my GitHub

Github.com/zq2599/blog…

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

This paper gives an overview of

  • This article is the third “Spring Cloud Gateway combat” series, the previous article introduces a variety of routing configuration methods, they have a common problem: the route configuration change must restart the Gateway application to take effect, smart you see the key problem: this is not suitable for the production environment!
  • How do I make the changed routes take effect immediately without restarting the application? That’s today’s topic: dynamic routing

Design ideas

  • Put the configuration on nacOS, write a listener to listen for configuration changes on NACOS, and update the changed configuration to the Gateway application process:
  • This idea is embodied in the following three classes:
  1. Encapsulate the code that manipulates routes into a class named RouteOperator, which is used to delete and add intra-process routes
  2. Make a configuration class RouteOperatorConfig that registers RouteOperator as a bean in the Spring environment
  3. The RouteOperator method is called to update the route in the process. The code that listens for the NACOS configuration and calls RouteOperator is placed in the RouteConfigListener class
  • Yml + gateway-dynamic-by-nacOS is the familiar classic configuration. Bootstrap. yml is the local configuration of nacOS. Gateway-dynamic-by-nacos in naOCS, which contains the configuration required by the entire application (such as service port number, database, etc.), there is also a configuration file in nacOS, named gateway-json-routes, which is json format, which contains the routing configuration. JSON is chosen because it is easier to parse and process than YML.

  • Finally, the entire microservices architecture is shown below:

  • I’m clear. Start coding

Download the source code

  • The full source code for this article can be downloaded at GitHub with the following address and link information (github.com/zq2599/blog…
The name of the link note
Project home page Github.com/zq2599/blog… The project’s home page on GitHub
Git repository address (HTTPS) Github.com/zq2599/blog… The project source warehouse address, HTTPS protocol
Git repository address (SSH) [email protected]:zq2599/blog_demos.git The project source warehouse address, SSH protocol
  • The git project has multiple folders. The source code for this project is in the spring-cloud-tutorials folder, as shown in the red box below:

  • Spring-cloud-tutorials were the parent project and several sub-projects, and today’s code is gateway-dynamic-by-nacos, as shown below:

coding

  • Add a project named gateway-dynamic-by-nacos. The pom. XML content of the project is as follows.

      
<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>spring-cloud-tutorials</artifactId>
        <groupId>com.bolingcavalry</groupId>
        <version>1.0 the SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>gateway-dynamic-by-nacos</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.bolingcavalry</groupId>
            <artifactId>common</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>

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

        <! Springboot content breakpoint exposed -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <! When using bootstrap.yml, this dependency must have -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>

        <! -- Routing policy uses LB in such a way that this dependency must have -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

        <! -- NacOS: Configuration center -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <! -- NACOS: Registry -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <! -- If the parent project is not Springboot, you need to use the plugin in the following way to generate a normal JAR -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.bolingcavalry.gateway.GatewayApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
Copy the code
  • The bootstrap.yml configuration file contains only nacos. Other configuration information still comes from naOCs:
spring:
  application:
    name: gateway-dynamic-by-nacos
  cloud:
    nacos:
      config:
        server-addr: 127.0. 01.: 8848
        file-extension: yml
        group: DEFAULT_GROUP
Copy the code
  • The class that handles the configuration of the in-process route is RouteOperator, as shown below. The entire configuration is a string, deserialized with Jackson’s ObjectMapper (note that the configuration file was yML in the previous field, but in this case it is JSON). Later on the nacos configuration to use JSON format), and then the routing configuration process is mainly done RouteDefinitionWriter type of bean, in order to make configuration takes effect immediately, with applicationEventPublisher post messages within a process:
package com.bolingcavalry.gateway.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;

@Slf4j
public class RouteOperator {
    private ObjectMapper objectMapper;

    private RouteDefinitionWriter routeDefinitionWriter;

    private ApplicationEventPublisher applicationEventPublisher;

    private static final List<String> routeList = new ArrayList<>();

    public RouteOperator(ObjectMapper objectMapper, RouteDefinitionWriter routeDefinitionWriter, ApplicationEventPublisher applicationEventPublisher) {
        this.objectMapper = objectMapper;
        this.routeDefinitionWriter = routeDefinitionWriter;
        this.applicationEventPublisher = applicationEventPublisher;
    }

    /** * Clears all routes in the collection and empties the collection */
    private void clear(a) {
        // Call the API to clean it up
        routeList.stream().forEach(id -> routeDefinitionWriter.delete(Mono.just(id)).subscribe());
        // Clear the collection
        routeList.clear();
    }

    /** * Add route *@param routeDefinitions
     */
    private void add(List<RouteDefinition> routeDefinitions) {

        try {
            routeDefinitions.stream().forEach(routeDefinition -> {
                routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
                routeList.add(routeDefinition.getId());
            });
        } catch(Exception exception) { exception.printStackTrace(); }}/** * Update route */
    private void publish(a) {
        applicationEventPublisher.publishEvent(new RefreshRoutesEvent(routeDefinitionWriter));
    }

    /** * Update all routing information *@param configStr
     */
    public void refreshAll(String configStr) {
        log.info("start refreshAll : {}", configStr);
        // Invalid strings are not processed
        if(! StringUtils.hasText(configStr)) { log.error("invalid string for route config");
            return;
        }

        // Deserialize with Jackson
        List<RouteDefinition> routeDefinitions = null;

        try {
            routeDefinitions = objectMapper.readValue(configStr, new TypeReference<List<RouteDefinition>>(){});
        } catch (JsonProcessingException e) {
            log.error("get route definition from nacos string error", e);
        }

        // If the value is null, the deserialization fails
        if (null==routeDefinitions) {
            return;
        }

        // Clear all current routes
        clear();

        // Add the latest route
        add(routeDefinitions);

        // Publish via in-app messages
        publish();

        log.info("finish refreshAll"); }}Copy the code
  • Make a configuration class routeOperatorConfig. Java and register the instantiated RouteOperator with the Spring environment: routeOperatorConfig. Java
package com.bolingcavalry.gateway.config;

import com.bolingcavalry.gateway.service.RouteOperator;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RouteOperatorConfig {
    @Bean
    public RouteOperator routeOperator(ObjectMapper objectMapper, RouteDefinitionWriter routeDefinitionWriter, ApplicationEventPublisher applicationEventPublisher) {

        return newRouteOperator(objectMapper, routeDefinitionWriter, applicationEventPublisher); }}Copy the code
  • Finally is RouteConfigListener nacos listening in class, key technical points are visible ConfigService. AddListener, used to add to monitor, there is the update after the configuration change routing logic, another important step: Call getConfig immediately to get the current configuration, refresh the current process routing configuration:
package com.bolingcavalry.gateway.service;

import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.Executor;

@Component
@Slf4j
public class RouteConfigListener {

    private String dataId = "gateway-json-routes";

    private String group = "DEFAULT_GROUP";

    @Value("${spring.cloud.nacos.config.server-addr}")
    private String serverAddr;

    @Autowired
    RouteOperator routeOperator;

    @PostConstruct
    public void dynamicRouteByNacosListener(a) throws NacosException {

        ConfigService configService = NacosFactory.createConfigService(serverAddr);

        // Add a listener. Configuration changes on NACOS will be executed
        configService.addListener(dataId, group, new Listener() {

            public void receiveConfigInfo(String configInfo) {
                // All parsing and processing is done by RouteOperator
                routeOperator.refreshAll(configInfo);
            }

            public Executor getExecutor(a) {
                return null; }});// Get the current configuration
        String initConfig = configService.getConfig(dataId, group, 5000);

        // Update immediatelyrouteOperator.refreshAll(initConfig); }}Copy the code
  • Routeconfiglistener. Java also contains the dataId value gateway-json-routes. This is the name of the nacos configuration file that we will use later in nacos configuration

  • Finally, there’s the bland startup category:

package com.bolingcavalry.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) { SpringApplication.run(GatewayApplication.class,args); }}Copy the code
  • With the coding done, add two configurations on NACOS;

  • The first configuration, named gateway-dynamic-by-nacos, reads as follows:

server:
  port: 8086

# expose endpoints
management:
  endpoints:
    web:
      exposure:
        include: The '*'
  endpoint:
    health:
      show-details: always
Copy the code
  • Gateway -json-routes = gateway-json-routes = gateway-json-routes = gateway-json-routes = gateway-json-routes = gateway-json-routes
[{"id": "path_route_addr"."uri": "http://127.0.0.1:8082"."predicates":[
            {
                "name": "Path"."args": {
                    "pattern": "/hello/**"}}]}]Copy the code
  • At this point, we have completed the development work, next to verify whether dynamic routing can achieve the desired effect, I use the client tool postman

validation

  • Ensure that services such as NACOS, provider-Hello, and gateway-dynamic-by-nacOS are all started:

  • With the postman to http://127.0.0.1:8086/hello/str, can access to the normal, prove the Gateway application has been downloaded from nacos smooth route:

  • At this time if you visit http://127.0.0.1:8086/lbtest/str should fail, because there is no nacos configuration this path routing, the following figure, so he failed:

  • In nacOS, modify the content of gateway-json-routes and add the path_route_lb route configuration. After modification, the complete configuration is as follows:
[{"id": "path_route_addr"."uri": "http://127.0.0.1:8082"."predicates":[
            {
                "name": "Path"."args": {
                    "pattern": "/hello/**"}}]}, {"id": "path_route_lb"."uri": "lb://provider-hello"."predicates":[
            {
                "name": "Path"."args": {
                    "pattern": "/lbtest/**"}}]}]Copy the code
  • After clicking the publish button in the lower right corner, the console of the gateway-dynamic-by-nacos application immediately outputs the following message, indicating that the listener is in effect:
The 2021-08-15 19:39:45. 18736-883 the INFO [- 127.0.0.1 _8848] C.A.N.C lient. Config. Impl. ClientWorker: [fixed-127.0.0.1_8848] [polling-resp] config changed. dataId=gateway-json-routes, Group = DEFAULT_GROUP 19:39:45 2021-08-15. 18736-883 the INFO [- 127.0.0.1 _8848] C.A.N.C lient. Config. Impl. ClientWorker: Get changedGroupKeys:[gateway-json-routes+DEFAULT_GROUP] 2021-08-15 19:39:45.890 INFO 18736 -- [-127.0.0.1_8848] c.a.n.client.config.impl.ClientWorker : [fixed-127.0.0.1_8848] [data-received] dataId=gateway-json-routes, group=DEFAULT_GROUP, tenant=null, md5=54fb76dcad838917818d0160ce2bd72f, content=[ { "id": "path_route_addr", "uri": "Http://127.0.0.1:8082", "predicates... Type = json 19:39:45 2021-08-15. 18736-891 the INFO [- 127.0.0.1 _8848] C.B.G ateway. Service. RouteOperator: Start refreshAll: [{" id ":" path_route_addr ", "uri" : "http://127.0.0.1:8082", "predicates" : [{" name ": "Path", "args": { "pattern": "/hello/**" } } ] } , { "id": "path_route_lb", "uri": "lb://provider-hello", "predicates":[ { "name": "Path", "args": { "pattern": "/ lbtest / * *"}}}]] 19:39:45 2021-08-15. 18736-894 the INFO [- 127.0.0.1 _8848] C.B.G ateway. Service. RouteOperator: Finish refreshAll 19:39:45 2021-08-15. 18736-894 the INFO [- 127.0.0.1 _8848] C.A.N a cosine. Client. Config. Impl. CacheData: [fixed - 127.0.0.1 _8848] [notify - ok] dataId = gateway - json - routes, group = DEFAULT_GROUP, md5=54fb76dcad838917818d0160ce2bd72f, Listener = com. Bolingcavalry. Gateway. Service. RouteConfigListener $1 @ 123 ae1f6 19:39:45 2021-08-15. 18736-894 the INFO [- 127.0.0.1 _8848] C.A.N a cosine. Client. Config. Impl. CacheData: [fixed-127.0.0.1_8848] [notify-listener] time cost=3ms in ClientWorker, dataId=gateway-json-routes, group=DEFAULT_GROUP, md5=54fb76dcad838917818d0160ce2bd72f, listener=com.bolingcavalry.gateway.service.RouteConfigListener$1@123ae1f6Copy the code
  • Select * from ‘postman’ where ‘postman’ = ‘postman’;

  • Because it relies on the Spring-boot-starter -actuator library and the configuration files are added, we can also check the internal configuration of the SpringBoot application. Use the browser to http://localhost:8086/actuator/gateway/routes, the configuration of the latest situation, the following figure:

  • At this point, the development and verification of dynamic routing has been completed, I hope this practical function can give you some reference, develop a more flexible and practical gateway service;

You are not alone, Xinchen original accompany all the way

  1. Java series
  2. Spring series
  3. The Docker series
  4. Kubernetes series
  5. Database + middleware series
  6. The conversation series

Welcome to pay attention to the public number: programmer Xin Chen

Wechat search “programmer Xin Chen”, I am Xin Chen, looking forward to enjoying the Java world with you…

Github.com/zq2599/blog…