
概述
在新服务网关——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@Validprivate List<PredicateDefinition> predicates = new ArrayList();//断言集合@Validprivate List<FilterDefinition> filters = new ArrayList();//过滤器集合@NotNullprivate 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: 0host: 127.0.0.1port: 6379password:timeout: 500lettuce:pool:max-active: 20max-wait: -1max-idle: 10min-idle: 0mybatis:mapper-locations: classpath:mapper/*.xmlconfiguration:map-underscore-to-camel-case: true
mybatis反向工程generatorConfig,xml
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE generatorConfigurationPUBLIC "-//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.Driverjdbc.url=jdbc:mysql://localhost:3306/zuul_route?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaijdbc.username=rootjdbc.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_routewhere 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中加载服务路由缓存信息至配置文件*/@Componentpublic class RedisRouteDefinitionRepository implements RouteDefinitionRepository {private static final String configKey="gateway:routes";//与配置文件中的服务网关配置内容一致@Autowiredprivate RedisTemplate<String,Object> redisTemplate;/***从redis中获取缓存中的服务路由信息并保存至配置文件* @return*/@Overridepublic 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*/@Overridepublic 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*/@Overridepublic 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) |
@Servicepublic class GatewayServiceHandler implements ApplicationEventPublisherAware, CommandLineRunner {@Autowiredprivate DbConfigRouteMapper dbConfigRouteMapper;@Autowiredprivate RedisRouteDefinitionRepository redisRouteDefinitionRepository;private ApplicationEventPublisher publisher;//用于发布事件并对事件进行监听@Autowiredprivate RedisTemplate redisTemplate;/*** gateway启动时执行该方法* @param args* @throws Exception*/@Overridepublic void run(String... args) throws Exception {this.loadRouteConfig();}/***设置应用程序发布事件* @param applicationEventPublisher*/@Overridepublic 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 {@Autowiredprivate GatewayServiceHandler gatewayServiceHandler;@Autowiredprivate 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 "删除路由配置信息失败";}}}




