暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

SpringCloud学习笔记——新服务网关Gateway动态路由

CodeWu 2021-01-25
1120

概述

在新服务网关——Gateway断言中,我们采用得是静态配置(配置文件或配置类方式)方式创建服务路由信息,在服务路由配置信息发生变更后,需要重新启动Gateway网关应用,无法实现动态加载服务路由配置信息并进行服务路由的功能。

静态路由加载

在Gateway实现静态路由加载的过程中,主要涉及以下核心类。

RouteLocator

路由定位器,该接口只有一个getRoutes方法,用于获取所有路由配置信息。

RouteLocatorBuilder

路由定位器创建类,通过routes方法创建所有服务路由信息。

Route

服务路由信息,定义了如下属性:

public class Route implements Ordered {
private final String id;//服务路由标识
private final URI uri;//服务实例路由地址
private final int order;//服务路由执行顺序
private final AsyncPredicate<ServerWebExchange> predicate;//服务路由断言集合
private final List<GatewayFilter> gatewayFilters;//服务路由过滤器集合
private final Map<String, Object> metadata;//服务路由元数据
}

RouteDefinition

用于封装服务路由信息,定义了如下属性:

public class RouteDefinition {
private String id;//服务路由标识
@NotEmpty
@Valid
private List<PredicateDefinition> predicates = new ArrayList();//断言集合
@Valid
private List<FilterDefinition> filters = new ArrayList();//过滤器集合
@NotNull
private URI uri;//服务实例uri地址
private Map<String, Object> metadata = new HashMap();//服务路由元数据信息
private int order = 0;//服务路由执行顺序
}

服务路由标识,不指定(配置文件或配置类)内容为UUID。多个RouteDefinition完成了gateway服务路由功能。

RouteDefinitionLocator

服务路由装载器,通过getRouteDefinitions方法获取服务路由集合信息。该接口提供不同的实现类,对应不同的服务路由配置信息加载方式。

类名描述
PropertiesRouteDefinitionLocator从配置文件中加载服务路由信息
CachingRouteDefinitionLocator从缓存中加载服务路由信息
CompositeRouteDefinitionLocator组合方式加载服务路由信息,为获取服务路由定义信息提供统一入口
InMemoryRouteDefinitionRepository从内存中加载服务路由信息
DiscoveryClientRouteDefinitionLocator从服务注册中心加载服务路由信息

GatewayAutoConfiguration

Gateway自动配置类,用于在Gateway应用启动时装配RouteDefinitionLocator并加载对应的服务路由信息。


如果需要动态刷新服务路由配置相关信息,需要发起RefreshRoutesEvent事件,服务路由装载器实现类会对该事件进行监听。

动态路由实现

Gateway不支持从MySQL中读取存储服务路由配置信息,需要从redis中读取服务路由配置信息。

断言信息和过滤器信息未通过数据库存储。

项目依赖

   <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>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>



<!--mybatis反向工程插件-->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.7</version>
<configuration>
<!-- 指定generatorConfig.xml配置文件位置 -->
<configurationFile>
${basedir}/src/main/resources/generatorConfig,xml
</configurationFile>
<verbose>true</verbose><!--允许移动生成的文件 -->
<overwrite>true</overwrite><!-- 是否覆盖 -->
</configuration>


<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.27</version>
</dependency>
</dependencies>
</plugin>

配置信息

spring:
redis:
database: 0
host: 127.0.0.1
port: 6379
password:
timeout: 500
lettuce:
pool:
max-active: 20
max-wait: -1
max-idle: 10
min-idle: 0
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true

mybatis反向工程generatorConfig,xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!--引入数据库连接配置文件-->
<properties resource="db.properties"/>
<!--数据库context-->
<context id="DB2Tables" targetRuntime="MyBatis3">
<commentGenerator>
<!--是否去除自动生成注释 true:是 false:否-->
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<!--配置数据库连接信息-->
<jdbcConnection driverClass="${jdbc.driver}" connectionURL="${jdbc.url}" userId="${jdbc.username}" password="${jdbc.password}"/>
<!--默认false,把JDBC DECIMAL和NUMERIC解析为Integer,
true,把JDBC DECIMAL和NUMERIC解析为BigDecimal
-->
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!--指定JavaBean生成位置
targetPackage:目标包名
targetProject:目标路径
-->
<javaModelGenerator targetPackage="com.spring.cloud.gateway.customize.pojo" targetProject=".\src\main\java">
<!--是否让schema做为包的后缀-->
<property name="enableSubPackages" value="true"/>
<!--数据库返回值清除前后空格-->
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!--指定Mapper映射器文件生成位置
targetPackage:目标包名
targetProject:目标路径
-->
<sqlMapGenerator targetPackage="mapper" targetProject=".\src\main\resources">
<!--是否让schema做为包的后缀-->
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!--指定Mapper映射器文件接口生成位置
targetPackage:目标包名
targetProject:目标路径
-->
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.spring.cloud.gateway.customize.mapper"
targetProject=".\src\main\java"
>
<!--是否让schema做为包的后缀-->
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!--指定数据表和对应实体间的生成策略
tableName:数据表名(此处为员工调薪表)
domainObjectName:实体名
enableCountByExample:是否生成按查询条件计数实体(false:不生成)
enableUpdateByExample:是否生成按条件更新实体(false:不生成)
enableDeleteByExample:是否生成按条件删除实体(false:不生成)
enableSelectByExample:是否生成按条件查询实体(false:不生成)
selectByExampleQueryId:是否生成按主键查询实体(false:不生成)
-->
<table tableName="db_config_route" domainObjectName="DbConfigRoute"
enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false"
enableSelectByExample="false" selectByExampleQueryId="false"></table>


</context>


</generatorConfiguration>

数据库配置文件

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/zuul_route?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=xxxxxx

对应实体信息

public class DbConfigRoute {
private Integer id;


private String path;


private String serviceId;


private String url;


private Boolean enable;


private Boolean retryable;
}

前端参数封装

public class GatewayRouteParam {
private String path;
private String serviceId;
private String url;
private Boolean enable;
private Boolean retryable;
}

服务路由数据访问层映射接口

public interface DbConfigRouteMapper {


/**
* 根据服务路由标识查询服务路由详情信息
* @param id:服务路由标识
* @return:服务路由详情
*/
DbConfigRoute selectByPrimaryKey(Integer id);


/**
* 查询所有服务路由信息
* @return 服务路由集合
*/
List<DbConfigRoute> findAllDbConfigRoute();
/**
* 添加动态路由配置信息
* @param dbConfigRoute:动态路由配置实体
* @return 受影响行数
*/
public Integer addRoute(DbConfigRoute dbConfigRoute);


/**
* 更新动态路由配置信息
* @param dbConfigRoute:动态路由配置实体
* @return 受影响行数
*/
public Integer updateRoute(DbConfigRoute dbConfigRoute);


/**
* 删除动态路由配置信息
* @param serviceId:动态路由服务标识
* @return 受影响行数
*/
public Integer deleteRoute(String serviceId);
}

数据访问层映射实现

<mapper namespace="com.spring.cloud.gateway.customize.mapper.DbConfigRouteMapper">
<resultMap id="BaseResultMap" type="com.spring.cloud.gateway.customize.pojo.DbConfigRoute">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="path" jdbcType="VARCHAR" property="path" />
<result column="service_id" jdbcType="VARCHAR" property="serviceId" />
<result column="url" jdbcType="VARCHAR" property="url" />
<result column="enable" jdbcType="BIT" property="enable" />
<result column="retryable" jdbcType="BIT" property="retryable" />
</resultMap>
<sql id="Base_Column_List">
id, path, service_id, url, enable, retryable
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from db_config_route
where id = #{id,jdbcType=INTEGER}
</select>
<select id="findAllDbConfigRoute" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from db_config_route where retryable=true
</select>
<insert id="addRoute" parameterType="com.spring.cloud.gateway.customize.pojo.DbConfigRoute">
insert into db_config_route (path,service_id,url,enable,retryable)
values(#{path,jdbcTye=VARCHAR},#{serviceId,jdbcType=VARCHAR},
#{url,jdbcType=VARCHAR},#{enable,jdbcType=BIT},#{retryable,jdbcType=BIT})
</insert>
<update id="updateRoute" parameterType="com.spring.cloud.gateway.customize.pojo.DbConfigRoute">
update db_config_route set path=#{path,jdbcType=VARCHAR},url=#{url,jdbcType=VARCHAR}
where service_id=#{serviceId,jdbcType=VARCHAR}
</update>
<update id="deleteRoute" parameterType="java.lang.String">
update db_config_route set enable=#{enable,jdncType=BIT}
    where service_id=#{serviceId,jdbcType=VARCHAR}
</update>
</mapper>

从Redis中加载服务路由缓存信息至配置文件

/**
* 从redis中加载服务路由缓存信息至配置文件
*/
@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
private static final String configKey="gateway:routes";//与配置文件中的服务网关配置内容一致
@Autowired
private RedisTemplate<String,Object> redisTemplate;
/**
*从redis中获取缓存中的服务路由信息并保存至配置文件
* @return
*/
@Override
public Flux<RouteDefinition> getRouteDefinitions(){
ObjectMapper routeConfigMapper=new ObjectMapper();
List<RouteDefinition> routeDefinitionList=new ArrayList<RouteDefinition>();
redisTemplate.opsForHash().values(configKey).stream().forEach(routeDefinition->{
try {
String routeInfo=routeConfigMapper.writeValueAsString(routeDefinition);
routeDefinitionList.add(routeConfigMapper.readValue(routeInfo,RouteDefinition.class));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
});
return Flux.fromIterable(routeDefinitionList);
}


/**
*从redis中读取新增加的服务路由信息并保存至配置文件
* @param route
* @return
*/
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
ObjectMapper routeConfigMapper=new ObjectMapper();
return route.flatMap(routeDefinition->{
try {
String routeInfo=routeConfigMapper.writeValueAsString(routeDefinition);
redisTemplate.opsForHash().put(configKey,routeDefinition.getId(),routeInfo);
return Mono.empty();
} catch (JsonProcessingException e) {
e.printStackTrace();
}
});
}


/**
* 从配置文件和redis中移除服务路由配置信息
* @param routeId:服务路由标识
* @return
*/
@Override
public Mono<Void> delete(Mono<String> routeId){
return routeId.flatMap(id->{
if(redisTemplate.opsForHash().hasKey(configKey,id))
{
redisTemplate.opsForHash().delete(configKey,id);
return Mono.empty();
}
return Mono.defer(()->Mono.error(new NotFoundException("配置文件中未找到id为"+id+"的服务网关配置信息")));
});
}
}

Gateway启动时加载服务路由配置信息

需实现如下接口:

接口
描述
CommandLineRunner项目启动后执行某项功能,需执行功能代码在run方法中实现
ApplicationEventPublisherAware事件发布器,需要在服务路由配置信息更新后发布服务网关配置刷新事件(RefreshRoutesEvent)
@Service
public class GatewayServiceHandler implements ApplicationEventPublisherAware, CommandLineRunner {
@Autowired
private DbConfigRouteMapper dbConfigRouteMapper;
@Autowired
private RedisRouteDefinitionRepository redisRouteDefinitionRepository;
private ApplicationEventPublisher publisher;//用于发布事件并对事件进行监听
@Autowired
private RedisTemplate redisTemplate;
/**
* gateway启动时执行该方法
* @param args
* @throws Exception
*/
@Override
public void run(String... args) throws Exception {
this.loadRouteConfig();
}


/**
*设置应用程序发布事件
* @param applicationEventPublisher
*/
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher=applicationEventPublisher;
}


/**
* 将数据库中的服务路由配置信息转换为字符串
* @return
*/
public String loadRouteConfig()
{
//清空缓存中的服务路由信息
this.redisTemplate.delete(RedisRouteDefinitionRepository.configKey);
//迭代服务路由集合,添加至缓存并保存至配置文件
List<DbConfigRoute> dbConfigRoutes=dbConfigRouteMapper.findAllDbConfigRoute();
dbConfigRoutes.forEach(dbConfigRoute -> {
RouteDefinition routeDefinition=convertRoute(dbConfigRoute);
redisRouteDefinitionRepository.save(Mono.just(routeDefinition)).subscribe();
});
//发布刷新配置事件
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}


/**
* 将服务路由实体信息转换为RouteDefinition
* @param route:服务路由实体
* @return RouteDefinition
*/
private RouteDefinition convertRoute(DbConfigRoute route)
{
RouteDefinition routeDefinition=new RouteDefinition();//服务路由包装类
PredicateDefinition predicateDefinition=new PredicateDefinition();//服务断言包装类


URI uri=null;
if(route.getUrl().startsWith("http"))
{
uri= UriComponentsBuilder.fromHttpUrl(route.getUrl()).build().toUri();
}
else{
uri= UriComponentsBuilder.fromUriString("lb://"+route.getUrl()).build().toUri();
}
routeDefinition.setId(route.getServiceId());
routeDefinition.setUri(uri);
//添加Path断言信息
Map<String,String> predicateParam=new HashMap<String,String>();
predicateParam.put("Path",route.getPath());
predicateDefinition.setArgs(predicateParam);
routeDefinition.setPredicates(Arrays.asList(predicateDefinition));
return routeDefinition;
}
public void saveRouteConfig(DbConfigRoute routeConfig)
{
RouteDefinition routeDefinition=convertRoute(routeConfig);
redisRouteDefinitionRepository.save(Mono.just(routeDefinition)).subscribe();
//发布刷新配置事件
this.publisher.publishEvent(new RefreshRoutesEvent(this));
}
public void updateRouteConfig(DbConfigRoute routeConfig)
{
RouteDefinition routeDefinition=convertRoute(routeConfig);
try {
redisRouteDefinitionRepository.delete(Mono.just(routeDefinition.getId()));
redisRouteDefinitionRepository.save(Mono.just(routeDefinition)).subscribe();
//发布刷新配置事件
this.publisher.publishEvent(new RefreshRoutesEvent(this));
}catch (Exception e)
{
e.printStackTrace();
}
}
public void deleteRouteConfig(DbConfigRoute configRoute)
{
RouteDefinition routeDefinition=convertRoute(configRoute);
redisRouteDefinitionRepository.delete(Mono.just(routeDefinition.getId()));
//发布刷新配置事件
this.publisher.publishEvent(new RefreshRoutesEvent(this));


}
}

动态路由数据接口

@RestController
@RequestMapping("/gatewayRoute")
public class GatawayConfigRouteController {
@Autowired
private GatewayServiceHandler gatewayServiceHandler;
@Autowired
private DbConfigRouteMapper dbConfigRouteMapper;
@GetMapping("/refreshGatewayConfig")
public String refreshRouteConfig()
{
return this.gatewayServiceHandler.loadRouteConfig();
}
@PostMapping("/saveGatewayConfig")
public String saveRouteConfig(@RequestBody GatewayRouteParam param)
{
DbConfigRoute dbConfigRoute=new DbConfigRoute();
BeanUtils.copyProperties(param,dbConfigRoute);
Integer rowCount=dbConfigRouteMapper.addRoute(dbConfigRoute);
if(rowCount>0)
{
refreshRouteConfig();
}
else{
return "保存动态路由配置信息失败";
}
}
@PostMapping("/updateGatewayConfig")
public String updateRouteConfig(@RequestBody GatewayRouteParam param)
{
DbConfigRoute dbConfigRoute=new DbConfigRoute();
BeanUtils.copyProperties(param,dbConfigRoute);
Integer rowCount=dbConfigRouteMapper.updateRoute(dbConfigRoute);
if(rowCount>0)
{
refreshRouteConfig();
}
else{
return "更新动态路由配置信息失败";
}
}
@PostMapping("/deleteGatewayConfig")
public String deleteRouteConfig(String serviceId)
{
Integer rowCount=dbConfigRouteMapper.deleteRoute(serviceId);
if(rowCount>0)
{
refreshRouteConfig();
}
else{
return "删除路由配置信息失败";
}
}
}



文章转载自CodeWu,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论