springboot+spring cloud gateway开发,配置动态路由

发布时间 2023-06-20 21:37:16作者: 夏威夷8080

所谓的动态路由,就是可以根据运行时环境(负载情况、头信息、版本号),动态的修改路由规则,从而转发到不同的目标服务上。

动态路由是相对于传统的静态路由而言的,静态路由一旦配置好之后需求有变动,就很难进行灵活的调整。

Spring Cloud Gateway 或 Zuul 都可以实现动态路由,本文以Spring Cloud Gateway 为例。

 

在写代码前,有必要对Spring Cloud Gateway 里的下面2个类做个了解。

1、org.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository
它是一个路由定义存储仓库,里面有一些增删改查方法,看类名就知道是存在内存里的。它实现了RouteDefinitionRepository接口,实际生产环境中,路由定义集合肯定是要持久化存储的,所以我们的任务就是要改造它。
2、org.springframework.cloud.gateway.route.RouteDefinition
可以把它理解为路由定义的bean,它包含了路由条件和路由行为等,仓库里存的就是它,如果你觉得它不满足你的需求,你也可以继承它,加一些自定义属性。

我们可以选择把路由信息存到mysql里,所以要建张路由信息表,前缀我的昵称夏威夷的首字母

CREATE TABLE `xwy_gateway_route` (
    `route_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '路由ID',
    `route_name` VARCHAR(255) NOT NULL COMMENT '路由名称',
    `app_name` VARCHAR(255) NULL DEFAULT NULL COMMENT '应用名称,也是路由标识',
    `route_path` VARCHAR(255) NULL DEFAULT NULL COMMENT '路径',
    `service_id` VARCHAR(255) NULL DEFAULT NULL COMMENT '服务ID',
    `strip_prefix` TINYINT(3) NOT NULL DEFAULT '1' COMMENT '忽略前缀',
    `remark` VARCHAR(255) NULL DEFAULT NULL COMMENT '备注',
    `create_time` DATETIME NULL DEFAULT NULL COMMENT '创建时间',
    `update_time` DATETIME NULL DEFAULT NULL COMMENT '修改时间',
    PRIMARY KEY (`route_id`) USING BTREE
)
COMMENT='网关路由表'
ENGINE=InnoDB
AUTO_INCREMENT=1
;

然后生成一个对应的路由信息entity,这边就不列代码了,假设类名就叫XwyGatewayRoute。

这边就不引入mybatis了,直接用JdbcTemplate操作。

import java.net.URI;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.event.EventListener;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public class MyRouteDefinitionLocator implements ApplicationEventPublisherAware {
  private static final Logger log = LoggerFactory.getLogger(MyRouteDefinitionLocator.class);
  
  @Autowired
  private InMemoryRouteDefinitionRepository repository;
  
  @Autowired
  private JdbcTemplate jdbcTemplate;
  
  @Autowired
  private ApplicationEventPublisher applicationEventPublisher;
  
  private static final String SELECT_ROUTES = "SELECT * FROM xwy_gateway_route WHERE status = 1 AND is_delete = 1";
  
  public MyRouteDefinitionLocator(JdbcTemplate jdbcTemplate, InMemoryRouteDefinitionRepository repository) {
    this.jdbcTemplate = jdbcTemplate;
    this.repository = repository;
  }
  
  public Mono<Void> refresh() {
    loadRoutes();
    return Mono.empty();
  }
  
  private Mono<Void> loadRoutes() {
    List<XwyGatewayRoute> routeList = this.jdbcTemplate.query("SELECT * FROM xwy_gateway_route", new RowMapper<XwyGatewayRoute>() {
          public XwyGatewayRoute mapRow(ResultSet rs, int i) throws SQLException {
            XwyGatewayRoute result = new XwyGatewayRoute();
            result.setRouteName(rs.getString("route_name"));
            result.setAppName(rs.getString("app_name"));
            result.setPath(rs.getString("path"));
            result.setServiceId(rs.getString("service_id"));
            result.setStripPrefix(Integer.valueOf(rs.getInt("strip_prefix")));return result;
          }
        });
    if (routeList != null)
  log.info("=============准备加载动态路由=============="); routeList.forEach(routeDTO
-> {
            // RouteDefinitionVo继承自RouteDefinition
RouteDefinitionVo vo
= new RouteDefinitionVo();
// PredicateDefinition用于定义路由谓词(Predicate)及其参数,

              // PredicateDefinition类提供了以下属性:

              // name:路由谓词名称,例如Path、Method、Header等。
              // args:一个Map类型的对象,用于存储路由谓词的参数。通常包含一些键值对,例如path、method、header等。

            List<PredicateDefinition> predicates = new ArrayList<>();
            List<FilterDefinition> filters = new ArrayList<>();
            vo.setId(routeDTO.getApplicationName());
            vo.setRouteName(routeDTO.getRouteName());
            PredicateDefinition predicatePath = new PredicateDefinition();
            Map<String, String> predicatePathParams = new HashMap<>(8);
            predicatePath.setName("Path");
            predicatePathParams.put("name", StringUtils.isBlank(routeDTO.getRouteName());
            predicatePathParams.put("pattern", routeDTO.getPath());
            predicatePath.setArgs(predicatePathParams);
            predicates.add(predicatePath);
            URI uri = UriComponentsBuilder.fromUriString("lb://" + routeDTO.getServiceId()).build().toUri();
            FilterDefinition stripPrefixDefinition = new FilterDefinition();
            Map<String, String> stripPrefixParams = new HashMap<>(8);
            stripPrefixDefinition.setName("StripPrefix");
            stripPrefixParams.put("_genkey_0", routeDTO.getStripPrefix().toString());
            stripPrefixDefinition.setArgs(stripPrefixParams);
            filters.add(stripPrefixDefinition);
            vo.setPredicates(predicates);
            vo.setFilters(filters);
            vo.setUri(uri);
        // 从数据库里读取到路由信息集合存到仓库里
this.repository.save(Mono.just(vo)).subscribe(); }); return Mono.empty(); } public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } // 利用 spring cloud stream 的RemoteApplicationEvent监听路由改动事件,重新载入路由信息 @EventListener(classes = {RemoteApplicationEvent.class}) public void refreshRoute(RemoteApplicationEvent event) { deleteRoute(); refresh(); } private void deleteRoute() { Flux<RouteDefinition> flux = this.repository.getRouteDefinitions(); flux.toStream().forEach(f -> this.repository.delete(Mono.just(f.getId())).subscribe()); } }

拼装好的路由信息,格式如下,最后是一个集合存起来,当然,你也可以选择把这些路由信息直接配置到nacos的json文件里,然后直接读取配置也可以。

{
    "id": "test-server",
    "routeName": "测试平台",
    "uri": "lb://test-server",
    "order": 7788,
    "predicates": [
        {
            "name": "Path",
            "args": {
                "pattern": "/wx/test/**"
            }
        }
    ],
    "filters": [
        {
       // 该过滤器表示删除请求路径中的前缀
"name": "StripPrefix", "args": {
// 表示要从请求路径中删除前两个路径段,例如,/wx/test/demo 对应的转发请求路径为 /demo
"_genkey_0": "2" } } ] }

增加配置类,把MyRouteDefinitionLocator托管给spring。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.JdbcTemplate;

public class MyRouteAutoConfiguration {
  private static final Logger log = LoggerFactory.getLogger(MyRouteAutoConfiguration.class);
  
  @Autowired
  private JdbcTemplate jdbcTemplate;
  
  @Autowired
  private InMemoryRouteDefinitionRepository repository;
  
  public MyRouteAutoConfiguration(JdbcTemplate jdbcTemplate, InMemoryRouteDefinitionRepository repository) {
    this.jdbcTemplate = jdbcTemplate;
    this.repository = repository;
  }
  
  @Bean
  public MyRouteDefinitionLocator myRouteDefinitionLocator(JdbcTemplate jdbcTemplate, InMemoryRouteDefinitionRepository repository) {
    MyRouteDefinitionLocator myRouteDefinitionLocator = new MyRouteDefinitionLocator(jdbcTemplate, repository);
    log.info("MyRouteDefinitionLocator [{}]", myRouteDefinitionLocator);
    return myRouteDefinitionLocator;
  }
}

加一个自定义注解,方便插拔使用动态路由配置。

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({MyRouteAutoConfiguration.class})
public @interface EnableDynamicRoute {}

启动类里加上这个注解,并且实现CommandLineRunner接口,org.springframework.boot.CommandLineRunner可以在springboot服务启动完后额外执行一些逻辑代码,例如执行数据脚本、加载配置文件、设置缓存等,重写它的run方法即可。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableDynamicRoute
@SpringCloudApplication
public class MyGatewayBootstrap implements CommandLineRunner {
  @Autowired
  private MyRouteDefinitionLocator myRouteDefinitionLocator;
  
  public static void main(String[] args) {
    SpringApplication.run(MyGatewayBootstrap.class, args);
  }
  
  public void run(String... strings) throws Exception {
    this.myRouteDefinitionLocator.refresh();
  }
}