Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
相应的入门demo网上很多,我们这边一笔带过
1.引入pom依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
2.application.yml 配置文件
server: servlet: context-path: / port: 18889 spring: application: name: client-gateway cloud: gateway: discovery: locator: enabled: false #表明gateway开启服务注册和发现的功能,并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router,# 这个router将以服务名开头的请求路径转发到对应的服务 lower-case-service-id: true #将请求路径上的服务名配置为小写(因为服务注册的时候,向注册中心注册时将服务名转成大写的了,比如以/service-hi/*的请求路径被路由转发到服务名为service-hi的服务上 routes: - id: test-id uri: lb://client-manage order: -1 predicates: - Path=/api2/** filters: - StripPrefix=1
- id:我们自定义的路由 ID,保持唯一
- uri:目标服务地址
- predicates:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
- filters:过滤规则,
上面那段路由配置表示所有已包含 /api2/ 的url都会被路由到 client-manage服务,StripPrefix=1表示路由时会去除/api2/。
我们也可以使用api的方式配置
@Bean public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) { // @formatter:off return builder.routes () .route (r -> r.path ("/api2/**") .filters (f -> f.stripPrefix (1)) .uri ("lb://client-manage") .order (0) .id ("client-manage") ) .build (); }
这两种配置效果是一致。
路由扩展
基于上面两种配置,我们的网关已经具有了路由的功能了。但是这里还不够工程化。设想下,现在我有上百个路由信息,配置文件或者api的形式去配置必然会导致可读性的缺失。同时我还想实现不停机的增加路由。这里就引入了动态增加路由的概念。翻看Gateway的代码,发现Gateway代码本身就支持动态增加路由,相关代码在 org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint#save
入参的 RouteDefinition 是对路由的封装,与上文的配置文件一一相对应。最终实际调用的是 RouteDefinitionWriter的save方法。
基于上述代码,我们要做以下几件事情
1.定义外部存储文件中的RouteDefinition的数据结构
2.启动时自动读取路由信息写入内存中。
单个路由的添加
@PostMapping(value = "addRoute") @ResponseBody public String addRoute() throws URISyntaxException{ RouteDefinition routeDefinition=new RouteDefinition(); routeDefinition.setId("test-id"); List<PredicateDefinition> predicates=new ArrayList<>(); PredicateDefinition definition=new PredicateDefinition(); //注意name definition.setName("Path"); definition.addArg("pattern","/api2/**"); predicates.add(definition); routeDefinition.setPredicates(predicates); List<FilterDefinition> filters =new ArrayList<>(); FilterDefinition filterDefinition = new FilterDefinition(); //注意name filterDefinition.setName("StripPrefix"); filterDefinition.addArg("parts","1"); filters.add(filterDefinition); routeDefinition.setFilters(filters); URI uri = new URI("lb://client-manage"); routeDefinition.setUri(uri); routeDefinition.setOrder(0); String save = routeService.add(routeDefinition); System.out.println(save); return ""; }
为确保功能的实现,我们先写死一个配置尝试用这种方式配置路由。这里注意两点
PredicateDefinition配置
PredicateDefinition的name代表每一个工厂类,只能从以下选择
addArgs的key是相应方法参数名称
public GatewayFilterSpec stripPrefix(int parts) { return filter(getBean(StripPrefixGatewayFilterFactory.class) .apply(c -> c.setParts(parts))); }
FilterDefinition配置
FilterDefinition和PredicateDefinition类似,name从以下选择
配置文件读取
单个配置生效后我们开始外部文件的形式去配置,这里为了便捷我依然从项目配置文件读取。实际我们可以把配置放到数据库,缓存。
新建api.properties文件
新增配置
api.methods.api2={"predicateDefinition":[{"predicateValue":"/api2/**","name":"Path","predicateKey":"pattern"}],"id":"test_id","uri":"lb://client-manage","filterDefinition":[{"filterKey":"parts","filterValue":"1","name":"StripPrefix"}],"order":"0"}
定义RouteDefines读取配置文件
@Configuration @PropertySource("classpath:api.properties") @ConfigurationProperties(prefix = "api") public class RouteDefines { public Map<String, String> methods = new HashMap<>(); public Map<String, String> getMethods() { return methods; } public void setMethods(Map<String, String> methods) { this.methods = methods; } }
定义InitRouteApplication初始化路由信息写入内存
package com.hdkj.client.gateway; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.hdkj.client.gateway.configuration.RouteDefines; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.cloud.gateway.filter.FilterDefinition; import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.stereotype.Component; /** * @author Xu.Minzhe * @version V1.0 * @package com.hdkj.client.gateway * @class: InitRouteApplication.java * @description: 初始化路由信息 * @Date 2019-04-25 10:15 */ @Component public class InitRouteApplication implements ApplicationRunner { private static final Logger logger = LoggerFactory.getLogger(InitRouteApplication.class); @Autowired private RouteDefines routeDefines; @Autowired private DynamicRouteService routeService; @Override public void run(ApplicationArguments args) throws Exception { Map<String, String> methods = routeDefines.getMethods(); methods.values().stream().forEach(x->{ try { System.out.println("配置文件读取的信息"+x); JSONObject jsonObject = JSONObject.parseObject(x); //组装RouteDefinition RouteDefinition routeDefinition = getRouteDefinition(jsonObject); //路由信息写入 String save = routeService.add(routeDefinition); } catch (Exception e) { logger.error("[路由初始化] 异常",e); } }); } /** * 组装RouteDefinition * @param jsonObject * @return * @throws URISyntaxException */ private RouteDefinition getRouteDefinition(JSONObject jsonObject) throws URISyntaxException { RouteDefinition routeDefinition=new RouteDefinition(); routeDefinition.setId(jsonObject.getString("id")); List<PredicateDefinition> predicateList = getPredicateList(jsonObject); routeDefinition.setPredicates(predicateList); List<FilterDefinition> filterDefinition1 = getFilterDefinition(jsonObject); routeDefinition.setFilters(filterDefinition1); URI uri = new URI(jsonObject.getString("uri")); routeDefinition.setUri(uri); routeDefinition.setOrder(jsonObject.getIntValue("order")); return routeDefinition; } /** * 解析json 获得PredicateList * @param jsonObject * @return */ private List<PredicateDefinition> getPredicateList(JSONObject jsonObject) { JSONArray predicateDefinition = jsonObject.getJSONArray("predicateDefinition"); List<PredicateDefinition> predicates=new ArrayList<>(); predicateDefinition.stream().forEach(predicate->{ JSONObject jsonObject1 = JSONObject.parseObject(predicate.toString()); PredicateDefinition definition=new PredicateDefinition(); definition.setName(jsonObject1.getString("name")); definition.addArg(jsonObject1.getString("predicateKey"),jsonObject1.getString("predicateValue")); predicates.add(definition); }); return predicates; } /** * 解析json 获得FilterDefinitionList * @param jsonObject * @return */ private List<FilterDefinition> getFilterDefinition(JSONObject jsonObject) { JSONArray predicateDefinition = jsonObject.getJSONArray("filterDefinition"); List<FilterDefinition> predicates=new ArrayList<>(); predicateDefinition.stream().forEach(predicate->{ JSONObject jsonObject1 = JSONObject.parseObject(predicate.toString()); FilterDefinition definition=new FilterDefinition(); definition.setName(jsonObject1.getString("name")); definition.addArg(jsonObject1.getString("filterKey"),jsonObject1.getString("filterValue")); predicates.add(definition); }); return predicates; } }
这里DynamicRouteService是路由写入实现类
@Component public class DynamicRouteService implements ApplicationEventPublisherAware { @Autowired private RouteDefinitionWriter routeDefinitionWriter; private ApplicationEventPublisher publisher; /** * 增加路由 * @param definition * @return */ public String add(RouteDefinition definition) { routeDefinitionWriter.save(Mono.just(definition)).subscribe(); this.publisher.publishEvent(new RefreshRoutesEvent(this)); return "success"; } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; } }
以上便是路由加载的实现。至于动态新增路由,有了以上的代码,实现也是相当简单了。这里不再叙述。