Spring Cloud GateWay系列(四):路由规则动态刷新

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()); }
		 */
	}
}

配置示例 

#路由规则 
spring.cloud.gateway.routes[0].id=csdn-service
spring.cloud.gateway.routes[0].uri=https://blog.csdn.net/
#断言器 路由转发的判断条件,目前SpringCloud Gateway支持多种方式,常见如:Path、Query、Method、Header等
spring.cloud.gateway.routes[0].predicates[0]= Path=/csdn/**,/csdnblog/**
#使用Gateway内置路径重写过滤器实现路径重写
spring.cloud.gateway.routes[0].filters[0]= RewritePath=/csdn(blog)?/(?<org>[^/]+)/(?<segment>.*), /${org}/${segment}

使用 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中

 示例源码

结合项目实践,重写了Gateway的动态刷新,示例项目使用的都是Reactor组件,需要对WebFlux有一定的了解。后续将会加入Hystrix和API限流。

https://github.com/zuotaorui/gateway-demo

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Nacos是一个分布式的服务注册和发现系统。Spring Cloud Gateway是开源的网关,可以实现动态路由。结合Nacos和Spring Cloud Gateway可以实现动态路由的功能。 下面是通过Nacos实现Spring Cloud Gateway动态路由的步骤: 1. 添加Nacos组件。在pom.xml文件中添加以下依赖: ``` <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> ``` 2. 实现Spring Cloud Gateway注册到Nacos的功能。 在application.yml文件中添加以下配置: ``` spring: application: name: gateway-service cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true ``` 其中,将enabled设置为true,表示启用Spring Cloud Gateway的服务发现功能;将lower-case-service-id设置为true,表示服务名全部转换为小写。 3. 实现动态路由。 在Nacos中创建config和route两个配置。config配置用来存放动态路由信息,route配置用来存放每个服务的路由信息。 在route配置中添加以下配置: ``` spring: cloud: gateway: routes: - id: user-service uri: loadbalancer://user-service predicates: - Path=/users ``` 其中,id表示服务名,uri表示服务的负载均衡地址,predicates表示路由谓语,可以是Path、Query等。 在config配置中添加以下配置: ``` spring: cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true routes: - id: user-service predicates: - Path=/api/users/** filters: - StripPrefix=1 uri: lb://user-service metadata: version: 1.0 ``` 其中,id表示服务名,predicates表示路由谓语,filters表示过滤器,StripPrefix表示去掉前缀的过滤器,uri表示服务的负载均衡地址,metadata表示元数据信息。 通过以上配置,就可以实现Nacos和Spring Cloud Gateway动态路由的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值