spring cloud gateway动态路由实现

为什么要用动态路由

个人觉得动态路由主要是解决微服务动态增减调整应运而生的;

动态路由的整体思路

路由配置加载过程

在gateway加载配置GatewayAutoConfiguration文件中,先判断是否存在自定义的加载器

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

RouteDefinitionRepository是gateway提供的自定义路由的接口,其继承了RouteDefinitionLocator和RouteDefinitionWriter

package org.springframework.cloud.gateway.route;

/**
 * @author Spencer Gibb
 */
public interface RouteDefinitionRepository extends RouteDefinitionLocator, RouteDefinitionWriter {

}

从上面源码可以看出,我们自己实现动态路由,可以从继承RouteDefinitionRepository进行实现

路由信息的存儲

我采用的是redis同步mysql数据的方式进行存储;根据个人喜好,其它方式均可以

代码实现

存储设置

DROP TABLE IF EXISTS `sys_route_conf`;
CREATE TABLE `sys_route_conf` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `route_name` varchar(30) DEFAULT NULL,
  `route_id` varchar(30) DEFAULT NULL,
  `predicates` json DEFAULT NULL COMMENT '断言',
  `filters` json DEFAULT NULL COMMENT '过滤器',
  `uri` varchar(50) DEFAULT NULL,
  `order` int(2) DEFAULT '0' COMMENT '排序',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `del_flag` char(1) DEFAULT '0',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COMMENT='路由配置表';

用户保存设置的路由信息

定义动态路由事件

import org.springframework.context.ApplicationEvent;

/**
 * @author moorgen
 * @date 2018/11/5
 * <p>
 * 路由初始化事件
 */
public class DynamicRouteInitEvent extends ApplicationEvent {
	public DynamicRouteInitEvent(Object source) {
		super(source);
	}
}

web系统的设置文件


import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.pig4cloud.pig.admin.service.SysRouteConfService;
import com.pig4cloud.pig.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.gateway.support.DynamicRouteInitEvent;
import com.pig4cloud.pigx.common.gateway.vo.RouteDefinitionVo;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.context.WebServerInitializedEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.scheduling.annotation.Async;

import java.net.URI;

/**
 * @author moorgen
 * @date 2018/10/31
 * <p>
 * 容器启动后保存配置文件里面的路由信息到Redis
 */
@Slf4j
@Configuration
@AllArgsConstructor
public class DynamicRouteInitRunner {
	private final RedisTemplate redisTemplate;
	private final SysRouteConfService routeConfService;
	
	@Async
	@Order
	//监听web初始化时间和动态路由事件
	@EventListener({WebServerInitializedEvent.class, DynamicRouteInitEvent.class})
	public void initRoute() {
		Boolean result = redisTemplate.delete(CacheConstants.ROUTE_KEY);
		log.info("初始化网关路由 {} ", result);
		//把mysql的数据加载到redis中
		routeConfService.routes().forEach(route -> {
			RouteDefinitionVo vo = new RouteDefinitionVo();
			vo.setRouteName(route.getRouteName());
			vo.setId(route.getRouteId());
			vo.setUri(URI.create(route.getUri()));
			vo.setOrder(route.getOrder());

			JSONArray filterObj = JSONUtil.parseArray(route.getFilters());
			vo.setFilters(filterObj.toList(FilterDefinition.class));
			JSONArray predicateObj = JSONUtil.parseArray(route.getPredicates());
			vo.setPredicates(predicateObj.toList(PredicateDefinition.class));

			log.info("加载路由ID:{},{}", route.getRouteId(), vo);
			redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(RouteDefinitionVo.class));
			redisTemplate.opsForHash().put(CacheConstants.ROUTE_KEY, route.getRouteId(), vo);
		});
		log.debug("初始化网关路由结束 ");
	}

	/**
	 * 用不到的时候可以忽略
	 * redis 监听配置,监听 gateway_redis_route_reload_topic,重新加载Redis
	 *
	 * @param redisConnectionFactory redis 配置
	 * @return
	 */
	@Bean
	public RedisMessageListenerContainer redisContainer(RedisConnectionFactory redisConnectionFactory) {
		RedisMessageListenerContainer container= new RedisMessageListenerContainer();
		container.setConnectionFactory(redisConnectionFactory);
		container.addMessageListener((message, bytes) -> {
			log.warn("接收到重新Redis 重新加载路由事件");
			initRoute();
		}, new ChannelTopic(CacheConstants.ROUTE_REDIS_RELOAD_TOPIC));
		return container;
	}
}

更新实现

/**
 * 更新路由信息
 *
 * @param routes 路由信息
 * @return
 */
@Override
@Transactional(rollbackFor = Exception.class)
public Mono<Void> updateRoutes(JSONArray routes) {
	// 清空Redis 缓存
	Boolean result = redisTemplate.delete(CacheConstants.ROUTE_KEY);
	log.info("清空网关路由 {} ", result);

	// 遍历修改的routes,保存到Redis
	List<RouteDefinitionVo> routeDefinitionVoList = new ArrayList<>();

	try {
		routes.forEach(value -> {
			log.info("更新路由 ->{}", value);
			RouteDefinitionVo vo = new RouteDefinitionVo();
			Map<String, Object> map = (Map) value;

			Object id = map.get("routeId");
			if (id != null) {
				vo.setId(String.valueOf(id));
			}

			Object routeName = map.get("routeName");
			if (routeName != null) {
				vo.setRouteName(String.valueOf(routeName));
			}

			Object predicates = map.get("predicates");
			if (predicates != null) {
				JSONArray predicatesArray = (JSONArray) predicates;
				List<PredicateDefinition> predicateDefinitionList =
						predicatesArray.toList(PredicateDefinition.class);
				vo.setPredicates(predicateDefinitionList);
			}

			Object filters = map.get("filters");
			if (filters != null) {
				JSONArray filtersArray = (JSONArray) filters;
				List<FilterDefinition> filterDefinitionList
						= filtersArray.toList(FilterDefinition.class);
				vo.setFilters(filterDefinitionList);
			}

			Object uri = map.get("uri");
			if (uri != null) {
				vo.setUri(URI.create(String.valueOf(uri)));
			}

			Object order = map.get("order");
			if (order != null) {
				vo.setOrder(Integer.parseInt(String.valueOf(order)));
			}
			redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(RouteDefinitionVo.class));
			redisTemplate.opsForHash().put(CacheConstants.ROUTE_KEY, vo.getId(), vo);
			routeDefinitionVoList.add(vo);
		});

		// 逻辑删除全部
		SysRouteConf condition = new SysRouteConf();
		condition.setDelFlag(CommonConstants.STATUS_NORMAL);
		this.remove(new UpdateWrapper<>(condition));

		//插入生效路由
		List<SysRouteConf> routeConfList = routeDefinitionVoList.stream().map(vo -> {
			SysRouteConf routeConf = new SysRouteConf();
			routeConf.setRouteId(vo.getId());
			routeConf.setRouteName(vo.getRouteName());
			routeConf.setFilters(JSONUtil.toJsonStr(vo.getFilters()));
			routeConf.setPredicates(JSONUtil.toJsonStr(vo.getPredicates()));
			routeConf.setOrder(vo.getOrder());
			routeConf.setUri(vo.getUri().toString());
			return routeConf;
		}).collect(Collectors.toList());
		this.saveBatch(routeConfList);
		log.debug("更新网关路由结束 ");

		this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
		redisTemplate.convertAndSend(CacheConstants.ROUTE_JVM_RELOAD_TOPIC, "UPMS路由信息,网关缓存更新");
	} catch (Exception e) {
		log.error("路由配置解析失败", e);
		// 回滚路由,重新加载即可
		this.applicationEventPublisher.publishEvent(new DynamicRouteInitEvent(this));
		// 抛出异常
		throw new RuntimeException(e);
	}
	return Mono.empty();
}

公共模块

import com.pig4cloud.pig.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.gateway.support.RedisRouteDefinitionWriter;
import com.pig4cloud.pigx.common.gateway.support.RouteCacheHolder;
import io.lettuce.core.ReadFrom;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.config.PropertiesRouteDefinitionLocator;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;

import java.time.Duration;



@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public class  DynamicRouteAutoConfiguration {
	/**
	 * 配置文件设置为空
	 * redis 加载为准
	 *
	 * @return
	 */
	@Bean
	public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator() {
		return new PropertiesRouteDefinitionLocator(new GatewayProperties());
	}


	/**
	 * redis 监听配置
	 *
	 * @param redisConnectionFactory redis 配置
	 * @return
	 */
	@Bean
	public RedisMessageListenerContainer redisContainer(RedisConnectionFactory redisConnectionFactory) {
		RedisMessageListenerContainer container= new RedisMessageListenerContainer();
		container.setConnectionFactory(redisConnectionFactory);
		container.addMessageListener((message, bytes) -> {
			log.info("接收到重新JVM 重新加载路由事件");
			RouteCacheHolder.removeRouteList();
		}, new ChannelTopic(CacheConstants.ROUTE_JVM_RELOAD_TOPIC));
		return container;
	}


	@Bean
	@ConditionalOnProperty(value = "spring.redis.cluster.enable",havingValue = "true")
	public LettuceConnectionFactory redisConnectionFactory(RedisProperties redisProperties) {
		RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());

		// https://github.com/lettuce-io/lettuce-core/wiki/Redis-Cluster#user-content-refreshing-the-cluster-topology-view
		ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
				.enablePeriodicRefresh()
				.enableAllAdaptiveRefreshTriggers()
				.refreshPeriod(Duration.ofSeconds(5))
				.build();

		ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
				.topologyRefreshOptions(clusterTopologyRefreshOptions).build();

		// https://github.com/lettuce-io/lettuce-core/wiki/ReadFrom-Settings
		LettuceClientConfiguration lettuceClientConfiguration = LettuceClientConfiguration.builder()
				.readFrom(ReadFrom.REPLICA_PREFERRED)
				.clientOptions(clusterClientOptions).build();

		return new LettuceConnectionFactory(redisClusterConfiguration, lettuceClientConfiguration);
	}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值