目录

前言

源码分析

实现动态路由

 


前言

上篇入门篇,通过配置或者代码的方式,实现了路由。(详情请跳转:SpringCloud学习系列Gateway-(1)入门篇

但这种配置方式有个弊端,就是每次接入一个新应用或者变更应用访问路径,就需要重新配置网关,新增或修改路由规则,然后重启gateway;对于一个网关层来说,一旦出现这种情况,就会影响所有接入应用在这段时间都不能访问,这无疑是不可行的。

那么,这一章,我们就看下如何采用动态路由的方式来解决这种问题。

源码分析

首先,我们来看下SpringCloud Gateway的初始化方式和路由执行方式。(gateway的maven版本:2.2.3.RELEASE)

1、初始化配置信息,我们来看下org.springframework.cloud:spring-cloud-gateway-core包下面的META-INF/spring.factories文件;

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
// 校验引用配置
org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration,\
// gateway网关的核心配置,路由等
org.springframework.cloud.gateway.config.GatewayAutoConfiguration,\
// 熔断配置
org.springframework.cloud.gateway.config.GatewayHystrixCircuitBreakerAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayResilience4JCircuitBreakerAutoConfiguration,\
// 负载均衡配置
org.springframework.cloud.gateway.config.GatewayLoadBalancerClientAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration,\
// redis限流配置
org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration,\
// 注册中心配置
org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfiguration,\
org.springframework.cloud.gateway.config.SimpleUrlHandlerMappingGlobalCorsAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayReactiveLoadBalancerClientAutoConfiguration

org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.cloud.gateway.config.GatewayEnvironmentPostProcessor

这一章,我们只需关注网关的核心配置类 org.springframework.cloud.gateway.config.GatewayAutoConfiguration

// 这里一些其他的代码就先省略
public class GatewayAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean
	public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator(
			GatewayProperties properties) {
		return new PropertiesRouteDefinitionLocator(properties);
	}

	@Bean
	@ConditionalOnMissingBean(RouteDefinitionRepository.class)
	public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() {
		return new InMemoryRouteDefinitionRepository();
	}

    // 初始化装载不同路由配置方式的列表
	@Bean
	@Primary
	public RouteDefinitionLocator routeDefinitionLocator(
			List<RouteDefinitionLocator> routeDefinitionLocators) {
		return new CompositeRouteDefinitionLocator(
				Flux.fromIterable(routeDefinitionLocators));
	}

	@Bean
	public RouteRefreshListener routeRefreshListener(
			ApplicationEventPublisher publisher) {
		return new RouteRefreshListener(publisher);
	}

}

这里第三个方法就是负责把继承了routeDefinitionLocator接口类的不同方式路由配置汇总;PropertiesRouteDefinitionLocator 和InMemoryRouteDefinitionRepository 都继承了 routeDefinitionLocator 接口类,对应第一个方法和第二个方法的初始化类。

public class PropertiesRouteDefinitionLocator implements RouteDefinitionLocator {

	private final GatewayProperties properties;

	public PropertiesRouteDefinitionLocator(GatewayProperties properties) {
		this.properties = properties;
	}

	@Override
	public Flux<RouteDefinition> getRouteDefinitions() {
		return Flux.fromIterable(this.properties.getRoutes());
	}

}

PropertiesRouteDefinitionLocator 就是加载application.properties或者application.yml文件中配置的路由信息;

public class InMemoryRouteDefinitionRepository implements RouteDefinitionRepository {

	private final Map<String, RouteDefinition> routes = synchronizedMap(
			new LinkedHashMap<String, RouteDefinition>());

	@Override
	public Mono<Void> save(Mono<RouteDefinition> route) {
		return route.flatMap(r -> {
			if (StringUtils.isEmpty(r.getId())) {
				return Mono.error(new IllegalArgumentException("id may not be empty"));
			}
			routes.put(r.getId(), r);
			return Mono.empty();
		});
	}

	@Override
	public Mono<Void> delete(Mono<String> routeId) {
		return routeId.flatMap(id -> {
			if (routes.containsKey(id)) {
				routes.remove(id);
				return Mono.empty();
			}
			return Mono.defer(() -> Mono.error(
					new NotFoundException("RouteDefinition not found: " + routeId)));
		});
	}

	@Override
	public Flux<RouteDefinition> getRouteDefinitions() {
		return Flux.fromIterable(routes.values());
	}

}

InMemoryRouteDefinitionRepository 从代码中可以看到,使用Map<String, RouteDefinition> routes来存储路由信息,就是使用了本地缓存的方式。(这里RouteDefinitionRepository 继承了routeDefinitionLocator )

但是InMemoryRouteDefinitionRepository 的bean初始化方式采用了@ConditionalOnMissingBean(RouteDefinitionRepository.class),这表示如果没有其他继承了RouteDefinitionRepository.class的类进行bean初始化的前提下,就初始化InMemoryRouteDefinitionRepository。

(注意:这里就是预留的让我们进行自定义路由加载的实现方式,后面实现动态路由就可以从这里入手)

我们再来看下这些类继承的routeDefinitionLocator类需要实现的是什么。

public interface RouteDefinitionLocator {

	Flux<RouteDefinition> getRouteDefinitions();

}

这个接口类很简单,只需要实现一个方法:获取路由信息类RouteDefinition的列表。

@Validated
public class RouteDefinition {

	private String id;

	@NotEmpty
	@Valid
	private List<PredicateDefinition> predicates = new ArrayList<>();

	@Valid
	private List<FilterDefinition> filters = new ArrayList<>();

	@NotNull
	private URI uri;

	private Map<String, Object> metadata = new HashMap<>();

	private int order = 0;
}

RouteDefinition的属性值,我们可以看下,其实是跟使用properties或者yml方式配置的一些参数书一一对应的。

那么这里就有个问题了,难道请求每次都会执行getRouteDefinitions()方法获取下路由信息吗?然而,并不会。

既然不会,怎么实现动态更新路由信息呢,我们回到GatewayAutoConfiguration配置类看第四个方法,初始化了一个监听器RouteRefreshListener。

public class RouteRefreshListener implements ApplicationListener<ApplicationEvent> {

	@Override
	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ContextRefreshedEvent
				|| event instanceof RefreshScopeRefreshedEvent
				|| event instanceof InstanceRegisteredEvent) {
			reset();
		}
		else if (event instanceof ParentHeartbeatEvent) {
			ParentHeartbeatEvent e = (ParentHeartbeatEvent) event;
			resetIfNeeded(e.getValue());
		}
		else if (event instanceof HeartbeatEvent) {
			HeartbeatEvent e = (HeartbeatEvent) event;
			resetIfNeeded(e.getValue());
		}
	}

	private void resetIfNeeded(Object value) {
		if (this.monitor.update(value)) {
			reset();
		}
	}

	private void reset() {
		this.publisher.publishEvent(new RefreshRoutesEvent(this));
	}

}

监听器里面监听了一个HeartbeatEvent的心跳事件,这里会发送RefreshRoutesEvent事件进行路由缓存刷新。

因为引入了eureka注册中心,这个心跳事件的发出者是由CloudEurekaClient定时发出,(当然这里也可以我们自己发出RefreshRoutesEvent来主动刷新),代码如下:

public class CloudEurekaClient extends DiscoveryClient {

    protected void onCacheRefreshed() {
        super.onCacheRefreshed();
        if (this.cacheRefreshedCount != null) {
            long newCount = this.cacheRefreshedCount.incrementAndGet();
            log.trace("onCacheRefreshed called with count: " + newCount);
            this.publisher.publishEvent(new HeartbeatEvent(this, newCount));
        }

    }

}

那么,到这里,相信大家都知道怎么去实现动态路由了。接下来,动手敲代码吧。

实现动态路由

1、继承RouteDefinitionRepository实现自己的获取路由信息类

import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

@Component
public class DynamicRouteDefinitionRepository implements RouteDefinitionRepository {

    @Resource
    private DynamicRouteConfig dynamicRouteConfig;

    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        return Flux.fromIterable(dynamicRouteConfig.getRouteDefinitions());
    }

    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return Mono.defer(() -> Mono.error(new NotFoundException("Unsupported operation")));
    }

    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return Mono.defer(() -> Mono.error(new NotFoundException("Unsupported operation")));
    }

}

2、自定义的路由操作类DynamicRouteConfig 

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;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;
import java.util.*;

@Component
public class DynamicRouteConfig {

    /**
     * @Description 后期可以采用数据库存储方式或者其他可以变更的存储方式
     **/
    public List<RouteDefinition> getRouteDefinitions() {
        RouteDefinition definition = new RouteDefinition();
        definition.setId("gateway-service");
        definition.setUri(URI.create("lb://gateway-service"));

        //定义第一个断言
        PredicateDefinition predicate = new PredicateDefinition();
        predicate.setName("Path");
        Map<String, String> predicateParams = new HashMap<>(4);
        predicateParams.put("pattern", "/service/**");
        predicate.setArgs(predicateParams);

        //定义Filter
        FilterDefinition filter = new FilterDefinition();
        filter.setName("RequestRateLimiter");
        Map<String, String> filterParams = new HashMap<>(8);
        filterParams.put("key-resolver", "#{@uriKeyResolver}");
        filterParams.put("redis-rate-limiter.replenishRate", "2");
        filterParams.put("redis-rate-limiter.burstCapacity", "2");
        filter.setArgs(filterParams);

        definition.setFilters(Arrays.asList(filter));
        definition.setPredicates(Arrays.asList(predicate));

        List<RouteDefinition> list = new ArrayList<>(4);
        list.add(definition);
        return list;
    }

}

这里我直接采用硬编码的方式,方便测试;后期大家可以直接从数据库或者其他存储获取这些参数,进行List<RouteDefinition>的拼装,这样当心跳事件接收到时,会重新从数据库拉取最新的配置数据更新路由信息,这样就达到动态路由配置的效果。

这里也可以自己进行主动推送事件进行刷新,实现发送RefreshRoutesEvent事件

import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Component;


@Component
public class DynamicRouteEventPublisher implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher eventPublisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.eventPublisher = applicationEventPublisher;
    }

    public void notify() {
        // 重新刷新,如果多节点的情况下,可以采用MQ的广播消息方式通知,进行更新
        this.eventPublisher.publishEvent(new RefreshRoutesEvent(this));
    }


}

这样在管理后台配置好路由信息后,保存到数据库,然后使用消息广播的方式通知各个gateway服务器实例调用这里的notify()方法刷新路由信息就可以。

到这,一个动态路由的方案就出来了。

 

 

 

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐