所谓的动态路由,就是可以根据运行时环境(负载情况、头信息、版本号),动态的修改路由规则,从而转发到不同的目标服务上。
动态路由是相对于传统的静态路由而言的,静态路由一旦配置好之后需求有变动,就很难进行灵活的调整。
Spring Cloud Gateway 或 Zuul 都可以实现动态路由,本文以Spring Cloud Gateway 为例。
在写代码前,有必要对Spring Cloud Gateway 里的下面2个类做个了解。
1、org.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository 它是一个路由定义存储仓库,里面有一些增删改查方法,看类名就知道是存在内存里的。它实现了RouteDefinitionRepository接口,实际生产环境中,路由定义集合肯定是要持久化存储的,所以我们的任务就是要改造它。 2、org.springframework.cloud.gateway.route.RouteDefinition 可以把它理解为路由定义的bean,它包含了路由条件和路由行为等,仓库里存的就是它,如果你觉得它不满足你的需求,你也可以继承它,加一些自定义属性。
我们可以选择把路由信息存到mysql里,所以要建张路由信息表,前缀我的昵称夏威夷的首字母
CREATE TABLE `xwy_gateway_route` ( `route_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '路由ID', `route_name` VARCHAR(255) NOT NULL COMMENT '路由名称', `app_name` VARCHAR(255) NULL DEFAULT NULL COMMENT '应用名称,也是路由标识', `route_path` VARCHAR(255) NULL DEFAULT NULL COMMENT '路径', `service_id` VARCHAR(255) NULL DEFAULT NULL COMMENT '服务ID', `strip_prefix` TINYINT(3) NOT NULL DEFAULT '1' COMMENT '忽略前缀', `remark` VARCHAR(255) NULL DEFAULT NULL COMMENT '备注', `create_time` DATETIME NULL DEFAULT NULL COMMENT '创建时间', `update_time` DATETIME NULL DEFAULT NULL COMMENT '修改时间', PRIMARY KEY (`route_id`) USING BTREE ) COMMENT='网关路由表' ENGINE=InnoDB AUTO_INCREMENT=1 ;
然后生成一个对应的路由信息entity,这边就不列代码了,假设类名就叫XwyGatewayRoute。
这边就不引入mybatis了,直接用JdbcTemplate操作。
import java.net.URI; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.bus.event.RemoteApplicationEvent; import org.springframework.cloud.gateway.filter.FilterDefinition; import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; import org.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.event.EventListener; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public class MyRouteDefinitionLocator implements ApplicationEventPublisherAware { private static final Logger log = LoggerFactory.getLogger(MyRouteDefinitionLocator.class); @Autowired private InMemoryRouteDefinitionRepository repository; @Autowired private JdbcTemplate jdbcTemplate; @Autowired private ApplicationEventPublisher applicationEventPublisher; private static final String SELECT_ROUTES = "SELECT * FROM xwy_gateway_route WHERE status = 1 AND is_delete = 1"; public MyRouteDefinitionLocator(JdbcTemplate jdbcTemplate, InMemoryRouteDefinitionRepository repository) { this.jdbcTemplate = jdbcTemplate; this.repository = repository; } public Mono<Void> refresh() { loadRoutes(); return Mono.empty(); } private Mono<Void> loadRoutes() { List<XwyGatewayRoute> routeList = this.jdbcTemplate.query("SELECT * FROM xwy_gateway_route", new RowMapper<XwyGatewayRoute>() { public XwyGatewayRoute mapRow(ResultSet rs, int i) throws SQLException { XwyGatewayRoute result = new XwyGatewayRoute(); result.setRouteName(rs.getString("route_name")); result.setAppName(rs.getString("app_name")); result.setPath(rs.getString("path")); result.setServiceId(rs.getString("service_id")); result.setStripPrefix(Integer.valueOf(rs.getInt("strip_prefix")));return result; } }); if (routeList != null)
log.info("=============准备加载动态路由=============="); routeList.forEach(routeDTO -> {
// RouteDefinitionVo继承自RouteDefinition
RouteDefinitionVo vo = new RouteDefinitionVo();
// PredicateDefinition用于定义路由谓词(Predicate)及其参数,
// PredicateDefinition类提供了以下属性:
// name:路由谓词名称,例如Path、Method、Header等。
// args:一个Map类型的对象,用于存储路由谓词的参数。通常包含一些键值对,例如path、method、header等。
List<PredicateDefinition> predicates = new ArrayList<>(); List<FilterDefinition> filters = new ArrayList<>(); vo.setId(routeDTO.getApplicationName()); vo.setRouteName(routeDTO.getRouteName()); PredicateDefinition predicatePath = new PredicateDefinition(); Map<String, String> predicatePathParams = new HashMap<>(8); predicatePath.setName("Path"); predicatePathParams.put("name", StringUtils.isBlank(routeDTO.getRouteName()); predicatePathParams.put("pattern", routeDTO.getPath()); predicatePath.setArgs(predicatePathParams); predicates.add(predicatePath); URI uri = UriComponentsBuilder.fromUriString("lb://" + routeDTO.getServiceId()).build().toUri(); FilterDefinition stripPrefixDefinition = new FilterDefinition(); Map<String, String> stripPrefixParams = new HashMap<>(8); stripPrefixDefinition.setName("StripPrefix"); stripPrefixParams.put("_genkey_0", routeDTO.getStripPrefix().toString()); stripPrefixDefinition.setArgs(stripPrefixParams); filters.add(stripPrefixDefinition); vo.setPredicates(predicates); vo.setFilters(filters); vo.setUri(uri);
// 从数据库里读取到路由信息集合存到仓库里 this.repository.save(Mono.just(vo)).subscribe(); }); return Mono.empty(); } public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } // 利用 spring cloud stream 的RemoteApplicationEvent监听路由改动事件,重新载入路由信息 @EventListener(classes = {RemoteApplicationEvent.class}) public void refreshRoute(RemoteApplicationEvent event) { deleteRoute(); refresh(); } private void deleteRoute() { Flux<RouteDefinition> flux = this.repository.getRouteDefinitions(); flux.toStream().forEach(f -> this.repository.delete(Mono.just(f.getId())).subscribe()); } }
拼装好的路由信息,格式如下,最后是一个集合存起来,当然,你也可以选择把这些路由信息直接配置到nacos的json文件里,然后直接读取配置也可以。
{ "id": "test-server", "routeName": "测试平台", "uri": "lb://test-server", "order": 7788, "predicates": [ { "name": "Path", "args": { "pattern": "/wx/test/**" } } ], "filters": [ {
// 该过滤器表示删除请求路径中的前缀 "name": "StripPrefix", "args": {
// 表示要从请求路径中删除前两个路径段,例如,/wx/test/demo 对应的转发请求路径为 /demo "_genkey_0": "2" } } ] }
增加配置类,把MyRouteDefinitionLocator托管给spring。
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository; import org.springframework.context.annotation.Bean; import org.springframework.jdbc.core.JdbcTemplate; public class MyRouteAutoConfiguration { private static final Logger log = LoggerFactory.getLogger(MyRouteAutoConfiguration.class); @Autowired private JdbcTemplate jdbcTemplate; @Autowired private InMemoryRouteDefinitionRepository repository; public MyRouteAutoConfiguration(JdbcTemplate jdbcTemplate, InMemoryRouteDefinitionRepository repository) { this.jdbcTemplate = jdbcTemplate; this.repository = repository; } @Bean public MyRouteDefinitionLocator myRouteDefinitionLocator(JdbcTemplate jdbcTemplate, InMemoryRouteDefinitionRepository repository) { MyRouteDefinitionLocator myRouteDefinitionLocator = new MyRouteDefinitionLocator(jdbcTemplate, repository); log.info("MyRouteDefinitionLocator [{}]", myRouteDefinitionLocator); return myRouteDefinitionLocator; } }
加一个自定义注解,方便插拔使用动态路由配置。
import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.context.annotation.Import; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import({MyRouteAutoConfiguration.class}) public @interface EnableDynamicRoute {}
启动类里加上这个注解,并且实现CommandLineRunner接口,org.springframework.boot.CommandLineRunner可以在springboot服务启动完后额外执行一些逻辑代码,例如执行数据脚本、加载配置文件、设置缓存等,重写它的run方法即可。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.cloud.client.SpringCloudApplication; import org.springframework.cloud.openfeign.EnableFeignClients; @EnableDynamicRoute @SpringCloudApplication public class MyGatewayBootstrap implements CommandLineRunner { @Autowired private MyRouteDefinitionLocator myRouteDefinitionLocator; public static void main(String[] args) { SpringApplication.run(MyGatewayBootstrap.class, args); } public void run(String... strings) throws Exception { this.myRouteDefinitionLocator.refresh(); } }