文章目录
【该项目提供了一个构建在Spring生态系统之上的API网关,包括:Spring 5、Spring Boot 2和 Project Reactor 。Spring Cloud Gateway旨在提供一种简单而有效的方法来路由到api,并为它们提供交叉关注点,例如:安全性、监视/度量和弹性。
使用Spring Cloud Gateway
要在项目中引入Spring Cloud Gateway,需要引用 group id为 org.springframework.cloud
和 artifact id为spring-cloud-starter-gateway
starter。最新的Spring Cloud Release 构建信息,请参阅Spring Cloud Project page。
如果应用了该starter,但由于某种原因不希望启用网关,请进行设置spring.cloud.gateway.enabled=false
。
Spring Cloud Gateway依赖Spring Boot和Spring Webflux提供的Netty runtime。它不能在传统的Servlet容器中工作或构建为WAR
词汇表
- Route 路由:gateway的基本构建模块。它由ID、目标URI、断言集合和过滤器集合组成。如果聚合断言结果为真,则匹配到该路由。
- Predicate 断言:这是一个Java 8 Function Predicate。输入类型是 Spring Framework
ServerWebExchange
。这允许开发人员可以匹配来自HTTP请求的任何内容,例如Header或参数。 - Filter 过滤器:这些是使用特定工厂构建的 Spring Framework
GatewayFilter
实例。所以可以在返回请求之前或之后修改请求和响应的内容。
如何工作
客户端向Spring Cloud Gateway发出请求。如果Gateway Handler Mapping确定请求与路由匹配,则将其发送到Gateway Web Handler。此handler通过特定于该请求的过滤器链处理请求。图中filters被虚线划分的原因是filters可以在发送代理请求之前或之后执行逻辑。先执行所有“pre filter”逻辑,然后进行请求代理。在请求代理执行完后,执行“post filter”逻辑。
编写代码
假设有一个服务的地址如下所示,现在我们需要通过网关转发到这个地址来
http://localhost:8010/app1/echo/2020
依赖
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.2.6.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
<spring-cloud.version>Hoxton.SR4</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
启动类
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
配置文件
server:
port: 9000
spring:
application:
name: cloud-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes:
- id: app1-service # 路由的id, 没有固定规则但要求唯一, 建议配合服务名
uri: http://localhost:8010 # 匹配后提供服务的地址
predicates:
- Path=/app1/** # 断言,路径匹配的进行路由
启动工程,在浏览器输入
http://localhost:9000/app1/echo/2020
这样就可以通过网关将请求转发到
http://localhost:8010/app1/echo/2020
配置路由方式
通过配置文件
spring:
cloud:
gateway:
routes:
- id: app1-service # 路由的id, 没有固定规则但要求唯一, 建议配合服务名
uri: http://localhost:8010 # 匹配后提供服务的地址
predicates:
- Path=/app1/** # 断言,路径匹配的进行路由
JavaBean配置
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route(
"app1-service",
r -> r.path("/app1/**").uri("http://localhost:8010")
);
return routes.build();
}
}
动态路由
Gateway会根据注册中心注册的服务列表,根据注册中心上微服务名路径创建动态路由转发,从而实现动态路由的功能。Gateway可以根据服务名进行负载均衡。
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: app1-service # 路由的id, 没有固定规则但要求唯一, 建议配合服务名
# uri: http://localhost:8101 # 匹配后提供服务的地址
uri: lb://app1-service # 匹配后提供服务的地址,app1-service就是我们要访问的服务名
predicates:
- Path=/app1/** # 断言,路径匹配的进行路由
常用predicates
官方文档:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.2.RELEASE/reference/html/#gateway-request-predicates-factories
After
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: app1-service # 路由的id, 没有固定规则但要求唯一, 建议配合服务名
uri: lb://app1-service # 匹配后提供服务的地址
predicates:
- Path=/app1/** # 断言,路径匹配的进行路由
- After=2020-05-02T15:41:31.892+08:00[Asia/Shanghai] # 在这个时间之后才可以正常访问
这个时间可通过下面代码生成
ZonedDateTime zonedDateTime = ZonedDateTime.now();
System.out.println(zonedDateTime);
Before
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: app1-service # 路由的id, 没有固定规则但要求唯一, 建议配合服务名
uri: lb://app1-service # 匹配后提供服务的地址
predicates:
- Path=/app1/** # 断言,路径匹配的进行路由
- After=2020-05-02T15:41:31.892+08:00[Asia/Shanghai] # 在这个时间之前才可以正常访问
Between
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: app1-service # 路由的id, 没有固定规则但要求唯一, 建议配合服务名
uri: lb://app1-service # 匹配后提供服务的地址
predicates:
- Path=/app1/** # 断言,路径匹配的进行路由
- Between=2020-05-02T15:47:31.892+08:00[Asia/Shanghai],2020-05-02T15:50:31.892+08:00[Asia/Shanghai] # 在这个时间段才可以正常访问
还有Cookie 、 Header 、 Host 、 Method 、 Path 、 Query 、 RemoteAddr ,用法都是类似的
GatewayFilter
官方文档:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.2.RELEASE/reference/html/#gatewayfilter-factories
路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应。SpringCloud Gateway内置了很多路由过滤器,他们都由GatewayFilter工厂类来产生。
以AddRequestHeader
为例
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: app1-service # 路由的id, 没有固定规则但要求唯一, 建议配合服务名
# uri: http://localhost:8101 # 匹配后提供服务的地址
uri: lb://app1-service # 匹配后提供服务的地址
predicates:
- Path=/app1/** # 断言,路径匹配的进行路由
filters:
- AddRequestHeader=X-Request-red, blue
为这个路由的所有请求带上请求头信息,key=X-Request-red,value = blue
自定义过滤器
自定义过滤器需要实现GlobalFilter
接口和Ordered
接口
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String username = request.getQueryParams().getFirst("username");
if (username == null) {
System.out.println("用户名为空,非法操作");
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
// 值越小,优先级越高
return 0;
}
}
返回错误信息
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String username = request.getQueryParams().getFirst("username");
if (username == null) {
System.out.println("用户名为空,非法操作");
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> map = new HashMap<>();
map.put("code","000001");
map.put("msg","用户名为空,非法操作");
try {
String string = objectMapper.writeValueAsString(map);
ServerHttpResponse response = exchange.getResponse();
DataBuffer buffer = response.bufferFactory().wrap(string.getBytes());
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 指定编码,否则在浏览器中会中文乱码
response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
// 值越小,优先级越高
return 0;
}
}
重定向
对于浏览器,通常是发现没有权限后跳转到登录页面。响应状态码需要为HttpStatus.SEE_OTHER(303)。
重定向(redirect)会丢失之前请求的参数,对于需要转发到目标URL的参数,需手工添加。
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String username = request.getQueryParams().getFirst("username");
// 重定向(redirect)到登录页面
if (username == null) {
System.out.println("用户名为空,非法操作");
String url = "http://localhost:9000/login.html";
ServerHttpResponse response = exchange.getResponse();
// 303状态码表示由于请求对应的资源存在着另一个URI,应使用GET方法定向获取请求的资源
response.setStatusCode(HttpStatus.SEE_OTHER);
response.getHeaders().set(HttpHeaders.LOCATION, url);
return response.setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
// 值越小,优先级越高
return 0;
}
}
负载均衡实现原理
Gateway可以作为服务端的负载均衡,那么负载均衡的处理关键就是与Ribbon集成,Gateway是利用GlobalFilter进行实现的,它的实现类是LoadBalancerClientFilter
,代码逻辑比较简单。
public class LoadBalancerClientFilter implements GlobalFilter, Ordered {
protected final LoadBalancerClient loadBalancer;
private LoadBalancerProperties properties;
public LoadBalancerClientFilter(LoadBalancerClient loadBalancer,
LoadBalancerProperties properties) {
this.loadBalancer = loadBalancer;
this.properties = properties;
}
@Override
@SuppressWarnings("Duplicates")
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
if (url == null
|| (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
return chain.filter(exchange);
}
// preserve the original url
addOriginalRequestUrl(exchange, url);
if (log.isTraceEnabled()) {
log.trace("LoadBalancerClientFilter url before: " + url);
}
// 获取服务实例
final ServiceInstance instance = choose(exchange);
if (instance == null) {
throw NotFoundException.create(properties.isUse404(),
"Unable to find instance for " + url.getHost());
}
URI uri = exchange.getRequest().getURI();
// if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
// if the loadbalancer doesn't provide one.
String overrideScheme = instance.isSecure() ? "https" : "http";
if (schemePrefix != null) {
overrideScheme = url.getScheme();
}
// 构造请求URL地址
URI requestUrl = loadBalancer.reconstructURI(
new DelegatingServiceInstance(instance, overrideScheme), uri);
if (log.isTraceEnabled()) {
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
}
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
return chain.filter(exchange);
}
// 获取服务实例
protected ServiceInstance choose(ServerWebExchange exchange) {
return loadBalancer.choose(
((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost());
}
}
自动配置类
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ LoadBalancerClient.class, RibbonAutoConfiguration.class,
DispatcherHandler.class })
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@EnableConfigurationProperties(LoadBalancerProperties.class)
public class GatewayLoadBalancerClientAutoConfiguration {
@Bean
@ConditionalOnBean(LoadBalancerClient.class)
@ConditionalOnMissingBean({ LoadBalancerClientFilter.class,
ReactiveLoadBalancerClientFilter.class })
public LoadBalancerClientFilter loadBalancerClientFilter(LoadBalancerClient client,
LoadBalancerProperties properties) {
return new LoadBalancerClientFilter(client, properties);
}
}