Spring Cloud Gateway 微服务网关
-
创建项目(命名 为 Gateway), 添加pom 依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> <version>2.2.0.RELEASE</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2.2.0.RELEASE</version> </dependency>
-
Router 路由器:包括,11种路由模式,具体参见官网
-
添加路由配置: 意思是所有路径匹配到 /acc/** ,都会路由到 lb://account-service
spring: cloud: gateway: routes: - id: host_route uri: lb://account-service predicates: - Path=/acc/**
-
在启动类添加注解,服务注册和发现
@EnableDiscoveryClient public class GatewayApplication { }
-
测试
- 默认路由地址:http://localhost:9001/account-service/acc/user?id=2
- 使用Path路由配置:http://localhost:9001/acc/user?id=2
-
打开gateway 监控端点 ,各端点介绍,参见Gateway管理API
<!-- 添加 actuator 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
#将 gateway 端点暴露 management.endpoint.gateway.enabled=true management.endpoints.web.exposure.include=gateway
查询全局过滤器, http://localhost:9001/actuator/gateway/globalfilters 查询路由中的过滤器, http://localhost:9001/actuator/gateway/routefilters 刷新路由缓存, http://localhost:9001/actuator/gateway/refresh 展示路由列表, http://localhost:9001/actuator/gateway/routes 获取单个路由的信息, http://localhost:9001/actuator/gateway/routes/host_route 创建路由,发送Post请求, ../actuator/gateway/routes/{id_route_to_create} 删除路由,发送Delete请求: ../actuator/gateway/routes/{id_route_to_delete}
-
-
Filter 过滤器
-
网关经常需要对路由请求进行过滤,如鉴权之后构造头部之类的,过滤的种类很多,如增加请求头、增加请求参数 、增加响应头和断路器等等功能,这就用到了Spring Cloud Gateway 的 Filter。
-
Filter 的作用:
- 当我们有很多个服务时,客户端请求各个服务的Api时,每个服务都需要做相同的事情,比如鉴权、限流、日志输出等。
- 因此可以将这些功能等转移到网关处理以减少重复开发。
-
- PRE: 这种过滤器在被代理的微服务(Proxied Service)执行之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- POST:这种过滤器在被代理的微服务(Proxied Service)执行完成后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
-
Filter 的分类
-
GatewayFilter:应用到单个路由或者一个分组的路由上。自定义的GatewayFilter,同样需要指定某个路由器
-
GlobalFilter:应用到所有的路由上。当请求到来时,
Filtering Web Handler
处理器会添加所有GlobalFilter
实例和匹配的GatewayFilter
实例到过滤器链中。因此,自定义的GlobalFilter,不需要什么额外的配置。 -
自定义 GatewayFilter,有两种实现方式
-
一种是直接 实现GatewayFilter接口,不过还需要手动将自定义的GatewayFilter 注册到 router中
// 统计某个或者某种路由的处理时长 public class CustomerGatewayFilter implements GatewayFilter, Ordered { private static final Logger log = LoggerFactory.getLogger( CustomerGatewayFilter.class ); private static final String COUNT_START_TIME = "countStartTime"; @Override public Mono<Void> filter(ServerWebExchange exch, GatewayFilterChain chain) { return chain.filter(exch).then( Mono.fromRunnable(() -> { long startTime = exch.getAttribute(COUNT_START_TIME); long endTime=(Instant.now().toEpochMilli() - startTime); log.info(exch.getRequest().getURI().getRawPath() + ": " + endTime + "ms"); }) ); } @Override public int getOrder() { return 0; } }
-
另一种是 自定义过滤器工厂(继承AbstractGatewayFilterFactory类) , 选择自定义过滤器工厂的方式,可以在配置文件中配置过滤器
@Component public class CustomerGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomerGatewayFilterFactory.Config> { private static final Logger log = LoggerFactory.getLogger( CustomerGatewayFilterFactory.class ); private static final String COUNT_START_TIME = "countStartTime"; @Override public List<String> shortcutFieldOrder() { return Arrays.asList("enabled"); } public CustomerGatewayFilterFactory() { super(Config.class); log.info("Loaded GatewayFilterFactory [CustomerGatewayFilterFactory]"); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { if (!config.isEnabled()) { return chain.filter(exchange); } exchange.getAttributes().put(COUNT_START_TIME, System.currentTimeMillis()); return chain.filter(exchange).then( Mono.fromRunnable(() -> { Long startTime = exchange.getAttribute(COUNT_START_TIME); if (startTime != null) { StringBuilder sb = new StringBuilder( exchange.getRequest().getURI().getRawPath() ).append(": ") .append(System.currentTimeMillis() - startTime) .append("ms"); sb.append(" params:") .append(exchange.getRequest().getQueryParams()); log.info(sb.toString()); }})); }; } public static class Config { //控制是否开启统计 private boolean enabled; public Config() {} // getter setter 省略 } }
spring: cloud: gateway: routes: - id: elapse_route uri: lb://account-service filters: - Customer=true predicates: - Method=GET
-
-
自定义GlobalFilter : 可以在这里实现授权、日志等功能
@Component public class AuthorizeFilter implements GlobalFilter, Ordered { private static final Logger log = LoggerFactory.getLogger(AuthorizeFilter.class); private static final String AUTHORIZE_TOKEN = "token"; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain){ String token = exchange.getRequest().getQueryParams() .getFirst(AUTHORIZE_TOKEN); if ( StringUtils.isBlank( token )) { log.info( "token is empty ..." ); exchange.getResponse().setStatusCode( HttpStatus.UNAUTHORIZED ); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } @Override public int getOrder() { return 0; } }
-
-
Gateway + Nacos 实现动态路由 , 参见 springcloud-demo
-
路由信息定义在配置文件中,这种方式有一个缺点就是修改路由信息必须重启服务才能生效。网关作为全部流量的入口,可用时间当然越长越好,不重启服务而修改路由是一个更好的选择,结合Nacos可以做到这一点。在Gateway项目中添加 nacos config 依赖,并配置nacos地址
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
-
- Spring Cloud Gateway本身还不支持直接从Nacos动态加载路由配置表,需要自己编写监听器监听配置变化并刷新路由表
-
方法1:(save和delete方法,供使用gateway-endpoint 访问时 添加和删除路由时使用)
@Component public class NacosDynamicRouteService implements ApplicationEventPublisherAware { @Override public void setApplicationEventPublisher(ApplicationEventPublisher aep) { this.applicationEventPublisher = aep; } private ApplicationEventPublisher applicationEventPublisher; private String dataId = "gateway-router"; private String group = "DEFAULT_GROUP"; @Value("${spring.cloud.nacos.config.server-addr}") private String serverAddr; @Autowired private RouteDefinitionWriter routeDefinitionWriter; private static final List<String> ROUTE_LIST = new ArrayList<>(); @PostConstruct public void dynamicRouteByNacosListener() { try { ConfigService configService = NacosFactory .createConfigService(serverAddr); // springboot 启动时,获取配置信息 String configInfo = configService.getConfig(dataId, group, 5000); configRouter(configInfo); // 监听 nacos 上的配置文件,当配置文件被修改后,获取配置信息 configService.addListener(dataId, group, new Listener() { @Override public void receiveConfigInfo(String configInfo) { configRouter(configInfo); } @Override public Executor getExecutor() { return null; } }); } catch (NacosException e) { e.printStackTrace(); } } private void configRouter(String configInfo){ clearRoute(); try { List<RouteDefinition> gatewayRouteDefinitions = JSONObject.parseArray( configInfo, RouteDefinition.class); for (RouteDefinition routeDefinition : gatewayRouteDefinitions) { addRoute(routeDefinition); } publish(); } catch (Exception e) { e.printStackTrace(); } } private void clearRoute() { for(String id : ROUTE_LIST) { this.routeDefinitionWriter.delete(Mono.just(id)).subscribe(); } ROUTE_LIST.clear(); } private void addRoute(RouteDefinition definition) { try { routeDefinitionWriter.save(Mono.just(definition)).subscribe(); ROUTE_LIST.add(definition.getId()); } catch (Exception e) { e.printStackTrace(); } } private void publish() { this.applicationEventPublisher.publishEvent( new RefreshRoutesEvent(this.routeDefinitionWriter)); } }
-
方法2:(仅支持集群环境 各机器同步路由配置,无法根据nacos配置主动更新)
-
自定义RouteDefinitionWriter Spring Cloud Gateway默认的RouteDefinitionWriter实现类是org.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository,Route信息保存在当前实例的内存中,这在集群环境中会存在同步问题。我们可以自定义一个基于Redis的RouteDefinitionWriter。 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.StringRedisTemplate; import org.springframework.stereotype.Component; import com.alibaba.fastjson.JSON; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** * 使用Redis保存自定义路由配置(代替默认的InMemoryRouteDefinitionRepository) * <p/> * 存在问题:每次请求都会调用getRouteDefinitions,当网关较多时,会影响请求速度,考虑放到本地Map中,使用消息通知Map更新。 * * @since 2018年7月9日 下午2:39:02 */ @Component public class RedisRouteDefinitionRepository implements RouteDefinitionRepository { public static final String GATEWAY_ROUTES = "geteway_routes"; @Autowired private StringRedisTemplate redisTemplate; @Override public Flux<RouteDefinition> getRouteDefinitions() { 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("RouteDefinition not found: " + routeId))); }); } }
-
在 Nacos 管理后台,创建配置文件 gateway-router.json ,内容如下。
[{ "id": "account-router", "order": 0, "predicates": [{"args": {"pattern": "/acc/**"},"name": "Path" }], "filters":[{"args": {"pattern": true },"name": "Customer" }], "uri": "lb://account-service" }]
-
同时删除原来在 application.xml 中关于路由的配置
-
测试:http://localhost:9001/acc/user?id=2