origin

The Predicate is used to implement the blacklist, but you need to restart the project every time you change the blacklist rules. Therefore, you need to store the routing information in the external data source. Periodically refresh the routing information in the SCG memory.

Train of thought



inSpring Cloud Gateway -RouteDefinitionLocator, RouteLocatorWe have already introducedRouteDefinitionRepositoryThis interface has only one implementation in the SCGInMemoryRouteDefinitionRepositoryAnd the interface inheritsRouteDefinitionWriter.RouteDefinitionWriterDefined in theSave, deleteMethod is used to save/add/delete routing information.

  1. So we can do thatRouteDefinitionRepositoryUsed to hold the RouteDefinition retrieved from RedisRedisRouteDefinitionRepositoryBecause ofRouteDefinitionRepositoryinheritedRouteDefinitionLocatorAnd therefore will beCompositeRouteDefinitionLocatorPut it in, and beCachingRouteLocatorTake the corresponding RouteDefinition in Redis and replace it with Route.
  2. With a place to store the RouteDefinition defined in Redis, isn’t there a role that takes the data in Redis and assembles it into a RouteDefinition stored inRedisRouteDefinitionRepository, so you need to defineRedisRouteDefinitionRepositoryOperatorUsed to fetch the database from Redis to generate a RouteDefinition. Perhaps our routing information will be stored in MySQL, MongoDB, etc., so we can abstract out an interface that takes data from Repository and converts it to RouteDefinitionRouteDefinitionRepositoryOperator.
  3. Based on the above, we can convert data from Redis to RouteDefinition when SCG starts and save it toRedisRouteDefinitionRepositoryHowever, it is not enough to realize the synchronization of SCG update after the routing information in Redis is modified. A heartbeat mechanism similar to Nacos is needed to periodically notify SCG to obtain data in Redis again. So you can imitateNacos heartbeat mechanism implementationRedisRouteDefinitionWatchSends a heartbeat eventCachingRouteLocatorRe-fetch the RouteDefinition to regenerate the Route.

implementation

RouteDefinitionRepositoryOperator

/** * Define an abstraction to get RouteDefinition from different data sources * @author li.hongjian * @email [email protected] * @date 2021/4/1 */
public interface RouteDefinitionRepositoryOperator {

	Flux<RouteDefinition> getRouteDefinitions();

}
Copy the code

RedisRouteDefinitionRepositoryOperator

/ * * * the Description: Used to retrieve the RouteDefinition Redis and save to {@ link RedisRouteDefinitionRepository} * * @ author li. Hongjian * @ email [email protected]  * @Date 2021/4/1 */
public class RedisRouteDefinitionRepositoryOperator implements RouteDefinitionRepositoryOperator {

	private final String REDIS_ROUTE_ID_PREFIX = "route-*";

	private StringRedisTemplate redisTemplate;

	public RedisRouteDefinitionRepositoryOperator(StringRedisTemplate redisTemplate) {
		this.redisTemplate = redisTemplate;
	}


	@Override
	public Flux<RouteDefinition> getRouteDefinitions() {
		// Get RedisKey for the specified prefix. Redis uses Hash as its data structure and predicates and filters as its value structure.
		// Predicates data structure JsonArray
		// Since the PredicateDefinition constructor supports passing arguments in formats like Path=/ API /hello and automatically encapsulates them as name and args, we can store the following structures in Redis
		/ / such as: [" Path = / API/hello ", "BlackRemoteAddr = 172.17.30.1/18172.17. 31.1/18"]. Said PathRoutePredicateFactory and BlackRemoteAddrRoutePredicateFactory
		//filters are the same as predicates
		return Flux.fromStream(redisTemplate.keys(REDIS_ROUTE_ID_PREFIX).parallelStream().map(routeId -> {
			RouteDefinition routeDefinition = new RouteDefinition();
			// Use RedisKey as the RouteID
			routeDefinition.setId(routeId);
			Map<Object, Object> entries = redisTemplate.opsForHash().entries(routeId);
			String uri = (String) entries.get("uri");
			try {
				routeDefinition.setUri(new URI(uri));
			} catch (URISyntaxException e) {
				e.printStackTrace();
			}
			// Initialize the PredicateDefinition and add it to RouteDefinition
			initPredicate(routeDefinition, entries);

			// Initialize FilterDefinition and add it to RouteDefinition
			initFilter(routeDefinition, entries);
			return routeDefinition;
		}));
	}

	private void initPredicate(RouteDefinition routeDefinition, Map<Object, Object> entries) {
		Object predicates = entries.get("predicates");
		if (predicates == null) {
			return;
		}
		JSONArray predicateArry = JSONArray.parseArray((String) predicates);
		predicateArry.parallelStream().forEach(predicate -> {
			// Iterate through the predicates, create the RouteDefinition, and add it to the RouteDefinition
			PredicateDefinition predicateDefinition = new PredicateDefinition((String) predicate);
			routeDefinition.getPredicates().add(predicateDefinition);
		});
	}

	private void initFilter(RouteDefinition routeDefinition, Map<Object, Object> entries) {
		Object filters = entries.get("filters");
		if (filters == null) {
			return;
		}
		JSONArray predicateArry = JSONArray.parseArray((String) filters);
		predicateArry.parallelStream().forEach(filter -> {
			// Iterate through the predicates, create the RouteDefinition, and add it to the RouteDefinitionFilterDefinition filterDefinition = new FilterDefinition((String) filter); routeDefinition.getFilters().add(filterDefinition); }); }}Copy the code

RedisRouteDefinitionRepository

/ * * * the Description: Redis as a RouteDefinition Repository * * @author Li. hongjian * @email [email protected] * @date 2021/4/1 */
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository{

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

	private RedisRouteDefinitionRepositoryOperator redidRouteDefinitionOperator;

	/ RedisRouteDefinitionRepositoryOperator is assembled in a * * * * @ param redidRouteDefinitionOperator * /
	public RedisRouteDefinitionRepository(RedisRouteDefinitionRepositoryOperator redidRouteDefinitionOperator) {
		this.redidRouteDefinitionOperator = redidRouteDefinitionOperator;
	}

	/ * * * in {@ link CompositeRouteDefinitionLocator# getRouteDefinitions ()} call call redidRouteDefinitionOperator fetches data * to Redis @return */
	@Override
	public Flux<RouteDefinition> getRouteDefinitions() {
		redidRouteDefinitionOperator.getRouteDefinitions().flatMap(r -> save(Mono.just(r))).subscribe();
		return Flux.fromIterable(routes.values());
	}

	@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))); }); }}Copy the code

RedisRouteDefinitionWatch

/** * @author li.hongjian * @email [email protected] * @Date 2021/4/1 */public class RedisRouteDefinitionWatch implements ApplicationEventPublisherAware, SmartLifecycle { private final TaskScheduler taskScheduler = getTaskScheduler(); private final AtomicLong redisWatchIndex = new AtomicLong(0); private final AtomicBoolean running = new AtomicBoolean(false); private ApplicationEventPublisher publisher; private ScheduledFuture<? > watchFuture; private static ThreadPoolTaskScheduler getTaskScheduler() { ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); taskScheduler.setBeanName("Redis-Watch-Task-Scheduler");
		taskScheduler.initialize();
		return taskScheduler;
	}


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

	@Override
	public void start() {
		if (this.running.compareAndSet(false, true)) {
			this.watchFuture = this.taskScheduler.scheduleWithFixedDelay(
					this::redisServicesWatch, 30000); // Start a timer for 30 seconds}}/** * It is better to define a custom event, because if you use Nacos, it will conflict. In this case, you need to modify the SCG source code to listen for custom events */
	private void redisServicesWatch() {
		// nacos doesn't support watch now , publish an event every 30 seconds.
		this.publisher.publishEvent( //30s Issue an event notifying SCG to repull
				new HeartbeatEvent(this, redisWatchIndex.getAndIncrement()));
	}

	@Override
	public void stop() {
		if (this.running.compareAndSet(true, false) && this.watchFuture != null) {
			this.watchFuture.cancel(true);
		}
	}

	@Override
	public boolean isRunning() {
		return false;
	}
}
Copy the code

This is done, based on Redis configuration of routing information and dynamic refresh function.

use

1. Data in Redis:



2, will RedisRouteDefinitionWatch, RedisRouteDefinitionRepository, RedisRouteDefinitionRepositoryOperator into the Spring container, such as @ Bean injection

Through the above two steps, can be completed. The code is poorly written.



You can verify by yourself, the test is effective.Code store address