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 

SpringGateway 可以转发tcp数据吗 springcloud gateway 自定义转发规则_gateway

使用定时任务动态加载路由信息至Gateway中