声明:本文授权微信“java知音”公众号独家发布

Spring Cloud Gateway是由spring官方基于Spring5.0,Spring Boot2.0,Project Reactor等技术开发的网关,目的是代替原先版本中的Spring Cloud Netfilx Zuul。目前Netfilx已经开源了Zuul2.0,但Spring 没有考虑集成,而是推出了自己开发的Spring Cloud GateWay。该项目提供了一个构建在Spring Ecosystem之上的API网关,旨在提供一种简单而有效的途径来发送API,并向他们提供交叉关注点:例如:安全性,监控/指标和弹性。在这里废话少说,直接把我实现动态自定义路由的方法托出,共大家参考。由于水平有限,难免有不当或者错误之处,请大家指正,谢谢。

GateWay配置

一般的,我们如果使用Spring Cloud GateWay进行配置,类似于下面的样子:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
      - id: sample-service-a
        uri: lb://SAMPLE-SERVICE-A-HA
        predicates:
        - Path=/customeradd/**
        filters:
        - RewritePath=/customeradd,/customer/add

当我们要新增或者改变一个网关路由时,我们不得不停止网关服务,修改配置文件,保存再重新启动网关服务,这样才能让我们新的设置生效。设想一样,如果是在生产环境,为了一个小小的路由变更,这样的停止再重启恐怕谁也受不了吧。接下来,看看我们怎么能做到动态配置网关路由,让网关路由配置在服务不需要重启的情况生效。

在mysql定义表gateway_define, 表结构如下面的GatewayDefine实体类:

@Entity
@Table(name = "gateway_define")
public class GatewayDefine implements Serializable {
   @Id
   @GeneratedValue(strategy = GenerationType.AUTO)
   private String id;

   private String uri;

   private String predicates;

   private String filters;

   public String getId() {
       return id;
   }

   public void setId(String id) {
       this.id = id;
   }

   public String getUri() {
       return uri;
   }
   public void setUri(String uri) {
       this.uri = uri;
   }
   public String getPredicates() {
       return this.predicates;
   }

   public void setPredicates(String predicates) {
       this.predicates = predicates;
   }

   public List<PredicateDefinition> getPredicateDefinition() {
       if (this.predicates != null) {
           List<PredicateDefinition> predicateDefinitionList = JSON.parseArray(this.predicates, PredicateDefinition.class);
           return predicateDefinitionList;
       } else {
           return null;
       }
   }

   public String getFilters() {
       return filters;
   }
   public List<FilterDefinition> getFilterDefinition() {
       if (this.filters != null) {
           List<FilterDefinition> filterDefinitionList = JSON.parseArray(this.filters, FilterDefinition.class);
           return filterDefinitionList;
       } else {
           return null;
       }
   }

   public void setFilters(String filters) {
       this.filters = filters;
   }

   @Override
   public String toString() {
       return "GatewayDefine{" +
               "id='" + id + '\'' +
               ", uri='" + uri + '\'' +
               ", predicates='" + predicates + '\'' +
               ", filters='" + filters + '\'' +
               '}';
   }
}

其中:id为Eureka注册的服务名; uri、predicates、filters分别应上面配置文件片段中的predicates和filters(这两个保存的都是json)

定义repository和service,采用JPA实现

@Repository
public interface GatewayDefineRepository  extends JpaRepository<GatewayDefine, String> {

  @Override
  List<GatewayDefine> findAll();
  @Override
  GatewayDefine save(GatewayDefine gatewayDefine);
  @Override
  void deleteById(String id);
  @Override
  boolean existsById(String id);
}

public interface GatewayDefineService {
  List<GatewayDefine> findAll() throws Exception;
  String loadRouteDefinition() throws Exception;
  GatewayDefine save(GatewayDefine gatewayDefine) throws Exception;
  void deleteById(String id) throws Exception;
  boolean existsById(String id)throws Exception;

}

@Service
public class GatewayDefineServiceImpl implements GatewayDefineService {
  @Autowired
  GatewayDefineRepository gatewayDefineRepository;

  @Autowired
  private GatewayDefineService gatewayDefineService;

  @Autowired
  private RouteDefinitionWriter routeDefinitionWriter;

  private ApplicationEventPublisher publisher;

  @Override
  public List<GatewayDefine> findAll() throws Exception {
      return gatewayDefineRepository.findAll();
  }

  @Override
  public String loadRouteDefinition() {
      try {
          List<GatewayDefine> gatewayDefineServiceAll = gatewayDefineService.findAll();
          if (gatewayDefineServiceAll == null) {
              return "none route defined";
          }
          for (GatewayDefine gatewayDefine : gatewayDefineServiceAll) {
              RouteDefinition definition = new RouteDefinition();
              definition.setId(gatewayDefine.getId());
              definition.setUri(new URI(gatewayDefine.getUri()));
              List<PredicateDefinition> predicateDefinitions = gatewayDefine.getPredicateDefinition();
              if (predicateDefinitions != null) {
                  definition.setPredicates(predicateDefinitions);
              }
              List<FilterDefinition> filterDefinitions = gatewayDefine.getFilterDefinition();
              if (filterDefinitions != null) {
                  definition.setFilters(filterDefinitions);
              }
              routeDefinitionWriter.save(Mono.just(definition)).subscribe();
              this.publisher.publishEvent(new RefreshRoutesEvent(this));
          }
          return "success";
      } catch (Exception e) {
          e.printStackTrace();
          return "failure";
      }
  }

  @Override
  public GatewayDefine save(GatewayDefine gatewayDefine) throws Exception {
      gatewayDefineRepository.save(gatewayDefine);
      return gatewayDefine;
  }

  @Override
  public void deleteById(String id) throws Exception {
      gatewayDefineRepository.deleteById(id);
  }

  @Override
  public boolean existsById(String id) throws Exception {
      return gatewayDefineRepository.existsById(id);
  }
}

注:
loadRouteDefinition是重点,它从数据库里获取动态定义的路由,最后封装成RouteDefinition 类实例,调用RouteDefinitionWritersave方法保存。RouteDefinitionWriter是个接口,真正实现的是InMemoryRouteDefinitionRepository类,在InMemoryRouteDefinitionRepository定义了一个SynchronizedMap<K, V> 类,所有的设置都在这儿保存。

定义MysqlRouteDefinitionRepository类,实现RouteDefinitionRepository接口的getRouteDefinitions方法,获取从数据库里面装载的路由配置,当然还有save和delete其他方法。

public class MysqlRouteDefinitionRepository implements RouteDefinitionRepository {
  @Autowired
  private GatewayDefineService gatewayDefineService;
  @Override
  public Flux<RouteDefinition> getRouteDefinitions() {
      try {
          List<GatewayDefine> gatewayDefineList = gatewayDefineService.findAll();
          Map<String, RouteDefinition> routes = new LinkedHashMap<String, RouteDefinition>();
          for (GatewayDefine gatewayDefine: gatewayDefineList) {
              RouteDefinition definition = new RouteDefinition();
              definition.setId(gatewayDefine.getId());
              definition.setUri(new URI(gatewayDefine.getUri()));
              List<PredicateDefinition> predicateDefinitions = gatewayDefine.getPredicateDefinition();
              if (predicateDefinitions != null) {
                  definition.setPredicates(predicateDefinitions);
              }
              List<FilterDefinition> filterDefinitions = gatewayDefine.getFilterDefinition();
              if (filterDefinitions != null) {
                  definition.setFilters(filterDefinitions);
              }
              routes.put(definition.getId(), definition);

          }
          return Flux.fromIterable(routes.values());
      } catch (Exception e) {
          e.printStackTrace();
          return Flux.empty();
      }
  }

  @Override
  public Mono<Void> save(Mono<RouteDefinition> route) {
      return route.flatMap(r -> {
          try {
              GatewayDefine gatewayDefine = new GatewayDefine();
              gatewayDefine.setId(r.getId());
              gatewayDefine.setUri(r.getUri().toString());
              gatewayDefine.setPredicates(JSON.toJSONString(r.getPredicates()));
              gatewayDefine.setFilters(JSON.toJSONString(r.getFilters()));
              gatewayDefineService.save(gatewayDefine);
              return Mono.empty();

          } catch (Exception e) {
              e.printStackTrace();
              return Mono.defer(() -> Mono.error(new NotFoundException("RouteDefinition save error: "+ r.getId())));
          }

      });
  }

  @Override
  public Mono<Void> delete(Mono<String> routeId) {
      return routeId.flatMap(id -> {
          try {
              gatewayDefineService.deleteById(id);
              return Mono.empty();

          } catch (Exception e) {
              e.printStackTrace();
              return Mono.defer(() -> Mono.error(new NotFoundException("RouteDefinition delete error: " + routeId)));
          }            
      });
  }
}

在启动类GatewayServiceApplication中添加两个Bean。

@Bean
 public RouteDefinitionWriter routeDefinitionWriter() {
     return new InMemoryRouteDefinitionRepository();
 }

 @Bean
 public MysqlRouteDefinitionRepository mysqlRouteDefinitionRepository() {
     return new MysqlRouteDefinitionRepository();
 } 

添加ApplicationStartup类,在Spring Boot启动时装载路由配置信息, 说明看注释:

 /**
* 在Spring Boot程序启动后会检测程序中是否有CommandLineRunner
* 和ApplicationRunner接口的实例,
* 如果存在,则会执行对应实现类中的run()方法,而且只执行一次
*/
public class ApplicationStartup implements ApplicationRunner {

   @Autowired
   private GatewayDefineService gatewayDefineService;

   @Override
   public void run(ApplicationArguments args) throws Exception {
       gatewayDefineService.loadRouteDefinition();
   }
}

完成。

其他

我这里面没有界面设置路由,我是在配置文件中配置我要的路由,然后通过 /actuator/gateway/routes 获取所有路由的json, 也可以通过 /actuator/gateway/routes/{id} 获取单独一个路由的json.然后手工往数据库里面插入数据,再把网关服务停止,删除配置文件中的路由设定,再重新启动网关功能,通过 /actuator/gateway/routes 能够获取同样的路由json, 通过curl访问设置的路由同样生效。
当然完全可以独立开发一个应用,有界面来读取数据库中的路由配置,可以增加和修改路由信息。再通过Spring Cloud Config的配置来刷新多个网关路由的信息,实现多个网关服务的路由信息实时更新。反正有各种方法可供选择。

最后

第一次写博客,完全是记录自己前一段时间的研究心得。水平有限,有什么不对的地方请大家指正。还有,Spring Cloud GateWay还不支持OAuth2, 所以想统一集成授权、认证等功能的还是使用ZUUL吧。下一次有时间,我会写一下ZUUL的动态路由功能实现以及避免频繁刷新路由信息。反正和GateWay相似,但是还是有区别的。
实现参考了网上很多人的源码和文章,在此表示感谢!也阅读了Spring Cloud GateWay 部分源代码,对Spring Cloud GateWay有了一定的认识,聊以自慰。

后记

最新的Spring Cloud Greenwich.RELEASEGateway 过滤器新增支持OAuth2,我觉得可以抛弃ZUUL了。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐