为什么要用动态路由
个人觉得动态路由主要是解决微服务动态增减调整应运而生的;
动态路由的整体思路
路由配置加载过程
在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);
}
}