Spring Cloud Gateway用法详解
T1 - 简介Spring Cloud Gateway提供了一个基于Spring生态系统的API网关,是依赖于webflux的响应式框架,不能在传统的Servlet容器中工作。网关的常见作用包括路由转发、鉴权、限流、过滤等,以下为Spring Cloud Gateway的工作流程图:主要组件:Route:路由网关的基本构建块,由ID、目标URI、一系列Predicates和Filte...
-
T1 - 简介
Spring Cloud Gateway提供了一个基于Spring生态系统的API网关,是依赖于webflux的响应式框架,不能在传统的Servlet容器中工作。网关的常见作用包括路由转发、鉴权、限流、过滤等,以下为Spring Cloud Gateway的工作流程图:
主要组件:- Route:路由网关的基本构建块,由ID、目标URI、一系列Predicates和Filters组成。路由根据predicate返回是否为true进行批评。
- Predicate:Java8的函数式接口,输入类型是Spring的ServerWebExchange。Predicate可用于匹配HTTP请求的各种内容,如头部和参数。
- Filter:使用特定工厂构建的实例,请求和响应都可以在被发送到下游请求之前或之后被修改
流程:request->RouteLocator->RoutePredicateHandlerMapping(predicate)->FilteringWebHandler(filter)->Service->filter
1.RouteLocatorBuilder.build()生成RouteLocator
2.RoutePredicateHandlerMapping.lookupRoute()通过RouteLocator获取路由映射调用配置的predicate
3.FilteringWebHandler.handle()调用filtergateway的rout、predicate、filter都可通过实例Bean或配置文件进行配置,且都是基于COC(Convention Over Configuration-约定由于配置)的设计方式,在gateway中显示为根据特定命名方式设计、实例化Bean。
-
T2 - Predicate用法
gateway的所有Predicate都由XxxRoutePredicateFactory工厂类进行实例化,在配置文件中只需设置{Xxx=paramA[, paramB…]}即可完成一个Predicate类的实例化。下图为PredicateFactory的类层级图:
predicate与filter都可通过bean或spring配置文件进行实例化,Demo如下:-
java bean配置
@Bean public RouteLocator pathRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route(p -> p .path("/**") .and() .header("token", "\\w+") .uri("lb://user-consumer")) .build(); }
-
application.yml配置
server: port: 8999 spring: application: name: gateway-server cloud: gateway: discovery: locator: enabled: false lower-case-service-id: true routes: - uri: lb://user-consumer predicates: - Header=token, \w+ - Path=/**
以上例子配置了HeaderRoutePredicateFactory与PathRoutePredicateFactory,Header Predicate设置若header中包含符合\w+的token header,请求将路由到user-consumer服务。无论是通过bean还是application.yml配置,都是通过predicate的前缀名进行bean的配置,具体应用为bean route()中调用PredicateSpec.path()实现PathRoutePredicate,调用header方法实现HeaderRoutePredicate。使用配置文件的配置格式为:{Predicate-prefix}=param1[,param2,param3…],参数顺序以XxxRoutePredicateFactory.shortcutFieldOrder()的顺序为准,以HeaderRoutePredicateFactory为例,列表中第一个为header,第二个为regexp,则配置格式为Header={header},{regexp}。HeaderRoutePredicateFactory部分源码:
/** * Header key. */ public static final String HEADER_KEY = "header"; /** * Regexp key. */ public static final String REGEXP_KEY = "regexp"; public HeaderRoutePredicateFactory() { super(Config.class); } @Override public List<String> shortcutFieldOrder() { return Arrays.asList(HEADER_KEY, REGEXP_KEY); }
-
-
T2 - Filter用法
上图为filter的所在包与依赖树,其配置方式与predicate类似,配置Demo如下:-
java bean配置
@Bean public RouteLocator pathRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route(p -> p .path("/**") .and() .header("token", "\\w+") .filters(f -> f.stripPrefix(1) .addResponseHeader("response-header", "head-val")) .uri("lb://user-consumer")) .build(); }
-
application.yml配置
server: port: 8999 spring: application: name: gateway-server cloud: gateway: discovery: locator: enabled: false lower-case-service-id: true routes: - uri: lb://user-consumer predicates: - Header=token, \w+ - Path=/** filters: - StripPrefix=1 - AddResponseHeader=response-header, head-val
以上例子基于predicate的基础上添加了StripPrefixGatewayFilterFactory与AddResponseHeaderGatewayFilterFactory的过滤器,StripPrefixGatewayFilterFactory为截取路径中的前i个路径(如i=2,请求路径为/a/b/hi,则由网关请求到service的路径为/hi),AddResponseHeaderGatewayFilterFactory则对service返回结果进行加工添加header,然后再返回结果。这2个体现了filter可对请求到service前|后进行参数、header、路径、响应的处理,即官方文档中的"pre"与"post"两种filter,对响应的处理(post)Filter工厂类一般名称中包含Response,配置文件的配置方式同predicate。
-
-
T3 - Eureka服务注册发现
当没有配置注册中心时,每次添加新服务都需在网关添加新的服务配置,请求流程为Web->Gateway->Service;当添加注册中心后,网关便可通过注册中心访问中心上的服务,无论服务新增还是减少都无需重新配置网关路由,请求流程转变为Web->Gateway->Registry Center->Service。集成注册中心eureka配置Demo:
eureka: client: service-url: defaultZone: http://localhost:8000/eureka/ logging: level: org.springframework.cloud.gateway: debug spring: profiles: gateway-eureka cloud: gateway: discovery: locator: # 启用DiscoveryClient网关集成,通过网关地址与serviceName访问注册中心的service enabled: true # 服务名转小写 lower-case-service-id: true
配置注册中心后便可通过 http://{ip}.{gatewayPort}/{serviceName}/{api} 访问注册中心上的服务接口,由T2可知gateway可通过Predicate将gateway接收到的请求路由到其它url,但配置注册中心后会发现我们无需配置具体服务路由gateway就可将请求分发到相应的service上,由此我联想到了以下2个问题:
- 那么gateway是怎么分发这些路由的呢?
- 没有配置注册中心时是在routes中配置predicate与filter的,那配置注册中心后该如何配置呢?
-
T3.1 - gateway分发路由规则
以下是gateway的自动化配置类GatewayDiscoveryClientAutoConfiguration.java:
@Configuration @ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true) @AutoConfigureBefore(GatewayAutoConfiguration.class) @AutoConfigureAfter(CompositeDiscoveryClientAutoConfiguration.class) @ConditionalOnClass({ DispatcherHandler.class, DiscoveryClient.class }) @EnableConfigurationProperties public class GatewayDiscoveryClientAutoConfiguration { public static List<PredicateDefinition> initPredicates() { ArrayList<PredicateDefinition> definitions = new ArrayList<>(); // TODO: add a predicate that matches the url at /serviceId? // add a predicate that matches the url at /serviceId/** PredicateDefinition predicate = new PredicateDefinition(); predicate.setName(normalizeRoutePredicateName(PathRoutePredicateFactory.class)); predicate.addArg(PATTERN_KEY, "'/'+serviceId+'/**'"); definitions.add(predicate); return definitions; } public static List<FilterDefinition> initFilters() { ArrayList<FilterDefinition> definitions = new ArrayList<>(); // add a filter that removes /serviceId by default FilterDefinition filter = new FilterDefinition(); filter.setName(normalizeFilterFactoryName(RewritePathGatewayFilterFactory.class)); String regex = "'/' \+ serviceId + '/(?<remaining>.*)'"; String replacement = "'/${remaining}'"; filter.addArg(REGEXP_KEY, regex); filter.addArg(REPLACEMENT_KEY, replacement); definitions.add(filter); return definitions; } @Bean @ConditionalOnBean(DiscoveryClient.class) @ConditionalOnProperty(name = "spring.cloud.gateway.discovery.locator.enabled") public DiscoveryClientRouteDefinitionLocator discoveryClientRouteDefinitionLocator( DiscoveryClient discoveryClient, DiscoveryLocatorProperties properties) { return new DiscoveryClientRouteDefinitionLocator(discoveryClient, properties); } @Bean public DiscoveryLocatorProperties discoveryLocatorProperties() { DiscoveryLocatorProperties properties = new DiscoveryLocatorProperties(); properties.setPredicates(initPredicates()); properties.setFilters(initFilters()); return properties; } }
由以上类可看出gateway默认配置了匹配/serviceId/**的PathRoutePredicate与RewritePathGatewayFilter,spring.cloud.gateway.discovery.locator.enabled为true时会将predicate和filter配置到类DiscoveryClientRouteDefinitionLocator,DiscoveryClientRouteDefinitionLocator会以SpEL解析predicate、filter配置,serviceId会被解析成对应的service,当有服务注册到注册中心时,gateway会检测并将predicate与filter应用到服务,如下图所示,问题1解决。
-
T3.2 - 统一配置服务predicate与filter
配置注册中心后,可用过spring.cloud.gateway.discovery.locator.predicates|filters对注册中心中的服务进行predicate与filter的统一配置,但会覆盖默认的predicate与filter配置,此时可添加PathRoutePredicate与RewritePathGatewayFilter默认配置保持默认功能,配置Demo如下图所示:
spring: profiles: gateway-eureka cloud: gateway: discovery: locator: # 启用DiscoveryClient网关集成,可通过网关地址与serviceName访问注册中心的service enabled: true # 服务名转小写 lower-case-service-id: true predicates: - name: Path args: patterns: "'/'+serviceId+'/**'" - name: Header args: header: "'token'" regexp: "'\\w+'" filters: - name: RewritePath args: replacement: "'/${remaining}'" regexp: "'/' + serviceId + '/(?<remaining>.*)'" - name: AddResponseHeader args: name: "'response-test-head'" value: "'response-test'"
-
T4 - 自定义全局Filter
当配置注册中心后,可能就会出现各服务统一Token鉴权、统一日志记录、超时设置等需求,这些可以统一在gateway进行处理。gateway全局过滤器都需实现GlobalFilter,一般还需实现Ordered接口控制Bean实例化顺序,以一个简单token全局过滤器为例:
@Slf4j @Component @ConditionalOnProperty(value = "spring.cloud.gateway.discovery.locator.global-token-filter-enabled", havingValue = "true") public class GlobalTokenFilter implements GlobalFilter, Ordered { @PostConstruct public void init() { log.info("GlobalTokenFilter init finish"); } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { Map<String, String> headers = exchange.getRequest().getHeaders().toSingleValueMap(); boolean hasToken = headers.containsKey("token") && StringUtils.isNotBlank(headers.get("token")); if (!hasToken) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } log.info("token value:" + headers.get("token")); return chain.filter(exchange); } @Override public int getOrder() { return -100; } }
请求的各种信息都可通过ServerWebExchange获取,当有请求到达gateway时,GlobalTokenFilter检测是否含token的头部,没有的话gateway将返回404。我们也可以通过继承AbstractGatewayFilterFactory来自定义过滤器,如简单的请求记录过滤器:
@Slf4j @Component public class RequestRecordGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestRecordGatewayFilterFactory.Config> { public static final String PARTS_KEY = "print"; public RequestRecordGatewayFilterFactory() { super(Config.class); } @Override public List<String> shortcutFieldOrder() { return Arrays.asList(PARTS_KEY); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { if (config.print != null && config.print) { // 打印相对路径 log.info("requestRecord1:{},参数:{}", exchange.getRequest().getPath().value(), exchange.getRequest().getQueryParams().toSingleValueMap()); // 打印绝对路径 log.info("requestRecord1:{},参数:{}", exchange.getAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR), exchange.getRequest().getQueryParams().toSingleValueMap()); } return chain.filter(exchange); }; } @Override public GatewayFilter apply(Consumer<Config> consumer) { Config config = newConfig(); consumer.accept(config); return apply(config); } @ToString public static class Config { private Boolean print; public boolean isPrint() { return print; } public void setPrint(boolean print) { this.print = print; } } }
同时application.yml添加RequestRecordFilter配置:
spring: profiles: gateway-eureka cloud: gateway: discovery: locator: # 启用DiscoveryClient网关集成,可通过网关地址与serviceName访问注册中心的service enabled: true # 服务名转小写 lower-case-service-id: true predicates: - name: Path args: patterns: "'/'+serviceId+'/**'" - name: Header args: header: "'token'" regexp: "'\\w+'" filters: - name: RewritePath args: replacement: "'/${remaining}'" regexp: "'/' + serviceId + '/(?<remaining>.*)'" - name: AddResponseHeader args: name: "'response-test-head'" value: "'response-test'" - name: RequestRecord args: print: true
请求的参数日志也可通过一个GlobalFilter去实现,区别是GlobalFilter的ServerWebExchange尚未把原始请求的url设置到属性中(Attrubute),所以打印出的绝对路径为null,且继承AbstractGatewayFilterFactory获取的相对路径是截掉了serviceId的,因为在其它过滤器操作执行前路径已被RewritePath过滤器进行了路径处理,具体效果图如下:
若要自定义Predicate只需继承AbstractRoutePredicateFactory类根据业务需求重写相应方法,具体可参考AbstractRoutePredicateFactory的子类,与自定义Fitler大同小异,这里便不唠叨了。
-
T5 - 总结
该文章主要讲gateway的主要用法并根据个人的思考扩展了一些gateway的用例,大部分知识点都是基于官方文档进行总结,且由以上功能Demo可看出gateway的处理流程并不复杂,由Web->Gateway(Predicate->GlobalFilter->Pre Filter)->Service->Gateway(Post Gateway)->Web
更多推荐
所有评论(0)