天天看點

Spring Cloud Gateway Nacos 實作動态路由

微服務都是互相獨立的,假如我們的網關和其他服務都線上上已經運作了好久,這個時候增加了一個微服務,這個時候要通過網關通路的話需要通過修改配置檔案來增加路由規則,并且需要重新開機項目,是以我們需要實作動态路由

方式一

1、建立路由配置接口

建立路由釋出接口

/**
 * 路由配置服務
 * @author : jiagang
 * @date : Created in 2022/7/20 11:07
 */
public interface RouteService {
    /**
     * 更新路由配置
     *
     * @param routeDefinition
     */
    void update(RouteDefinition routeDefinition);

    /**
     * 添加路由配置
     *
     * @param routeDefinition
     */
    void add(RouteDefinition routeDefinition);
}
           

實作類如下

package com.mdx.gateway.service.impl;

import com.mdx.gateway.service.RouteService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

/**
 * @author : jiagang
 * @date : Created in 2022/7/20 11:10
 */
@Service
@Slf4j
public class RouteServiceImpl implements RouteService, ApplicationEventPublisherAware {

    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;

    /**
     * 事件釋出者
     */
    private ApplicationEventPublisher publisher;

    @Override
    public void update(RouteDefinition routeDefinition) {
        log.info("更新路由配置項:{}", routeDefinition);
        this.routeDefinitionWriter.delete(Mono.just(routeDefinition.getId()));
        routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
    }

    @Override
    public void add(RouteDefinition routeDefinition) {
        log.info("新增路由配置項:{}", routeDefinition);
        routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
    }

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

其中:

  • RouteDefinitionWriter

    :提供了對路由的增加删除等操作
  • ApplicationEventPublisher

    : 是ApplicationContext的父接口之一,他的功能就是釋出事件,也就是把某個事件告訴所有與這個事件相關的監聽器

2、在nacos建立gateway-routes配置檔案

将路由資訊放到nacos的配置檔案下

建立配置檔案,并将order服務的路由添加到配置檔案

Spring Cloud Gateway Nacos 實作動态路由

配置路由如下:

[
    {
        "predicates":[
            {
                "args":{
                    "pattern":"/order/**"
                },
                "name":"Path"
            }
        ],
        "id":"mdx-shop-order",
        "filters":[
            {
                "args":{
                    "parts":1
                },
                "name":"StripPrefix"
            }
        ],
        "uri":"lb://mdx-shop-order",
        "order":1
    }
]
           

這個路由配置對應的就是gateway中的RouteDefinition類

3、在本地配置檔案下配置路由的data-id和group和命名空間

gateway:
  routes:
    config:
      data-id: gateway-routes  #動态路由
      group: shop
      namespace: mdx
           

完整配置檔案(删除或者注釋掉之前配置在本地檔案的路由)

server:
  port: 9010

spring:
  application:
    name: mdx-shop-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        namespace: mdx
        group: mdx
    gateway:
      discovery:
        locator:
          enabled: true  #開啟通過服務中心的自動根據 serviceId 建立路由的功能
  main:
    web-application-type: reactive

gateway:
  routes:
    config:
      data-id: gateway-routes  #動态路由
      group: shop
      namespace: mdx
           

4、建立路由相關配置類

建立配置類引入配置

/**
 * @author : jiagang
 * @date : Created in 2022/7/20 14:44
 */
@ConfigurationProperties(prefix = "gateway.routes.config")
@Component
@Data
public class GatewayRouteConfigProperties {

    private String dataId;
    private String group;
	private String namespace;
}
           

5、執行個體化nacos的ConfigService,交由springbean管理

ConfigService 這個類是nacos的分布式配置接口,主要是用來擷取配置和添加監聽器

由NacosFactory來建立ConfigService

/**
 * 将configService交由spring管理
 * @author : jiagang
 * @date : Created in 2022/7/20 15:27
 */
@Configuration
public class GatewayConfigServiceConfig {

    @Autowired
    private GatewayRouteConfigProperties configProperties;

    @Autowired
    private NacosConfigProperties nacosConfigProperties;

    @Bean
    public ConfigService configService() throws NacosException {
        Properties properties = new Properties();
        properties.setProperty(PropertyKeyConst.SERVER_ADDR, nacosConfigProperties.getServerAddr());
        properties.setProperty(PropertyKeyConst.NAMESPACE, configProperties.getNamespace());
        return NacosFactory.createConfigService(properties);
    }
}
           

6、動态路由主要實作

項目啟動時會加載這個類

@PostConstruc

注解的作用,在spring bean的生命周期依賴注入完成後被調用的方法

package com.mdx.gateway.route;
import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mdx.common.utils.StringUtils;
import com.mdx.gateway.service.RouteService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;

/**
 * @author : jiagang
 * @date : Created in 2022/7/20 15:04
 */
@Component
@Slf4j
@RefreshScope
public class GatewayRouteInitConfig {

    @Autowired
    private GatewayRouteConfigProperties configProperties;

    @Autowired
    private NacosConfigProperties nacosConfigProperties;

    @Autowired
    private RouteService routeService;
    /**
     * nacos 配置服務
     */
    @Autowired
    private ConfigService configService;
    /**
     * JSON 轉換對象
     */
    private final ObjectMapper objectMapper = new ObjectMapper();

    @PostConstruct
    public void init() {
        log.info("開始網關動态路由初始化...");
        try {
            // getConfigAndSignListener()方法 發起長輪詢和對dataId資料變更注冊監聽的操作
            // getConfig 隻是發送普通的HTTP請求
            // String getConfig(String dataId, String group, long timeoutMs) throws NacosException;
            String initConfigInfo = configService.getConfigAndSignListener(configProperties.getDataId(), configProperties.getGroup(), nacosConfigProperties.getTimeout(), new Listener() {
                @Override
                public Executor getExecutor() {
                    return null;
                }

                @Override
                public void receiveConfigInfo(String configInfo) {
                    if (StringUtils.isNotEmpty(configInfo)) {
                        log.info("接收到網關路由更新配置:\r\n{}", configInfo);
                        List<RouteDefinition> routeDefinitions = null;
                        try {
                            routeDefinitions = objectMapper.readValue(configInfo, new TypeReference<List<RouteDefinition>>() {
                            });
                        } catch (JsonProcessingException e) {
                            log.error("解析路由配置出錯," + e.getMessage(), e);
                        }
                        for (RouteDefinition definition : Objects.requireNonNull(routeDefinitions)) {
                            routeService.update(definition);
                        }
                    } else {
                        log.warn("目前網關無動态路由相關配置");
                    }
                }
            });
            log.info("擷取網關目前動态路由配置:\r\n{}", initConfigInfo);
            if (StringUtils.isNotEmpty(initConfigInfo)) {
                List<RouteDefinition> routeDefinitions = objectMapper.readValue(initConfigInfo, new TypeReference<List<RouteDefinition>>() {
                });
                for (RouteDefinition definition : routeDefinitions) {
                    routeService.add(definition);
                }
            } else {
                log.warn("目前網關無動态路由相關配置");
            }
            log.info("結束網關動态路由初始化...");
        } catch (Exception e) {
            log.error("初始化網關路由時發生錯誤", e);
        }
    }

}
           

如果項目啟動時,在釋出路由的時候卡在

this.publisher.publishEvent(new RefreshRoutesEvent(this));

這個地方走不下去,請在

GatewayRouteInitConfig

這個類加

@RefreshScope

注解

7、測試動态路由

前面我們已經把本地的yml中的路由注釋掉了,現在我們來通過gateway服務來掉一個order服務的接口

接口位址:

http://localhost:9010/mdx-shop-order/order/lb

其中9010是網關端口

可以看到路由成功

Spring Cloud Gateway Nacos 實作動态路由

然後我們再在nacos配置中心加一個user服務的路由

[
    {
        "predicates":[
            {
                "args":{
                    "pattern":"/mdx-shop-order/**"
                },
                "name":"Path"
            }
        ],
        "id":"mdx-shop-order",
        "filters":[
            {
                "args":{
                    "parts":1
                },
                "name":"StripPrefix"
            }
        ],
        "uri":"lb://mdx-shop-order",
        "order":1
    },
    {
        "predicates":[
            {
                "args":{
                    "pattern":"/mdx-shop-user/**"
                },
                "name":"Path"
            }
        ],
        "id":"mdx-shop-user",
        "filters":[
            {
                "args":{
                    "parts":1
                },
                "name":"StripPrefix"
            }
        ],
        "uri":"lb://mdx-shop-user",
        "order":2
    }
]
           

然後點釋出

可以看到gateway的監聽器已經監聽到配置的改動

Spring Cloud Gateway Nacos 實作動态路由

不重新啟動gateway的情況下再來通過網關通路下user服務

接口位址:http://localhost:9010/mdx-shop-user/user/getOrderNo?userId=mdx123456

其中9010是網關端口

可以看到成功路由

Spring Cloud Gateway Nacos 實作動态路由

到這裡gateway的使用和nacos動态路由就結束了~

方式二

方式二與方式一實作方式類似,就是nacos 監聽方式不一樣,簡單記錄下

import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.lang.reflect.Field;
import java.util.Map;

@Service
@Slf4j
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {

    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;
    private ApplicationEventPublisher publisher;

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

    //增加路由
    public String add(RouteDefinition definition) {
        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
        return "success";
    }

    //更新路由
    public String update(RouteDefinition definition) {
        try {
            delete(definition.getId());
        } catch (Exception e) {
            log.error("删除路由異常", e);
            return "update fail,not find route  routeId: " + definition.getId();
        }
        try {
            routeDefinitionWriter.save(Mono.just(definition)).subscribe();
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
            return "success";
        } catch (Exception e) {
            log.error("更新路由異常", e);
            return "update route  fail";
        }
    }

    //删除路由
    public String delete(String id) {
        StringBuilder sb = new StringBuilder();
        this.routeDefinitionWriter.delete(Mono.just(id))
                .doOnError(e -> {
                    String s = String.format("删除路由%s失敗", id);
                    log.error(s);
                    sb.append(s);
                }).doOnSuccess(e -> {
                    String s = String.format("删除路由%s成功", id);
                    log.error(s);
                    sb.append(s);
        }).subscribe();
        return sb.toString();
    }

    public Map<String, RouteDefinition> list() {
        Map<String, RouteDefinition> routes = Maps.newHashMap();
        InMemoryRouteDefinitionRepository in = (InMemoryRouteDefinitionRepository) routeDefinitionWriter;

        try {
            Field f = InMemoryRouteDefinitionRepository.class.getDeclaredField("routes");
            f.setAccessible(true);
            routes = (Map<String, RouteDefinition>) f.get(in);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return routes;
    }
}
           
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.api.common.Constants;
import com.alibaba.nacos.api.config.listener.AbstractListener;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.stereotype.Component;

import java.net.URI;
import java.util.List;
import java.util.Map;

@Component
@Slf4j
public class NacosDynamicSupport implements InitializingBean {

    @Value("#{'${spring.application.name}'.concat('.route')}")
    private String route;
    
    @Autowired
    private DynamicRouteServiceImpl dynamicRouteService;

    @Autowired
    private NacosConfigManager nacosConfigManager;

    @Override
    public void afterPropertiesSet() throws Exception {
        nacosConfigManager.getConfigService().addListener(route, Constants.DEFAULT_GROUP, new AbstractListener() {
            @Override
            public void receiveConfigInfo(String config) {
                log.error("mgateway:{}", config);
                if(StringUtils.isBlank(config)) {
                    return;
                }
                List<RouteDefinition> routeDefinitions =  JSON.parseArray(config, RouteDefinition.class);

                Map<String, RouteDefinition> oldRouteMap = dynamicRouteService.list();
                Map<String, RouteDefinition> newRouteMap = Maps.newHashMap();

                for(RouteDefinition definition: routeDefinitions) {
                    String id = definition.getId();
                    newRouteMap.put(id, definition);

                    if(!oldRouteMap.containsKey(id)) {
                        dynamicRouteService.add(definition);
                    } else if(!definition.equals(oldRouteMap.get(id))) {
                        dynamicRouteService.update(definition);
                    }
                }

                oldRouteMap.forEach((k,v) -> {
                    if(!newRouteMap.containsKey(k)) {
                       log.error(dynamicRouteService.delete(k));
                    }
                });
            }
        });

        String routeJson = nacosConfigManager.getConfigService().getConfig(route, Constants.DEFAULT_GROUP, 5000);
        if(StringUtils.isNotBlank(routeJson)) {
            List<RouteDefinition> routeDefinitions = JSON.parseArray(routeJson, RouteDefinition.class);
            for (RouteDefinition definition : routeDefinitions) {
                this.dynamicRouteService.add(definition);
            }
        }
    }
}
           
參考:https://blog.csdn.net/qq_38374397/article/details/125874882

繼續閱讀