流程:request->RouteLocator->RoutePredicateHandlerMapping(predicate)->FilteringWebHandler(filter)->Service->filter
1.RouteLocatorBuilder.build()生成RouteLocator
2.RoutePredicateHandlerMapping.lookupRoute()通过RouteLocator获取路由映射调用配置的predicate
3.FilteringWebHandler.handle()调用filter
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 shortcutFieldOrder() {
return Arrays.asList(HEADER_KEY, REGEXP_KEY);
}
上图为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。
当没有配置注册中心时,每次添加新服务都需在网关添加新的服务配置,请求流程为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的自动化配置类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 initPredicates() {
ArrayList 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 initFilters() {
ArrayList definitions = new ArrayList<>();
// add a filter that removes /serviceId by default
FilterDefinition filter = new FilterDefinition();
filter.setName(normalizeFilterFactoryName(RewritePathGatewayFilterFactory.class));
String regex = "'/' \+ serviceId + '/(?.*)'";
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解决。
配置注册中心后,可用过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 + '/(?.*)'"
- name: AddResponseHeader
args:
name: "'response-test-head'"
value: "'response-test'"
当配置注册中心后,可能就会出现各服务统一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 filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Map 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 {
public static final String PARTS_KEY = "print";
public RequestRecordGatewayFilterFactory() {
super(Config.class);
}
@Override
public List 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 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 + '/(?.*)'"
- 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大同小异,这里便不唠叨了。
该文章主要讲gateway的主要用法并根据个人的思考扩展了一些gateway的用例,大部分知识点都是基于官方文档进行总结,且由以上功能Demo可看出gateway的处理流程并不复杂,由Web->Gateway(Predicate->GlobalFilter->Pre Filter)->Service->Gateway(Post Gateway)->Web