1 摘要
在一般情况下,业务相对固定,使用静态路由,直接在项目配置中写死即可应对。但是在业务拓展和业务变更较多较大的情况下,则可以通过动态路由来更好管理服务。本文将介绍基于 Spring Cloud Alibaba 2.2 集成 Gateway 实现动态路由功能。
Spring Cloud Alibaba 2.2 简易集成 Gateway:
Spring Cloud Alibaba 集成网关Gateway
Gateway 全局过滤功能:
Spring Cloud Alibaba 集成网关 Gateway 全局过滤功能
2 核心 Maven 依赖
./cloud-alibaba-gateway-filter/pom.xml
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>${spring-cloud-alibaba.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>${spring-cloud-openfeign.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>${spring-cloud-gateway.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<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>
其中 ${spring-cloud-alibaba.version} 的版本为 2.2.3.RELEASE , ${spring-cloud-gateway.version} 的版本为 2.2.5.RELEASE ,${spring-cloud-openfeign.version} 的版本为 2.2.5.RELEASE
${hutool.version} 的版本为5.7.8 , ${mysql.version} 的版本为 8.0.27 , ${mybatis-plus.version} 的版本为 3.4.3.4
注意事项: Spring Cloud Alibaba 2.2.3.RELEASE 版本支持的 Spring Boot 版本为 2.3.1.RELEASE ,建议在搭建项目时要保持版本的一致性,Spring Boot 版本过高或过低都可能导致不兼容问题
Spring Cloud Gateway 不能和 Spring Boot Web 一起打包
?
3 名词释义
Gateway 网关的路由类:
org.springframework.cloud.gateway.route.Route
路由对象(Route) 组成属性:
public class Route implements Ordered {
private final String id;
private final URI uri;
private final int order;
private final AsyncPredicate<ServerWebExchange> predicate;
private final List<GatewayFilter> gatewayFilters;
private final Map<String, Object> metadata;
}
id : 路由 id,不可重复
uri : 路由地址,指通过该路由的请求会转发到当前 uri 路径上去
order : 拦截器的执行顺序,数值越小,优先级越高, 取值范围为: -2147483648 到 2147483647
predicate : 断言,指符合当前条件的请求会走当前的路由, Spring Cloud Gateway 自带 10 种断言模式,最常用的是 Path route Predicate Factory 和 Host Route Predicate Factory , 更多断言资料参考官方文档:
Route Predicate Factories
gatewayFilters : 过滤器,拦截器,当符合指定断言的请求进入当前路由之后,会进入当前路由的拦截器中,一个路由可以绑定多个拦截器,根据优先级依次执行。Spring Cloud Gateway 定义了大概 30 种拦截器,最常用的有 AddRequestHeader GatewayFilterFactory 和 PrefixPath GatewayFilterFactory 。更多拦截器资料参考官方文档:
GatewayFilter Factories
metadata : 元数据,用于定义一些额外的参数
?
4 Gateway 动态路由原理
Gateway 提供了一系列的方法来实现路由的管理功能,从而可以实现动态路由,在 GatewayControllerEndpoint 类中已经定义了路由的增删查等功能
org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint
借助 Swagger 可以更加清晰地看到 Gateway 预定义的接口:
可以根据名称猜测出大致的功能含义
?
5 数据库表
为了方便管理,可以将路由信息保存到本地。
./doc/sql/gateway-database-create.sql
drop table if exists gateway_route;
create table gateway_route
(
id bigint unsigned not null auto_increment comment '数据库id',
route_id varchar(32) comment '路由id',
uri varchar(128) comment '请求地址',
predicates varchar(512) comment '断言',
filters varchar(512) comment '拦截器',
metadata varchar(128) comment '附加参数',
filter_order int comment '执行顺序,数值越小优先级越高',
create_date bigint unsigned comment '创建时间',
update_date bigint unsigned comment '更新时间',
primary key (id)
);
alter table gateway_route comment '网关路由';
?
6 核心代码
6.1 配置信息
./cloud-alibaba-gateway-filter/src/main/resources/application.yml
server:
port: 8612
spring:
application:
name: cloud-alibaba-gateway-filter
datasource:
url: "jdbc:mysql://127.0.0.1:3306/demo?useUnicode=true&characterEncoding=utf8\
&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8\
&useSSL=true&allowMultiQueries=true&autoReconnect=true&nullCatalogMeansCurrent=true\
&nullCatalogMeansCurrent=true"
username: root
password: "root"
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
management:
endpoints:
web:
exposure:
include: "*"
6.2 路由实体类
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/model/RouteEntity.java
package com.ljq.demo.springboot.alibaba.gateway.filter.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@Data
@TableName(value = "gateway_route")
public class RouteEntity implements Serializable {
private static final long serialVersionUID = -7838086812254411423L;
@TableId(type = IdType.NONE)
private Long id;
private String routeId;
private String uri;
private String predicates;
private String filters;
private String metadata;
private Integer filterOrder;
private Long createDate;
private Long updateDate;
}
6.3 本地路由数据库持久层(DAO/Mapper)
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/dao/RouteMapper.java
package com.ljq.demo.springboot.alibaba.gateway.filter.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ljq.demo.springboot.alibaba.gateway.filter.model.RouteEntity;
import org.springframework.stereotype.Repository;
@Repository
public interface RouteMapper extends BaseMapper<RouteEntity> {
}
6.4 操作 Gateway 路由的 Repository
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/repository/DbRouteRepository.java
package com.ljq.demo.springboot.alibaba.gateway.filter.repository;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ljq.demo.springboot.alibaba.gateway.filter.common.util.RouteConvert;
import com.ljq.demo.springboot.alibaba.gateway.filter.dao.RouteMapper;
import com.ljq.demo.springboot.alibaba.gateway.filter.model.RouteEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Component
public class DbRouteRepository implements RouteDefinitionRepository {
@Autowired
private RouteMapper routeMapper;
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
List<RouteEntity> routeEntityList = routeMapper.selectList(Wrappers.emptyWrapper());
if (CollUtil.isEmpty(routeEntityList)) {
log.info("数据库内没有定义路由");
return Flux.empty();
}
List<RouteDefinition> routeDefinitionList = new ArrayList<>();
routeEntityList.stream().forEach(routeEntity -> {
routeDefinitionList.add(RouteConvert.toRouteDefinition(routeEntity));
});
log.info("json 数据库路由列表: {}", JSONUtil.toJsonStr(routeDefinitionList));
return Flux.fromIterable(routeDefinitionList);
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap(routeDefinition -> {
routeMapper.insert(RouteConvert.toRouteEntity(routeDefinition));
return Mono.empty();
});
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap(id -> {
routeMapper.delete(Wrappers.lambdaQuery(new RouteEntity()).eq(RouteEntity::getRouteId, id));
return Mono.empty();
});
}
}
在默认情况下,应用程序启动时会查询路由列表,即执行 getRouteDefinitions() 方法。在和 Spring Cloud Alibaba 集成后,程序启动之后,会每间隔 30 秒查询路由一次。为避免给数据库造成压力,可以考虑添加缓存。
6.4 路由管理业务层 (Service)
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/service/DynamicRouteService.java
package com.ljq.demo.springboot.alibaba.gateway.filter.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ljq.demo.springboot.alibaba.gateway.filter.model.RouteDeleteParam;
import com.ljq.demo.springboot.alibaba.gateway.filter.model.RouteEntity;
import org.springframework.cloud.gateway.route.RouteDefinition;
import java.util.List;
public interface DynamicRouteService extends IService<RouteEntity> {
List<RouteDefinition> listActive();
void add(RouteDefinition routeDefinition);
void update(RouteDefinition routeDefinition);
void delete(RouteDeleteParam routeDeleteParam);
}
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/service/impl/DynamicRouteServiceImpl.java
package com.ljq.demo.springboot.alibaba.gateway.filter.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ljq.demo.springboot.alibaba.gateway.filter.dao.RouteMapper;
import com.ljq.demo.springboot.alibaba.gateway.filter.model.RouteDeleteParam;
import com.ljq.demo.springboot.alibaba.gateway.filter.model.RouteEntity;
import com.ljq.demo.springboot.alibaba.gateway.filter.repository.DbRouteRepository;
import com.ljq.demo.springboot.alibaba.gateway.filter.service.DynamicRouteService;
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.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@Slf4j
@Service
public class DynamicRouteServiceImpl extends ServiceImpl<RouteMapper, RouteEntity> implements DynamicRouteService, ApplicationEventPublisherAware {
@Autowired
private DbRouteRepository dbRouteRepository;
private ApplicationEventPublisher applicationEventPublisher;
@Override
public List<RouteDefinition> listActive() {
Flux<RouteDefinition> routeDefinitionFlux = dbRouteRepository.getRouteDefinitions();
if (Objects.isNull(routeDefinitionFlux)) {
return Collections.emptyList();
}
List<RouteDefinition> routeDefinitionList = new ArrayList<>();
routeDefinitionFlux.collectList().subscribe(routeDefinitionList::addAll);
return routeDefinitionList;
}
@Override
public void add(RouteDefinition routeDefinition) {
dbRouteRepository.save(Mono.just(routeDefinition)).subscribe();
applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
}
@Override
public void update(RouteDefinition routeDefinition) {
dbRouteRepository.delete(Mono.just(routeDefinition.getId())).subscribe();
dbRouteRepository.save(Mono.just(routeDefinition)).subscribe();
applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
}
@Override
public void delete(RouteDeleteParam routeDeleteParam) {
dbRouteRepository.delete(Mono.just(routeDeleteParam.getRouteId())).subscribe();
applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
}
6.5 路由管理控制层
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/controller/DynamicRouteController.java
package com.ljq.demo.springboot.alibaba.gateway.filter.controller;
import com.ljq.demo.springboot.alibaba.gateway.filter.common.api.ApiResult;
import com.ljq.demo.springboot.alibaba.gateway.filter.model.RouteDeleteParam;
import com.ljq.demo.springboot.alibaba.gateway.filter.service.DynamicRouteService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/gateway/route")
public class DynamicRouteController {
@Autowired
private DynamicRouteService routeService;
@GetMapping(value = "/list/active", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<ApiResult<List<RouteDefinition>>> listActive() {
log.info("/list/active");
return ResponseEntity.ok(ApiResult.success(routeService.listActive()));
}
@PostMapping(value = "/add", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<ApiResult<RouteDefinition>> add(@RequestBody @Validated RouteDefinition routeDefinition) {
log.info("/add,新增路由参数: {}", routeDefinition);
routeService.add(routeDefinition);
return ResponseEntity.ok(ApiResult.success(routeDefinition));
}
@PutMapping(value = "/update", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<ApiResult<RouteDefinition>> update(@RequestBody @Validated RouteDefinition routeDefinition) {
log.info("/update,新增路由参数: {}", routeDefinition);
routeService.update(routeDefinition);
return ResponseEntity.ok(ApiResult.success(routeDefinition));
}
@DeleteMapping(value = "/delete", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<ApiResult<RouteDeleteParam>> delete(@RequestBody RouteDeleteParam routeDeleteParam) {
log.info("/delete,删除路由参数: {}", routeDeleteParam);
routeService.delete(routeDeleteParam);
return ResponseEntity.ok(ApiResult.success(routeDeleteParam));
}
}
6.6 其他相关类
本地路由实体类与 Gateway 路由定义类转换类:
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/common/util/RouteConvert.java
package com.ljq.demo.springboot.alibaba.gateway.filter.common.util;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.ljq.demo.springboot.alibaba.gateway.filter.model.RouteEntity;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import java.net.URI;
import java.util.Map;
public class RouteConvert {
private RouteConvert(){
}
public static RouteDefinition toRouteDefinition(RouteEntity routeEntity) {
RouteDefinition routeDefinition = new RouteDefinition();
routeDefinition.setId(routeEntity.getRouteId());
routeDefinition.setOrder(routeEntity.getFilterOrder());
routeDefinition.setUri(URI.create(routeEntity.getUri()));
JSONArray predicateArray = new JSONArray(JSONUtil.toJsonStr(routeEntity.getPredicates()));
if (!predicateArray.isEmpty()) {
routeDefinition.setPredicates(predicateArray.toList(PredicateDefinition.class));
}
JSONArray filtersArray = new JSONArray(JSONUtil.toJsonStr(routeEntity.getFilters()));
if (!filtersArray.isEmpty()) {
routeDefinition.setFilters(filtersArray.toList(FilterDefinition.class));
}
routeDefinition.setMetadata(JSONUtil.toBean(routeEntity.getMetadata(), Map.class));
return routeDefinition;
}
public static RouteEntity toRouteEntity(RouteDefinition routeDefinition) {
RouteEntity routeEntity = new RouteEntity();
routeEntity.setRouteId(routeDefinition.getId());
routeEntity.setUri(routeDefinition.getUri().toString());
routeEntity.setPredicates(JSONUtil.toJsonStr(routeDefinition.getPredicates()));
routeEntity.setFilters(JSONUtil.toJsonStr(routeDefinition.getFilters()));
routeEntity.setMetadata(JSONUtil.toJsonStr(routeDefinition.getMetadata()));
routeEntity.setFilterOrder(routeDefinition.getOrder());
return routeEntity;
}
}
删除路由请求参数:
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/model/RouteDeleteParam.java
package com.ljq.demo.springboot.alibaba.gateway.filter.model;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
@Data
public class RouteDeleteParam implements Serializable {
private static final long serialVersionUID = -2214111091023615696L;
@NotBlank(message = "路由ID不能为空")
private String routeId;
}
用于路由测试的控制层:
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/controller/DemoController.java
./package com.ljq.demo.springboot.alibaba.gateway.filter.controller;
import com.ljq.demo.springboot.alibaba.gateway.filter.common.api.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping(value = "/api/shop/user")
public class DemoController {
@GetMapping(value = "/list")
public ResponseEntity<?> list(){
log.info("/api/user/list");
return ResponseEntity.ok(ApiResult.success("请求成功啦!!!"));
}
}
用于测试的 SpringBoot 启动类:
主要是通过不同的端口实现多个服务
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/CloudAlibabaGatewayFilter2Application.java
package com.ljq.demo.springboot.alibaba.gateway.filter;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication(scanBasePackages = {"com.ljq.demo.springboot.*"})
@MapperScan(basePackages = {"com.ljq.demo.springboot.alibaba.gateway.filter.dao"})
public class CloudAlibabaGatewayFilter2Application {
public static void main(String[] args) {
System.setProperty("server.port", "8613");
SpringApplication.run(CloudAlibabaGatewayFilter2Application.class, args);
}
}
接口返回结果:
./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/common/api/ApiResult.java
7 测试
7.1 新增路由
请求地址:
http://127.0.0.1:8612/api/gateway/route/add
请求方式: POST
请求参数:
{
"filters": [
{
"args": {
"prefix":"/api"
},
"name": "PrefixPath"
}
],
"id": "demoRouter701",
"metadata": {},
"order": 0,
"predicates": [
{
"args": {
"pattern": "/shop/user/**"
},
"name": "Path"
}
],
"uri": "http://127.0.0.1:8613"
}
关于参数定义:
断言以及拦截器中的参数名一定要与 Gateway 官方的定义保持一致,具体可参考官方文档:
过滤器定义官方文档: https://cloud.spring.io/spring-cloud-gateway/reference/html/#gatewayfilter-factories
断言定义官方文档: https://cloud.spring.io/spring-cloud-gateway/reference/html/#gateway-request-predicates-factories
添加断言与路由时会对参数进行校验,具体类如下:
加载过滤器(filters):
org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#loadGatewayFilters
加载断言(predicates):
org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#loadGatewayFilters
7.2 查询路由列表
请求地址:
http://127.0.0.1:8612/api/gateway/route/list/active
请求方式: GET
请求结果:
{
"key": "api.response.success",
"message": "SUCCESS",
"data": [
{
"id": "demoRouter701",
"predicates": [
{
"name": "Path",
"args": {
"pattern": "/shop/user/**"
}
}
],
"filters": [
{
"name": "PrefixPath",
"args": {
"prefix": "/api"
}
}
],
"uri": "http://127.0.0.1:8613",
"metadata": {},
"order": 0
}
],
"extraDataMap": null,
"timestamp": 1635143729231
}
7.3 修改路由
请求路径:
http://127.0.0.1:8612/api/gateway/route/update
请求方式: PUT
请求参数: 格式同新增
7.4 删除路由
请求路径:
http://127.0.0.1:8612/api/gateway/route/delete
请求方式: DELETE
请求参数:
{
"routeId": "demoRouter26"
}
7.5 测试匹配路由规则的接口
根据上边的路由规则,路径中以 /shop/user/ 开头的接口就会走路由
接口地址:
http://127.0.0.1:8612/shop/user/list
请求方式: GET
请求参数: 无
返回结果:
{
"key": "api.response.success",
"message": "SUCCESS",
"data": "请求成功啦!!!",
"extraDataMap": null,
"timestamp": 1635143656374
}
当前的返回结果为端口为 8613 的服务返回的,表明路由已经生效
7.6 测试不匹配路由规则的接口
接口地址:
http://127.0.0.1:8612/user/info
请求方式: GET
请求参数: 无
返回结果:
{
"timestamp": "2021-10-25T06:39:35.241+00:00",
"path": "/user/info",
"status": 404,
"error": "Not Found",
"message": null,
"requestId": "d9a4b20c-18"
}
当前结果为 SpringBoot 返回的默认 404 错误,没有匹配路由规则就不会通过网关,到达对应的服务
7.7 测试微服务代理
在配置信息中,以下配置是表明 Gateway 将自动代理当前注册中心下所有的服务
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
将这一部分注释,则 Gateway 不再自动代理服务
当添加动态路由之后,Gateway 将遵循新的路由规则,也不再自动代理注册中心的服务
?
8 推荐参考资料
Spring Cloud Gateway 官方文档
详解SpringCloud-gateway动态路由两种方式,以及路由加载过程
spring cloud 2.x版本 Gateway动态路由教程
Convert Flux into List, Map – Reactor
Reactor 3快速上手——响应式Spring的道法术器
? ?
9 Github 源码
Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo
个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.
|