Spring Cloud Gateway 基于 WebFlux 框架实现,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty。
Spring Cloud Gateway最主要的功能就是路由转发,定义转发规则RouteDefinition时主要涉及了三个核心概念。
核心概念 | 描述 |
Route(路由) | 网关最基本的模块。它由一个ID、一个目标URI、一组断言(Predicate)和一组过滤器(Filter)组成。 |
Predicate(断言) | 路由转发的判断条件,我们可以通过Predicate对HTTP请求进行匹配,例如请求方式、请求路径、请求头、参数等,如果请求与断言匹配成功,则将请求转发到相应的服务。 |
Filter(过滤器) | 过滤器,可以使用Filter对请求进行拦截和修改,还可以使用它对请求的响应内容进行再处理。 |
RoutePredicateHandlerMapping
RoutePredicateHandlerMapping类执行RouteDefinition的Predicates断言匹配
public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
@Override
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
// don't handle requests on management port if set and different than server port
if (this.managementPortType == DIFFERENT && this.managementPort != null
&& exchange.getRequest().getURI().getPort() == this.managementPort) {
return Mono.empty();
}
exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getSimpleName());
//lookupRoute方法执行具体的Predicates断言匹配
return lookupRoute(exchange)
// .log("route-predicate-handler-mapping", Level.FINER) //name this
.flatMap((Function<Route, Mono<?>>) r -> {
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
if (logger.isDebugEnabled()) {
logger.debug("Mapping [" + getExchangeDesc(exchange) + "] to " + r);
}
exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
return Mono.just(webHandler);
}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
if (logger.isTraceEnabled()) {
logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
}
})));
}
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
return this.routeLocator.getRoutes()
// individually filter routes so that filterWhen error delaying is not a
// problem
.concatMap(route -> Mono.just(route).filterWhen(r -> {
// add the current route we are testing
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
return r.getPredicate().apply(exchange);
})
// instead of immediately stopping main flux due to error, log and
// swallow it
.doOnError(e -> logger.error("Error applying predicate for route: " + route.getId(), e))
.onErrorResume(e -> Mono.empty()))
// .defaultIfEmpty() put a static Route not found
// or .switchIfEmpty()
// .switchIfEmpty(Mono.<Route>empty().log("noroute"))
.next()
// TODO: error handling
.map(route -> {
if (logger.isDebugEnabled()) {
logger.debug("Route matched: " + route.getId());
}
validateRoute(route, exchange);
return route;
});
/*
* TODO: trace logging if (logger.isTraceEnabled()) {
* logger.trace("RouteDefinition did not match: " + routeDefinition.getId()); }
*/
}
}
配置示例
使用 application.properties配置路由转发规则固然可以满足需求,但是在新增和修改路由规则时需要重启Gateway实例。
POM文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>dev.miku</groupId>
<artifactId>r2dbc-mysql</artifactId>
</dependency>
<!--HikariCP连接池在R2BC中不可用,连接池选择使用R2DBC Pool-->
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-pool</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
spring-boot-starter-actuator组件引入GatewayControllerEndpoint类,实现了对路由规则RouteDefinition的动态设置。
@RestControllerEndpoint(id = "gateway")
public class GatewayControllerEndpoint extends AbstractGatewayControllerEndpoint {
@GetMapping("/routedefinitions")
public Flux<RouteDefinition> routesdef() {
return this.routeDefinitionLocator.getRouteDefinitions();
}
// TODO: Flush out routes without a definition
@GetMapping("/routes")
public Flux<Map<String, Object>> routes() {
return this.routeLocator.getRoutes().map(this::serialize);
}
//other
....
}
GatewayAutoConfiguration
Spring Cloud Gateway的自动装配类GatewayAutoConfiguration,实现Gateway的自动化配置
public class GatewayAutoConfiguration {
/**
*通过自定义RouteDefinitionRepository
*替换Gateway的默认实现InMemoryRouteDefinitionRepository
*实现将RouteRule持久化至MySQL数据库中
**/
@Bean
@ConditionalOnMissingBean(RouteDefinitionRepository.class)
public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() {
return new InMemoryRouteDefinitionRepository();
}
}
RouteDefinitionRepository
@Repository
public class MySQLRouteDefinitionRepository implements RouteDefinitionRepository {
@Autowired
private GatewayRouteRepository gatewayRouteRepository;
@Autowired
private GatewayRouteArgsRepository gatewayRouteArgsRepository;
@Autowired
private ReactiveStringRedisTemplate redisTemplate;
/**
*Gateway启动时会通过RouteDefinitionRouteLocator.getRoutes方法
*将路由规则RouteDefinition转换为Route
*this.routeDefinitionLocator.getRouteDefinitions().map(this::convertToRoute);
*加载到内存中
**/
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return initVersion().thenMany(findRouteDefinitions());
}
@Override
@Transactional(rollbackFor = Exception.class)
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap(r -> {
if (ObjectUtils.isEmpty(r.getId())) {
return Mono.error(new IllegalArgumentException("id may not be empty"));
} else {
return saveGatewayRoute(r)
.thenMany(savePredicateGatewayRouteArgs(r))
.thenMany(saveFilterGatewayRouteArgs(r))
.then();
}
}).doOnSuccess(v -> updateVersion().subscribe());
}
@Override
@Transactional(rollbackFor = Exception.class)
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap(id -> gatewayRouteRepository.findByRouteId(id)
.switchIfEmpty(Mono.error(
new NotFoundException("RouteDefinition not found: " + routeId)))
.then(gatewayRouteRepository.deleteByRouteId(id))
.then(gatewayRouteArgsRepository.deleteByRouteId(id))
).doOnSuccess(v -> updateVersion().subscribe());
}
}
使用actuator的rest-api可以动态刷新内存中保存的route-rule
curl --location --request POST 'localhost:9090/management/gateway/refresh'
集群中Gateway实例总是部署多台,每次RouteDefinition更新后,需要手动调用API刷新Gateway内存中保存的Route,这是极其枯燥重复的工作。使用Redis保存RouteDefinition的version,并在更新RouteDefinition时刷新version,各Gateway实例监控version实现Route的动态刷新。
@Slf4j
@Component
public class GatewayVersionSmartLifeCycle implements SmartLifecycle {
private boolean isRunning = false;
@Autowired
private ReactiveStringRedisTemplate redisTemplate;
@Autowired
private ApplicationEventPublisher publisher;
private Disposable disposable;
@Override
public void start() {
/* 每10秒检查当前route version是否最新 不是最新时刷新
* 监听 RefreshRoutesResultEvent 得到刷新结果
* 刷新失败的原因大概率是因为路由规则设置错误 失败时重置版本为 0 不断重试
*/
disposable =
Mono.defer(() -> {
if (GatewayVersion.init.get()) {
return redisTemplate.opsForValue().get(DataConstant.REDIS_KEY_VERSION)
.flatMap(v -> {
Long version = Long.valueOf(v);
if (version > GatewayVersion.version.get()) {
//Gateway路由信息已更改,需要重新初始化
GatewayVersion.version.set(version);
this.publisher.publishEvent(new RefreshRoutesEvent(this));
}
return Mono.empty();
});
}
return Mono.empty();
})
.repeatWhen(
Repeat.onlyIf(repeatContext -> true)
.fixedBackoff(Duration.ofSeconds(10)))
.subscribeOn(Schedulers.boundedElastic()).subscribe();
isRunning = true;
}
@Override
public void stop() {
if (disposable != null && !disposable.isDisposed()) {
disposable.dispose();
}
isRunning = false;
}
@Override
public boolean isRunning() {
return isRunning;
}
}
RouteDefinition设置存在错误时,例如RewritePath Filter未设置 regexp replacement两个参数时,将会导致Route刷新失败。 可以监听RefreshRoutesResultEvent,打印错误信息。
public class RefreshRoutesResultEventListener
implements ApplicationListener<RefreshRoutesResultEvent> {
@Override
public void onApplicationEvent(RefreshRoutesResultEvent event) {
if (event.isSuccess()) {
log.info("RefreshRoutesResultEventListener | refresh routes success. event:{}", event);
} else {
GatewayVersion.version.set(0);
log.error("RefreshRoutesResultEventListener | refresh routes failed. event:{} e:{}",
event, event.getThrowable());
}
}
}
示例演示
curl --location --request POST 'localhost:9090/management/gateway/routes/izx-hello4' \
--header 'Content-Type: application/json' \
--data-raw '{
"uri": "lb://izx-hello",
"order": 0,
"predicates": [
{
"name": "Path",
"args": {
"api-meizu": "/projects/**"
}
}
],
"filters": [
{
"name": "RewritePath",
"args": {
"regexp": "/projects/(?<segment>/?.*)",
"replacement":"/regexp${segment}"
}
}
]
}'
将RouteDefinition保存至MySQL DB
使用定时任务动态加载路由信息至Gateway中