摘要
本文讲解在Spring Cloud 中如何通过MySQL和redis实现动态路由配置,以及路由信息持久化在MySQL中,同时使用Redis作为分布式路由信息缓存。
本文讲解在Spring Cloud 中如何通过MySQL和redis实现动态路由配置,以及路由信息持久化在MySQL中,同时使用Redis作为分布式路由信息缓存。
Sping Cloud gateway 中自己集成了一套基于配置文件的一套路由规则,该规则需要配置在application.yml/properties文件中,如果在使用配置文件时想要动态化实现路由配置,需要网关结合Spring cloud config一起来使用(路由配置在config配置中心中,随着config修改路由信息,gateway会自动刷新而不需要重启刷新路由)。
本文将会在上述基础之上修改路由的存储方式为MySQL,并且把路由信息缓存在redis中,当数据库中的路由信息发生变化时, 可以主动通知网关去重新加载路由信息。
我们在使用此方法改造前,请去掉您的配置文件中配置的路由规则
注意,本部分代码需要使用的部分依赖为:
<!-- ali json依赖 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> <!-- gateway --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!--redis依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
首先我们需要实现接口,重写路由加载方法:
package cn.com.xxxx.route; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.cloud.gateway.route.RouteDefinitionRepository; import org.springframework.cloud.gateway.support.NotFoundException; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import com.alibaba.fastjson.JSON; import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /*** * @project: xxxx * @description: 从redis中读取路由信息到webFlux中 * @version 1.0.0 * @errorcode * 错误码: 错误描述 * @author * <li>2020-07-14 guopengfei@xxxx.com.cn Create 1.0 * @copyright ©2019-2020 xxxx,版权所有。 */ @Component @Slf4j public class RedisRouteDefinitionRepository implements RouteDefinitionRepository { public static final String GATEWAY_ROUTES = "gateway:routes"; @Autowired private RedisTemplate<String, Object> redisTemplate; // 请注意,此方法很重要,从redis取路由信息的方法,官方核心包要用,核心路由功能都是从redis取的 @Override public Flux<RouteDefinition> getRouteDefinitions() { log.info("从redis读取路由信息 begin >>>>>>>>>>>>>>>>>>>>>"); List<RouteDefinition> routeDefinitions = new ArrayList<>(); redisTemplate.opsForHash().values(GATEWAY_ROUTES).stream().forEach(routeDefinition -> { routeDefinitions.add(JSON.parseObject(routeDefinition.toString(), RouteDefinition.class)); }); return Flux.fromIterable(routeDefinitions); } @Override public Mono<Void> save(Mono<RouteDefinition> route) { return route.flatMap(routeDefinition -> { redisTemplate.opsForHash().put(GATEWAY_ROUTES, routeDefinition.getId(), JSON.toJSONString(routeDefinition)); return Mono.empty(); }); } @Override public Mono<Void> delete(Mono<String> routeId) { return routeId.flatMap(id -> { if (redisTemplate.opsForHash().hasKey(GATEWAY_ROUTES, id)) { redisTemplate.opsForHash().delete(GATEWAY_ROUTES, id); return Mono.empty(); } return Mono.defer(() -> Mono.error(new NotFoundException("路由文件没有找到: " + routeId))); }); } }
上述代码中,提供了从redis加载路由信息到Flux中,新增路由(redis)、删除路由(redis)的方法。
下面为项目初始化时加载数据库的路由到redis,以及查询所有路由信息( 从redis)、刷新Flux中路由信息的方法
package cn.com.xxxx.route; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.cloud.gateway.event.RefreshRoutesEvent; 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.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.web.util.UriComponentsBuilder; import com.alibaba.fastjson.JSON; import cn.com.xxxx.dao.GatewayRouteInfoDao; import cn.com.xxxx.pojo.GatewayRouteInfo; import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Mono; /*** * @project: xxxx * @description: 项目初始化加载数据库的路由配置到redis * @version 1.0.0 * @errorcode * 错误码: 错误描述 * @author * <li>2020-07-14 guopengfei@xxxx.com.cn Create 1.0 * @copyright ©2019-2020 xxxx,版权所有。 */ @Slf4j @Service public class GatewayServiceHandler implements ApplicationEventPublisherAware, CommandLineRunner { @Autowired private RedisRouteDefinitionRepository routeDefinitionWriter; private ApplicationEventPublisher publisher; @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; } @Autowired private RedisTemplate redisTemplate; // 自己的获取数据dao @Autowired private GatewayRouteInfoDao gatewayRouteInfoDao; @Override public void run(String... args) { this.loadRouteConfig(); } @SuppressWarnings("unchecked") public String loadRouteConfig() { log.info("====开始加载=====网关配置信息========="); // 删除redis里面的路由配置信息 redisTemplate.delete(RedisRouteDefinitionRepository.GATEWAY_ROUTES); // 从数据库拿到基本路由配置 List<GatewayRouteInfo> gatewayRouteList = gatewayRouteInfoDao.queryAllRoutes(); gatewayRouteList.forEach(gatewayRoute -> { RouteDefinition definition = handleData(gatewayRoute); routeDefinitionWriter.save(Mono.just(definition)).subscribe(); }); this.publisher.publishEvent(new RefreshRoutesEvent(this)); log.info("=======网关配置信息===加载完成======"); return "success"; } /** * 查询所有已经加载的路由 * * @return */ @SuppressWarnings("unchecked") public List<GatewayRouteInfo> queryAllRoutes() { List<GatewayRouteInfo> gatewayRouteInfos = new ArrayList<GatewayRouteInfo>(); redisTemplate.opsForHash().values(RedisRouteDefinitionRepository.GATEWAY_ROUTES).stream() .forEach(routeDefinition -> { RouteDefinition definition = JSON.parseObject(routeDefinition.toString(), RouteDefinition.class); gatewayRouteInfos.add(convert2GatewayRouteInfo(definition)); }); return gatewayRouteInfos; } /** * 将redis中路由信息转换为返回给前端的路由信息 * * @param routeDefinition * redis中的路由 * @return */ private GatewayRouteInfo convert2GatewayRouteInfo(Object obj) { RouteDefinition routeDefinition = (RouteDefinition) obj; GatewayRouteInfo gatewayRouteInfo = new GatewayRouteInfo(); gatewayRouteInfo.setUri(routeDefinition.getUri().toString()); gatewayRouteInfo.setServiceId(routeDefinition.getId()); List<PredicateDefinition> predicates = routeDefinition.getPredicates(); // 只有一个 if (CollectionUtils.isNotEmpty(predicates)) { String predicatesString = predicates.get(0).getArgs().get("pattern"); gatewayRouteInfo.setPredicates(predicatesString); } List<FilterDefinition> filters = routeDefinition.getFilters(); if (CollectionUtils.isNotEmpty(filters)) { String filterString = filters.get(0).getArgs().get("_genkey_0"); gatewayRouteInfo.setFilters(filterString); } gatewayRouteInfo.setOrder(String.valueOf(routeDefinition.getOrder()));; return gatewayRouteInfo; } public void saveRoute(GatewayRouteInfo gatewayRouteInfo) { RouteDefinition definition = handleData(gatewayRouteInfo); routeDefinitionWriter.save(Mono.just(definition)).subscribe(); this.publisher.publishEvent(new RefreshRoutesEvent(this)); } public void update(GatewayRouteInfo gatewayRouteInfo) { RouteDefinition definition = handleData(gatewayRouteInfo); try { this.routeDefinitionWriter.delete(Mono.just(definition.getId())); routeDefinitionWriter.save(Mono.just(definition)).subscribe(); this.publisher.publishEvent(new RefreshRoutesEvent(this)); } catch (Exception e) { e.printStackTrace(); } } public void deleteRoute(String routeId) { routeDefinitionWriter.delete(Mono.just(routeId)).subscribe(); this.publisher.publishEvent(new RefreshRoutesEvent(this)); } /** * 路由数据转换公共方法 * * @param gatewayRoute * @return */ private RouteDefinition handleData(GatewayRouteInfo gatewayRouteInfo) { RouteDefinition definition = new RouteDefinition(); Map<String, String> predicateParams = new HashMap<>(8); PredicateDefinition predicate = new PredicateDefinition(); FilterDefinition filterDefinition = new FilterDefinition(); Map<String, String> filterParams = new HashMap<>(8); URI uri = null; if (gatewayRouteInfo.getUri().startsWith("http")) { // http地址 uri = UriComponentsBuilder.fromHttpUrl(gatewayRouteInfo.getUri()).build().toUri(); } else { // 注册中心 uri = UriComponentsBuilder.fromUriString("lb://" + gatewayRouteInfo.getUri()).build().toUri(); } definition.setId(gatewayRouteInfo.getServiceId()); // 名称是固定的,spring gateway会根据名称找对应的PredicateFactory predicate.setName("Path"); predicateParams.put("pattern", gatewayRouteInfo.getPredicates()); predicate.setArgs(predicateParams); // 名称是固定的, 路径去前缀 filterDefinition.setName("StripPrefix"); filterParams.put("_genkey_0", gatewayRouteInfo.getFilters().toString()); filterDefinition.setArgs(filterParams); definition.setPredicates(Arrays.asList(predicate)); definition.setFilters(Arrays.asList(filterDefinition)); definition.setUri(uri); definition.setOrder(Integer.parseInt(gatewayRouteInfo.getOrder())); return definition; } }
以下为路由controller,提供了刷新路由的接口、查询已经加载的路由信息的接口
package cn.com.xxxx.controller; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.com.xxxx.commons.enums.BobfintechErrorNoEnum; import cn.com.xxxx.commons.pojo.BaseResponse; import cn.com.xxxx.commons.pojo.ResponseData; import cn.com.xxxx.pojo.GatewayRouteInfo; import cn.com.xxxx.route.GatewayServiceHandler; /** * @project: xxxx * @description: 路由controller * @version 1.0.0 * @errorcode * 错误码: 错误描述 * @author * <li>2020-07-15 guopengfei@xxxx.com.cn Create 1.0 * @copyright ©2019-2020 xxxx,版权所有。 */ @RestController @RequestMapping("/route") public class RouteController { @Autowired private GatewayServiceHandler gatewayServiceHandler; /** * 刷新路由配置 * * @param gwdefinition * @return */ @GetMapping("/refresh") public BaseResponse refresh() throws Exception { this.gatewayServiceHandler.loadRouteConfig(); return ResponseData.out(BobfintechErrorNoEnum.COM_BOBFINTECH_SUCCESS); } @SuppressWarnings("rawtypes") @GetMapping("/routes") public ResponseData routes() throws Exception { List<GatewayRouteInfo> gatewayRouteInfos = gatewayServiceHandler.queryAllRoutes(); return ResponseData.out(BobfintechErrorNoEnum.COM_BOBFINTECH_SUCCESS, gatewayRouteInfos); } }
GatewayRouteInfoDao如下(提供从mysql中查询路由信息的接口,注意,对路由表的CRUD后,可以直接调用上面controller中的刷新接口进行刷新,CURD自己实现吧):
package cn.com.xxxx.dao; import java.util.List; import org.apache.ibatis.annotations.Mapper; import cn.com.xxxx.pojo.GatewayRouteInfo; /** * @project: xxxx * @description: 路由Dao * @version 1.0.0 * @errorcode * 错误码: 错误描述 * @author * <li>2020-07-14 guopengfei@xxxx.com.cn Create 1.0 * @copyright ©2019-2020 xxxx,版权所有。 */ @Mapper public interface GatewayRouteInfoDao { List<GatewayRouteInfo> queryAllRoutes(); }
最后一个就是实体类:
package cn.com.xxxx.pojo; import java.util.Date; import lombok.Data; /** * @project: xxxx * @description: 路由信息类 * @version 1.0.0 * @errorcode * 错误码: 错误描述 * @author * <li>2020-07-14 guopengfei@xxxx.com.cn Create 1.0 * @copyright ©2019-2020 xxxx,版权所有。 */ @Data public class GatewayRouteInfo { private Long id; private String serviceId; private String uri; private String predicates; private String filters; private String order; private Date createDate; private Date updateDate; private String remarks; private String delFlag; }
下面是路由信息表的表结构:
DROP TABLE IF EXISTS `bob_gateway_route_info`; CREATE TABLE `bob_gateway_route_info` ( `id` int(20) NOT NULL, `service_id` varchar(100) NOT NULL COMMENT '服务id', `uri` varchar(100) NOT NULL COMMENT '转发地址', `predicates` varchar(200) NOT NULL COMMENT '访问路径', `filters` varchar(100) NOT NULL COMMENT '过滤条件', `order` varchar(2) NOT NULL DEFAULT '0' COMMENT '顺序', `create_date` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', `update_date` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `remarks` varchar(255) NOT NULL COMMENT '备注', `del_flag` varchar(255) NOT NULL COMMENT '删除标记', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of bob_gateway_route_info -- ---------------------------- INSERT INTO `bob_gateway_route_info` VALUES ('1', 'userService1', 'bob-userservice', '/userService/**', '1', '0', '2020-07-15 10:46:39', '2020-07-15 10:46:39', '测试用户服务访问', '0');
该表中只有一个测试服务bob-userservice(你需要有一个这样的服务,当然,这个配置有你决定)
表中每个参数和application配置文件中对应关系(方便你填写表用的)如下:
表 | 配置文件 | 说明 |
service_id | id | 路由标识(id:标识,具有唯一性) |
predicates | predicates | 路由条件(predicates:断言,匹配 HTTP 请求内容),网关代理的uri |
filters | filters | 过滤器,例如可通过- AddRequestParameter=name, zwc设置转发时添加指定参数 |
order | order | 路由执行的顺序 |
配置上述代码后,启动你的gateway项目,访问如下请求即可查询加载的路由想信息
http://localhost:你的端口号/route/routes
通过网关访问您设置的路由,将能成功访问。
向数据库添加新的路由,访问如下请求即可刷新路由:
http://localhost:8000/route/refresh
此时访问您添加的路由,将能正常访问。