1. 程式人生 > >Spring Cloud Alibaba | Nacos動態閘道器路由

Spring Cloud Alibaba | Nacos動態閘道器路由

Spring Cloud Alibaba | Gateway基於Nacos動態閘道器路由

本篇實戰所使用Spring有關版本:

SpringBoot:2.1.7.RELEASE

Spring Cloud:Greenwich.SR2

Spring CLoud Alibaba:2.1.0.RELEASE

前面幾篇文章我們介紹了《Nacos服務註冊與發現》和《Nacos配置管理》,還沒看過的小夥伴們快去看一下,本篇文章是建立在這兩篇文章基礎上的一次實戰。

背景介紹

在Spring Cloud微服務體系下,常用的服務閘道器有Netflix公司開源的Zuul,還有Spring Cloud團隊自己開源的Spring Cloud Gateway,其中NetFlix公司開源的Zuul版本已經迭代至2.x,但是Spring Cloud並未整合,目前Spring Cloud整合的Spring Cloud Zuul還是Zuul1.x,這一版的Zuul是基於Servlet

構建的,採用的方案是阻塞式的多執行緒方案,即一個執行緒處理一次連線請求,這種方式在內部延遲嚴重、裝置故障較多情況下會引起存活的連線增多和執行緒增加的情況發生。Spring Cloud自己開源的Spring Cloud Gateway則是基於Spring Webflux來構建的,Spring Webflux有一個全新的非堵塞的函式式 Reactive Web 框架,可以用來構建非同步的、非堵塞的、事件驅動的服務,在伸縮性方面表現非常好。使用非阻塞API, Websockets得到支援,並且由於它與Spring緊密整合,將會得到更好的開發體驗。

本文將基於Gateway服務閘道器來介紹如何使用Nacos的配置功能來實現服務閘道器動態路由。

實現方案

在開始之前我們先介紹一下具體實現方式:

  1. 路由資訊不再配置在配置檔案中,將路由資訊配置在Nacos的配置中。
  2. 在服務閘道器Spring Cloud Gateway中開啟監聽,監聽Nacos配置檔案的修改。
  3. Nacos配置檔案一旦發生改變,則Spring Cloud Gateway重新重新整理自己的路由資訊。

環境準備

首先,需要準備一個Nacos服務,我這裡的版本是使用的Nacos v1.1.3,如果不會配置Nacos服務的同學,請參考之前的文章《Nacos服務中心初探》

工程實戰

建立工程gateway-nacos-config,工程依賴pom.xml如下:

<?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.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.springcloud.alibaba</groupId>
    <artifactId>gateway-nacos-config</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gateway-nacos-config</name>
    <description>gateway-nacos-config</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
        <spring-cloud-alibaba.version>2.1.0.RELEASE</spring-cloud-alibaba.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</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>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </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>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/libs-milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

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

</project>
  • 在使用Spring Cloud Alibaba元件的時候,在<dependencyManagement>中需配置spring-cloud-alibaba-dependencies,它管理了Spring Cloud Alibaba元件的版本依賴。

配置檔案application.yml如下:

server:
  port: 8080
spring:
  application:
    name: spring-cloud-gateway-server
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.44.129:8848
management:
  endpoints:
    web:
      exposure:
        include: '*'
  • spring.cloud.nacos.discovery.server-addr:配置為Nacos服務地址,格式為ip:port

接下來進入核心部分,配置Spring Cloud Gateway動態路由,這裡需要實現一個Spring提供的事件推送介面ApplicationEventPublisherAware,程式碼如下:

@Component
public class DynamicRoutingConfig implements ApplicationEventPublisherAware {

    private final Logger logger = LoggerFactory.getLogger(DynamicRoutingConfig.class);

    private static final String DATA_ID = "zuul-refresh-dev.json";
    private static final String Group = "DEFAULT_GROUP";

    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;

    private ApplicationEventPublisher applicationEventPublisher;

    @Bean
    public void refreshRouting() throws NacosException {
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, "192.168.44.129:8848");
        properties.put(PropertyKeyConst.NAMESPACE, "8282c713-da90-486a-8438-2a5a212ef44f");
        ConfigService configService = NacosFactory.createConfigService(properties);
        configService.addListener(DATA_ID, Group, new Listener() {
            @Override
            public Executor getExecutor() {
                return null;
            }

            @Override
            public void receiveConfigInfo(String configInfo) {
                logger.info(configInfo);

                boolean refreshGatewayRoute = JSONObject.parseObject(configInfo).getBoolean("refreshGatewayRoute");

                if (refreshGatewayRoute) {
                    List<RouteEntity> list = JSON.parseArray(JSONObject.parseObject(configInfo).getString("routeList")).toJavaList(RouteEntity.class);

                    for (RouteEntity route : list) {
                        update(assembleRouteDefinition(route));
                    }
                } else {
                    logger.info("路由未發生變更");
                }


            }
        });
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    /**
     * 路由更新
     * @param routeDefinition
     * @return
     */
    public void update(RouteDefinition routeDefinition){

        try {
            this.routeDefinitionWriter.delete(Mono.just(routeDefinition.getId()));
            logger.info("路由更新成功");
        }catch (Exception e){
            logger.error(e.getMessage(), e);
        }

        try {
            routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
            this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
            logger.info("路由更新成功");
        }catch (Exception e){
            logger.error(e.getMessage(), e);
        }
    }

    public RouteDefinition assembleRouteDefinition(RouteEntity routeEntity) {

        RouteDefinition definition = new RouteDefinition();

        // ID
        definition.setId(routeEntity.getId());

        // Predicates
        List<PredicateDefinition> pdList = new ArrayList<>();
        for (PredicateEntity predicateEntity: routeEntity.getPredicates()) {
            PredicateDefinition predicateDefinition = new PredicateDefinition();
            predicateDefinition.setArgs(predicateEntity.getArgs());
            predicateDefinition.setName(predicateEntity.getName());
            pdList.add(predicateDefinition);
        }
        definition.setPredicates(pdList);

        // Filters
        List<FilterDefinition> fdList = new ArrayList<>();
        for (FilterEntity filterEntity: routeEntity.getFilters()) {
            FilterDefinition filterDefinition = new FilterDefinition();
            filterDefinition.setArgs(filterEntity.getArgs());
            filterDefinition.setName(filterEntity.getName());
            fdList.add(filterDefinition);
        }
        definition.setFilters(fdList);

        // URI
        URI uri = UriComponentsBuilder.fromUriString(routeEntity.getUri()).build().toUri();
        definition.setUri(uri);

        return definition;
    }
}

這裡主要介紹一下refreshRouting()這個方法,這個方法主要負責監聽Nacos的配置變化,這裡先使用引數構建一個ConfigService,再使用ConfigService開啟一個監聽,並且在監聽的方法中重新整理路由資訊。

Nacos配置如圖:

{
    "refreshGatewayRoute":false,
    "routeList":[
        {
            "id":"github_route",
            "predicates":[
                {
                    "name":"Path",
                    "args":{
                        "_genkey_0":"/meteor1993"
                    }
                }
            ],
            "filters":[

            ],
            "uri":"https://github.com",
            "order":0
        }
    ]
}

配置格式選擇JSON,Data ID和Group與程式中的配置保持一致,注意,我這裡的程式配置了namespace,如果使用預設namespace,可以不用配置。

這裡配置了一個路由/meteor1993,直接訪問這個路由會訪問到作者的Github倉庫。

剩餘部分的程式碼這裡就不一一展示了,已經上傳至程式碼倉庫,有需要的同學可以自行取用。

測試

啟動工程,這時是沒有任何路由資訊的,開啟瀏覽器訪問:http://localhost:8080/meteor1993 ,頁面返回404報錯資訊,如圖:

同時,也可以訪問連結:http://localhost:8080/actuator/gateway/routes ,可以看到如下列印:

[]

開啟在Nacos Server端的UI介面,選擇監聽查詢,選擇namespace為springclouddev的欄目,輸入DATA_ID為zuul-refresh-dev.json和Group為DEFAULT_GROUP,點選查詢,可以看到我們啟動的工程gateway-nacos-config正在監聽Nacos Server端,如圖:

筆者這裡的本地ip為:192.168.44.1。監聽正常,這時,我們修改剛才建立的配置,將裡面的refreshGatewayRoute修改為true,如下:

{"refreshGatewayRoute": true, "routeList":[{"id":"github_route","predicates":[{"name":"Path","args":{"_genkey_0":"/meteor1993"}}],"filters":[],"uri":"https://github.com","order":0}]}

點擊發布,可以看到工程gateway-nacos-config的控制檯列印日誌如下:

2019-09-02 22:09:49.254  INFO 8056 --- [38-2a5a212ef44f] c.s.a.g.config.DynamicRoutingConfig      : {
    "refreshGatewayRoute":true,
    "routeList":[
        {
            "id":"github_route",
            "predicates":[
                {
                    "name":"Path",
                    "args":{
                        "_genkey_0":"/meteor1993"
                    }
                }
            ],
            "filters":[

            ],
            "uri":"https://github.com",
            "order":0
        }
    ]
}
2019-09-02 22:09:49.268  INFO 8056 --- [38-2a5a212ef44f] c.s.a.g.config.DynamicRoutingConfig      : 路由更新成功

這時,我們的工程gateway-nacos-config的路由已經更新成功,訪問路徑:http://localhost:8080/actuator/gateway/routes ,可以看到如下列印:

[{"route_id":"github_route","route_definition":{"id":"github_route","predicates":[{"name":"Path","args":{"_genkey_0":"/meteor1993"}}],"filters":[],"uri":"https://github.com","order":0},"order":0}]

我們再次在瀏覽器中訪問連結:http://localhost:8080/meteor1993 ,可以看到頁面正常路由到Github倉庫,如圖:

總結

至此,Nacos動態閘道器路由就介紹完了,主要運用了服務閘道器端監聽Nacos配置改變的功能,實現服務閘道器路由配置動態重新整理,同理,我們也可以使用服務閘道器Zuul來實現基於Nacos的動態路由功能。

基於這個思路,我們可以使用配置中心來實現閘道器的動態路由,而不是使用服務閘道器本身自帶的配置檔案,這樣每次路由資訊變更,無需修改配置檔案而後重啟服務。

目前市面上使用比較多的配置中心有攜程開源的Apollo,服務閘道器還有Spring Cloud Zuul,下一篇文章我們介紹如何使用Apollo來實現Spring Cloud Zuul的動態路由。

示例程式碼

Github-示例程式碼

Gitee-示例代