Srping cloud gateway 实现动态路由(MySQL持久化+redis分布式缓存)

作者:青山常在人不老   阅读 (2475)  |  收藏 (0)  |  点赞 (0)

摘要

本文讲解在Spring Cloud 中如何通过MySQL和redis实现动态路由配置,以及路由信息持久化在MySQL中,同时使用Redis作为分布式路由信息缓存。


原文链接:Srping cloud gateway 实现动态路由(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_idid路由标识(id:标识,具有唯一性)
predicatespredicates路由条件(predicates:断言,匹配 HTTP 请求内容),网关代理的uri
filtersfilters过滤器,例如可通过- AddRequestParameter=name, zwc设置转发时添加指定参数
orderorder路由执行的顺序

配置上述代码后,启动你的gateway项目,访问如下请求即可查询加载的路由想信息

http://localhost:你的端口号/route/routes

通过网关访问您设置的路由,将能成功访问。

向数据库添加新的路由,访问如下请求即可刷新路由:

http://localhost:8000/route/refresh

此时访问您添加的路由,将能正常访问。

分类   Spring boot 开发
字数   14327

博客标签    Spring cloud 动态路由   gateway动态路由  

评论