环境信息
Spring Boot:2.0.8.RELEASE
Spring Boot内置的tomcat:tomcat-embed-core 8.5.37
Spring Cloud Gateway:2.0.4.RELEASE
Nacos:2.0.4.RELEASE
需求
Spring Cloud Gateway注册到注册中心(这里是Nacos,其它注册中心也一样,如Eureka,Consul,Zookeeper),自动从注册中心里获取注册上去的服务,并使用负载均衡策略将对应的请求路由到对应的服务上。
目前注册到注册中心的服务,服务名和服务的根路径是一样的,比如服务名service-a,根路径是/service-a。
访问路径规则:http://网关ip:端口/目标服务名/目标服务具体路径
自动重定向到:http://目标服务ip:端口/目标服务名/目标服务具体路径
如:
访问网关的路径:http://localhost:8000/service-a/logSetting/getLogLevel
直接访问目标服务service-a的路径:http://localhost:6001/service-a/logSetting/getLogLevel
分析
Spring Cloud Gateway官方提供了从注册中心自动获取服务并注册路由信息的功能,前提是gateway网关工程注册到了注册中心(如Eureka、Consul、Nacos、Zookeeper等)
官方文档:Spring Cloud Gateway
这种自动注册的路由信息:
断言predicates是Path,规则是"'/'+serviceId+'/**'",即匹配服务名
过滤器filters是RewritePath,规则是:
regexp: "'/' + serviceId + '/(?<remaining>.*)'"
replacement: "'/${remaining}'"
比如:
访问网关的路径:http://localhost:8000/service-a/logSetting/getLogLevel
会重定向到:http://localhost:6001/logSetting/getLogLevel
和需求相比,目标路径少了/service-a,会导致无法访问到目标服务,报错404
因此,需要设置自定义规则用来代替默认的规则。
以下是官方文档里的说明、示例
By default, the gateway defines a single predicate and filter for routes created with a DiscoveryClient.
The default predicate is a path predicate defined with the pattern /serviceId/**, where serviceId is the ID of the service from the DiscoveryClient.
The default filter is a rewrite path filter with the regex /serviceId/(?<remaining>.*) and the replacement /${remaining}. This strips the service ID from the path before the request is sent downstream.
If you want to customize the predicates or filters used by the DiscoveryClient routes, set spring.cloud.gateway.discovery.locator.predicates[x] and spring.cloud.gateway.discovery.locator.filters[y]. When doing so, you need to make sure to include the default predicate and filter shown earlier, if you want to retain that functionality. The following example shows what this looks like:
Example 71. application.properties
spring.cloud.gateway.discovery.locator.predicates[0].name: Path
spring.cloud.gateway.discovery.locator.predicates[0].args[pattern]: "'/'+serviceId+'/**'"
spring.cloud.gateway.discovery.locator.predicates[1].name: Host
spring.cloud.gateway.discovery.locator.predicates[1].args[pattern]: "'**.foo.com'"
spring.cloud.gateway.discovery.locator.filters[0].name: Hystrix
spring.cloud.gateway.discovery.locator.filters[0].args[name]: serviceId
spring.cloud.gateway.discovery.locator.filters[1].name: RewritePath
spring.cloud.gateway.discovery.locator.filters[1].args[regexp]: "'/' + serviceId + '/(?<remaining>.*)'"
spring.cloud.gateway.discovery.locator.filters[1].args[replacement]: "'/${remaining}'"
但是要注意:
官网上的配置的格式有问题,不能直接使用。
使用了自定义配置之后,默认的配置就不会生效了。如果需要和默认配置一样的功能,那么需要手动配置。
如果是applicaton.properties格式的配置文件,那么和默认配置一样的功能的话,应该这样子配置:注意args[pattern]等的值,不能有双引号(")!这些值是SPEL表达式。
# 断言配置
spring.cloud.gateway.discovery.locator.predicates[0].name: Path
spring.cloud.gateway.discovery.locator.predicates[0].args[pattern]: '/api/'+serviceId+'/**'
# 过滤器设置
spring.cloud.gateway.discovery.locator.filters[0].name: RewritePath
spring.cloud.gateway.discovery.locator.filters[0].args[regexp]: '/' + serviceId + '/(?<remaining>.*)'
spring.cloud.gateway.discovery.locator.filters[0].args[replacement]: '/${remaining}'
如果是application.yml格式的配置文件的话:
注意args的pattern、regex等的值,要有双引号(")!这些值是SPEL表达式。
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
lowerCaseServiceId: true
predicates:
- name: Path
args:
pattern: "'/'+serviceId+'/**'"
filters:
- name: RewritePath
args:
regexp: "'/' + serviceId + '/?(?<remaining>.*)'"
replacement: "'/' + '/${remaining}'"
实现
官方文档:Spring Cloud Gateway
开启自动注册功能
在application.properties里配置属性:
# 开启自动注册
spring.cloud.gateway.discovery.locator.enabled=true
# serviceId使用小写
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true
配置自定义规则。
application.properties版:主要是配置了路径重写过滤器,不将serviceId过滤掉。注意不要有双引号
spring.cloud.gateway.discovery.locator.filters[0].name: RewritePath
spring.cloud.gateway.discovery.locator.filters[0].args[regexp]: '/' + serviceId + '/(?<remaining>.*)'
spring.cloud.gateway.discovery.locator.filters[0].args[replacement]: '/' + serviceId + '/${remaining}
其它常见需求
手动注册路由信息
如果需要手动注册路由信息,而且访问路径还是和自动注册的一样(即:http://网关ip:端口/目标服务名/目标服务具体路径),那么需要设置路由的优先级order比0小,因为自动注册的路由信息优先级order是0,order值越小,优先级越高。如果没配置order,默认是0,但是手动注册的路由信息优先级会低于自动注册的,具体原因后面说。
spring.cloud.gateway.routes[0].id=service-a
spring.cloud.gateway.routes[0].uri=http://localhost:6001/service-a,http://localhost:16001/service-a
spring.cloud.gateway.routes[0].predicates[0]=Path=/service-a/**
spring.cloud.gateway.routes[0].order=-1
配置自动注册的服务
如果需要从注册中心里自动注册路由,但是想要排除一些不需要暴露给网关的服务,那么可以使用一下方式来排除:
spring.cloud.gateway.discovery.locator.includeExpression=!serviceId.contains('service-b')
扩展:includeExpression的值是SPEL表达式,serviceId是注册中心里的服务id。
表达式可以用contains、eq等等,来实现不同的筛选。
这个属性对应的是DiscoveryLocatorProperties类
@ConfigurationProperties("spring.cloud.gateway.discovery.locator")
public class DiscoveryLocatorProperties {
private boolean enabled = false;
private String routeIdPrefix;
private String includeExpression = "true";
private String urlExpression = "'lb://'+serviceId";
private boolean lowerCaseServiceId = false;
private List<PredicateDefinition> predicates = new ArrayList();
private List<FilterDefinition> filters = new ArrayList();
// 具体代码略...
}
配置全局的过滤器
spring:
cloud:
gateway:
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials Vary, RETAIN_UNIQUE
配置全局的cors
spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
cors-configurations:
'[/**]':
allowedOriginPatterns: "*"
allowedMethods: "*"
allowedHeaders: "*"
allowedOrigin: "*"
allowCredentials: true
maxAge: 360000
完整版的application.properties
# 开启自动注册
spring.cloud.gateway.discovery.locator.enabled=true
# serviceId使用小写
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true
# 过滤器设置
spring.cloud.gateway.discovery.locator.filters[0].name: RewritePath
spring.cloud.gateway.discovery.locator.filters[0].args[regexp]: '/' + serviceId + '/(?<remaining>.*)'
spring.cloud.gateway.discovery.locator.filters[0].args[replacement]: '/' + serviceId + '/${remaining}'
# 自定义路由信息
spring.cloud.gateway.routes[0].id=service-a
#spring.cloud.gateway.routes[0].uri=lb://service-a
spring.cloud.gateway.routes[0].uri=http://localhost:6001/service-a,http://localhost:16001/service-a
spring.cloud.gateway.routes[0].predicates[0]=Path=/service-a/**
spring.cloud.gateway.routes[0].order=-1
spring.cloud.gateway.default-filters[0]=DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials Vary, RETAIN_UNIQUE
application.yml
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
lowerCaseServiceId: true
predicates:
- name: Path
args:
pattern: "'/'+serviceId+'/**'"
filters:
- name: RewritePath
args:
regexp: "'/' + serviceId + '/?(?<remaining>.*)'"
replacement: "'/' + serviceId + '/${remaining}'"
routes:
- id: service-a
# uri: lb://service-a
uri: http://localhost:6001/service-a,http://localhost:16001/service-a
predicates:
- Path=/tfb-biz-common-service-app/**
order: -1
源码解析
配置相关类
GatewayDiscoveryClientAutoConfiguration
主要是根据属性spring.cloud.gateway.discovery.locator.enabled=true开启自动注册,并初始化自动注册的默认断言、过滤器
DiscoveryLocatorProperties
@ConfigurationProperties("spring.cloud.gateway.discovery.locator")
public class DiscoveryLocatorProperties {
private boolean enabled = false;
private String routeIdPrefix;
private String includeExpression = "true";
private String urlExpression = "'lb://'+serviceId";
private boolean lowerCaseServiceId = false;
private List<PredicateDefinition> predicates = new ArrayList();
private List<FilterDefinition> filters = new ArrayList();
public DiscoveryLocatorProperties() {
}// 59
GatewayDiscoveryClientAutoConfiguration类里的discoveryLocatorProperties()初始化了默认配置
@Bean
public DiscoveryLocatorProperties discoveryLocatorProperties() {
DiscoveryLocatorProperties properties = new DiscoveryLocatorProperties();// 66
properties.setPredicates(initPredicates());// 67
properties.setFilters(initFilters());// 68
return properties;// 69
}
DiscoveryLocatorProperties初始化的时候,又因为@ConfigurationProperties("spring.cloud.gateway.discovery.locator")会加载配置文件里的spring.cloud.gateway.discovery.locator对应的属性,从而覆盖掉默认配置。
上面常见需求里说的用includeExpression来设置需要注册或不注册的服务,就是在这个类里的属性。
RouteDefinitionRouteLocator
RouteDefinitionLocator
路由定义定位器接口,有多个实现类:
CachingRouteDefinitionLocator
CompositeRouteDefinitionLocator
DiscoveryClientRouteDefinitionLocator
InMemoryRouteDefinitionRepository
PropertiesRouteDefinitionLocator
RouteDefinitionRepository
DiscoveryClientRouteDefinitionLocator
自动注册的路由信息,具体是在这里设置的。
网关请求的流程:
路由入口:org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping#lookupRoute
RoutePredicateHandlerMapping
接着访问org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getRoutes
这里的routeDefinitionLocator是CompositeRouteDefinitionLocator对象:
是一个代理对象,包含了一个List:
iterable = {ArrayList@9085} size = 3
0 = {DiscoveryClientRouteDefinitionLocator@9087}
1 = {PropertiesRouteDefinitionLocator@9088}
2 = {InMemoryRouteDefinitionRepository@9089}
由此也可以看出先使用自动注册的路由信息,再使用配置文件里配置的路由信息。
这三个路由定义定位器,都是实现了RouteDefinitionLocator接口
DiscoveryClientRouteDefinitionLocator这个是从注册中心里主动注册的路由定义定位器
里面有个属性是DiscoveryLocatorProperties,在getRouteDefinitions()里有对于该属性的详细使用方式,比如includeExpression属性、lowerCaseServiceId属性等
PropertiesRouteDefinitionLocator是基于属性的路由定义定位器
public class PropertiesRouteDefinitionLocator implements RouteDefinitionLocator {
private final GatewayProperties properties;
public PropertiesRouteDefinitionLocator(GatewayProperties properties) {
this.properties = properties;// 33
}// 34
public Flux<RouteDefinition> getRouteDefinitions() {
return Flux.fromIterable(this.properties.getRoutes());// 38
}
}
属性GatewayProperties:
@ConfigurationProperties("spring.cloud.gateway")
@Validated
public class GatewayProperties {
private final Log logger = LogFactory.getLog(this.getClass());
@NotNull
@Valid
private List<RouteDefinition> routes = new ArrayList();
private List<FilterDefinition> defaultFilters = new ArrayList();
private List<MediaType> streamingMediaTypes;
public GatewayProperties() {
this.streamingMediaTypes = Arrays.asList(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_STREAM_JSON);// 55
}
// 省略
}
RouteDefinition是路由定义信息,不管是注册中心自动配置的,还是通过属性文件配置的,或者通过代码方式配置的
@Validated
public class RouteDefinition {
@NotEmpty
private String id = UUID.randomUUID().toString();
@NotEmpty
@Valid
private List<PredicateDefinition> predicates = new ArrayList();
@Valid
private List<FilterDefinition> filters = new ArrayList();
@NotNull
private URI uri;
private int order = 0;
// 省略
}
回到入口RoutePredicateHandlerMapping#lookupRoute里的具体代码:
获取到路由信息之后,会调用路由信息的断言predicate的apply(T t)方法,来判断该请求是否满足断言。如果满足,那么就会使用该路由信息来进行路由转发。
比如使用路径断言(predicate的name是Path),对应的是PathRoutePredicateFactory:
RouteDefinitionRouteLocator#getRoutes这里还调用了convertToRoute(),里面调用了getFilters,这个是过滤器
public Flux<Route> getRoutes() {
return this.routeDefinitionLocator.getRouteDefinitions().map(this::convertToRoute).map((route) -> {// 109 110 112
if (this.logger.isDebugEnabled()) {// 113
this.logger.debug("RouteDefinition matched: " + route.getId());// 114
}
return route;// 116
});
}
private Route convertToRoute(RouteDefinition routeDefinition) {
AsyncPredicate<ServerWebExchange> predicate = this.combinePredicates(routeDefinition);// 127
List<GatewayFilter> gatewayFilters = this.getFilters(routeDefinition);// 128
return ((AsyncBuilder)Route.async(routeDefinition).asyncPredicate(predicate).replaceFilters(gatewayFilters)).build();// 130 131 132 133
}
后续的处理会调用到各个过滤器
RouteDefinitionRouteLocator.loadGatewayFilters(),重点在于调用了GatewayFilter gatewayFilter = factory.apply(configuration);
private List<GatewayFilter> loadGatewayFilters(String id, List<FilterDefinition> filterDefinitions) {
List<GatewayFilter> filters = (List)filterDefinitions.stream().map((definition) -> {// 138 139
GatewayFilterFactory factory = (GatewayFilterFactory)this.gatewayFilterFactories.get(definition.getName());// 140
if (factory == null) {// 141
throw new IllegalArgumentException("Unable to find GatewayFilterFactory with name " + definition.getName());// 142
} else {
Map<String, String> args = definition.getArgs();// 144
if (this.logger.isDebugEnabled()) {// 145
this.logger.debug("RouteDefinition " + id + " applying filter " + args + " to " + definition.getName());// 146
}
Map<String, Object> properties = factory.shortcutType().normalize(args, factory, this.parser, this.beanFactory);// 149
Object configuration = factory.newConfig();// 151
ConfigurationUtils.bind(configuration, properties, factory.shortcutFieldPrefix(), definition.getName(), this.validator);// 153 154
GatewayFilter gatewayFilter = factory.apply(configuration);// 156
if (this.publisher != null) {// 157
this.publisher.publishEvent(new FilterArgsEvent(this, id, properties));// 158
}
return gatewayFilter;// 160
}
}).collect(Collectors.toList());// 162
ArrayList<GatewayFilter> ordered = new ArrayList(filters.size());// 164
for(int i = 0; i < filters.size(); ++i) {// 165
GatewayFilter gatewayFilter = (GatewayFilter)filters.get(i);// 166
if (gatewayFilter instanceof Ordered) {// 167
ordered.add(gatewayFilter);// 168
} else {
ordered.add(new OrderedGatewayFilter(gatewayFilter, i + 1));// 171
}
}
return ordered;// 175
}
比如说常见的路径重写过滤器RewritePathGatewayFilterFactory。
可以看到这里有两个参数regexp和replacement
public class RewritePathGatewayFilterFactory extends AbstractGatewayFilterFactory<RewritePathGatewayFilterFactory.Config> {
public static final String REGEXP_KEY = "regexp";
public static final String REPLACEMENT_KEY = "replacement";
public RewritePathGatewayFilterFactory() {
super(RewritePathGatewayFilterFactory.Config.class);// 38
}// 39
public List<String> shortcutFieldOrder() {
return Arrays.asList("regexp", "replacement");// 43
}
public GatewayFilter apply(RewritePathGatewayFilterFactory.Config config) {
String replacement = config.replacement.replace("$\\", "$");// 48
return (exchange, chain) -> {// 49
ServerHttpRequest req = exchange.getRequest();// 50
ServerWebExchangeUtils.addOriginalRequestUrl(exchange, req.getURI());// 51
String path = req.getURI().getRawPath();// 52
String newPath = path.replaceAll(config.regexp, replacement);// 53
ServerHttpRequest request = req.mutate().path(newPath).build();// 55 56 57
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, request.getURI());// 59
return chain.filter(exchange.mutate().request(request).build());// 61
};
}
// 省略
}