在微服务系统中,网关通常是外部流量进入系统的统一入口。
客户端请求不会直接访问每个后端服务,而是先进入网关,再由网关根据路由规则转发到对应的服务。
网关通常会承担这些职责:
请求路由;
服务转发;
负载均衡;
权限校验;
请求过滤;
统一日志;
限流熔断;
请求头、响应头处理。
在 Spring Cloud 体系中,常用的网关组件是 Spring Cloud Gateway。
本文记录 Spring Cloud Gateway 的基础使用方式,包括:
Gateway 基本依赖;
代码方式配置路由;
配置文件方式配置路由;
路由断言;
过滤器工厂;
自定义局部过滤器;
自定义全局过滤器。
一、Gateway 简介
早期 Spring Cloud 项目中,经常使用 Netflix Zuul 作为网关。
后来 Spring Cloud 推出了自己的网关组件:Spring Cloud Gateway。
Spring Cloud Gateway 和传统 Servlet 组件不太一样,它基于:
Spring Boot
Spring WebFlux
Reactor Netty
也就是说,它采用的是响应式编程模型。
所以在使用 Gateway 时,需要注意几个点。
1. 不要同时引入 spring-boot-starter-web
Gateway 基于 Spring WebFlux。
如果项目中同时引入:
<artifactId>spring-boot-starter-web</artifactId>
可能会和 WebFlux 产生冲突。
Gateway 项目中一般只保留 Gateway 相关依赖,不再引入传统 Spring MVC Web 依赖。
2. Gateway 默认使用 Netty
Gateway 默认运行在 Netty 容器上。
如果项目中引入 Tomcat、Jetty 等 Servlet 容器相关依赖,运行时可能出现异常。
3. 建议使用 jar 包方式启动
如果新建项目时选择了 war 模式,需要注意删除 IDE 自动生成的 ServletInitializer.java,并将打包方式改成 jar。
Gateway 更适合作为独立网关服务运行。
二、引入依赖
新建一个 Gateway 模块,引入 Gateway 依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
如果需要通过注册中心进行服务发现,可以再引入 Eureka Client。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
实际项目中,如果使用的是 Nacos、Consul 等注册中心,对应依赖需要替换成对应的服务发现组件。
三、通过代码配置路由
Spring Cloud Gateway 支持通过 Java 代码配置路由。
示例:
package com.scd.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
/**
* 创建路由规则
*/
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("customer", r -> r.path("/customer-api/**")
.filters(f -> f.stripPrefix(1))
.uri("http://localhost:3001"))
.route("goods", r -> r.path("/goods-api/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://goods"))
.build();
}
}
这里定义了两个路由。
第一个路由:
.route("customer", r -> r.path("/customer-api/**")
.filters(f -> f.stripPrefix(1))
.uri("http://localhost:3001"))
含义是:
路由 ID 为
customer;匹配路径
/customer-api/**;转发前去掉路径中的第一层;
最终转发到
http://localhost:3001。
例如请求:
http://localhost:6001/customer-api/customer/name/1
经过:
stripPrefix(1)
处理后,请求路径会变成:
/customer/name/1
最终转发到:
http://localhost:3001/customer/name/1
第二个路由:
.route("goods", r -> r.path("/goods-api/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://goods"))
这里的 URI 使用的是:
lb://goods
这是基于服务发现的路由方式。
其中:
goods
表示注册中心中的服务名。
Gateway 会通过注册中心找到 goods 服务的可用实例,并进行负载均衡转发。
四、通过配置文件配置路由
除了代码方式,也可以通过 application.yml 配置路由。
示例:
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: customer
uri: http://localhost:3001
predicates:
- Path=/customer-api/**
filters:
- StripPrefix=1
- id: goods
uri: lb://goods
predicates:
- Path=/goods-api/**
filters:
- StripPrefix=1
eureka:
client:
serviceUrl:
defaultZone: http://localhost:1001/eureka,http://localhost:1002/eureka
server:
port: 6001
logging:
level:
root: info
这段配置和前面的 Java 代码配置基本等价。
其中:
predicates:
- Path=/customer-api/**
表示路径断言。
filters:
- StripPrefix=1
表示过滤器配置,作用是去掉请求路径中的第一层。
uri: lb://goods
表示通过服务发现转发到 goods 服务。
五、Gateway 中的几个核心概念
Spring Cloud Gateway 中有几个核心概念。
1. Route
Route 表示一条路由规则。
一条路由通常由下面几部分组成:
路由 ID;
目标 URI;
断言集合;
过滤器集合。
只有当断言匹配成功后,请求才会进入对应路由。
2. Predicate
Predicate 表示路由断言。
它用于判断当前请求是否匹配某条路由。
例如:
- Path=/customer-api/**
就是一个路径断言。
只有请求路径匹配 /customer-api/** 时,当前路由才会生效。
3. Filter
Filter 表示过滤器。
过滤器可以在请求转发前后执行一些自定义逻辑。
常见用途包括:
修改请求路径;
添加请求头;
添加响应头;
权限校验;
日志记录;
参数处理;
灰度标识传递。
Gateway 中的过滤器分为两类:
局部过滤器;
全局过滤器。
局部过滤器只对指定路由生效。
全局过滤器对所有路由生效。
六、路由断言工厂
Gateway 的断言由路由断言工厂生成。
相关接口是:
RoutePredicateFactory<C>
它的核心方法是:
Predicate<ServerWebExchange> apply(C config);
Gateway 内置了很多断言工厂。
这些类的命名通常是:
XXXRoutePredicateFactory
例如:
PathRoutePredicateFactory
QueryRoutePredicateFactory
配置文件中的:
- Path=/customer-api/**
对应的就是 PathRoutePredicateFactory。
七、使用 Query 断言
除了路径断言,也可以使用请求参数断言。
例如要求请求中必须存在参数 id。
代码方式:
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("customer", r -> r.path("/customer-api/**")
.and().query("id")
.filters(f -> f.stripPrefix(1))
.uri("http://localhost:3001"))
.build();
}
如果要求参数 id 必须是数字,可以继续添加正则匹配:
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("customer", r -> r.path("/customer-api/**")
.and().query("id")
.and().query("id", "^[0-9]*$")
.filters(f -> f.stripPrefix(1))
.uri("http://localhost:3001"))
.build();
}
配置文件方式:
spring:
cloud:
gateway:
routes:
- id: customer
uri: http://localhost:3001
predicates:
- Path=/customer-api/**
- Query=id
- Query=id,^[0-9]*$
filters:
- StripPrefix=1这段配置表示:
请求路径必须匹配
/customer-api/**;请求参数中必须包含
id;id参数必须匹配数字格式。
例如可以匹配:
/customer-api/customer/name/1?id=100但不能匹配:
/customer-api/customer/name/1也不能匹配:
/customer-api/customer/name/1?id=abc八、过滤器工厂
Gateway 也内置了很多过滤器工厂。
相关接口是:
GatewayFilterFactory<C>
它的核心方法是:
GatewayFilter apply(C config);
过滤器工厂的命名规则通常是:
XXXGatewayFilterFactory
例如:
AddResponseHeaderGatewayFilterFactory
StripPrefixGatewayFilterFactory
前面使用的:
- StripPrefix=1对应的就是 StripPrefixGatewayFilterFactory。
如果想给响应添加一个响应头,可以使用 AddResponseHeader。
代码方式:
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("goods", r -> r.path("/goods-api/**")
.filters(f -> f.stripPrefix(1)
.addResponseHeader("response-header", "response-value"))
.uri("lb://goods"))
.build();
}配置文件方式:
spring:
cloud:
gateway:
routes:
- id: goods
uri: lb://goods
predicates:
- Path=/goods-api/**
filters:
- StripPrefix=1
- AddResponseHeader=response-header,response-value
这样 Gateway 在响应客户端时,会添加响应头:
response-header: response-value九、自定义局部过滤器
局部过滤器只对某个路由生效。
可以通过实现 GatewayFilter 的方式定义局部过滤器。
示例:
private GatewayFilter myGatewayFilter() {
return (exchange, chain) -> {
System.out.println("我是局部过滤器逻辑");
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest newRequest = request.mutate()
.header("request-header", "my-request-header")
.build();
String id = request.getQueryParams().getFirst("id");
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
response.getHeaders().add("response-header", "my-response-header");
return chain.filter(exchange.mutate().request(newRequest).build());
};
}然后在指定路由中使用:
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("goods", r -> r.path("/goods-api/**")
.filters(f -> f.stripPrefix(1)
.filter(myGatewayFilter()))
.uri("lb://goods"))
.build();
}这里需要注意一个点。
不能直接这样修改请求头:
request.getHeaders().add("header", "myheader");
因为:
request.getHeaders()
返回的是只读对象。
正确做法是使用:
request.mutate()
构建一个新的请求对象。
例如:
ServerHttpRequest newRequest = request.mutate()
.header("request-header", "my-request-header")
.build();
然后通过:
exchange.mutate().request(newRequest).build()
把新的 request 放回 exchange。
十、自定义全局过滤器
全局过滤器对所有路由生效。
只需要定义一个 GlobalFilter Bean。
@Bean
public GlobalFilter globalFilter() {
return (exchange, chain) -> {
System.out.println("我是全局过滤器");
return chain.filter(exchange);
};
}
因为加了:
@Bean
Spring 会把这个 GlobalFilter 注册到 IoC 容器中。
Gateway 会自动识别它,并加入过滤器链。
全局过滤器适合处理所有请求都需要经过的逻辑,例如:
统一日志;
traceId 生成;
鉴权;
黑白名单;
统一请求头处理;
全局异常信息记录。
十一、完整示例
下面是一个整合了路由、局部过滤器和全局过滤器的示例。
package com.scd.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
/**
* 创建路由规则
*/
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("customer", r -> r.path("/customer-api/**")
.filters(f -> f.stripPrefix(1))
.uri("http://localhost:3001"))
.route("goods", r -> r.path("/goods-api/**")
.filters(f -> f.stripPrefix(1)
.filter(myGatewayFilter()))
.uri("lb://goods"))
.build();
}
/**
* 自定义局部过滤器
*/
private GatewayFilter myGatewayFilter() {
return (exchange, chain) -> {
System.out.println("我是局部过滤器逻辑");
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest newRequest = request.mutate()
.header("request-header", "my-request-header")
.build();
String id = request.getQueryParams().getFirst("id");
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
response.getHeaders().add("response-header", "my-response-header");
return chain.filter(exchange.mutate().request(newRequest).build());
};
}
/**
* 自定义全局过滤器
*/
@Bean
public GlobalFilter globalFilter() {
return (exchange, chain) -> {
System.out.println("我是全局过滤器");
return chain.filter(exchange);
};
}
}
启动 Gateway 模块后,访问:
http://localhost:6001/customer-api/customer/name/1请求会转发到:
http://localhost:3001/customer/name/1访问:
http://localhost:6001/goods-api/goods/customer/name/1请求会通过服务发现转发到 goods 服务。
十二、使用 Gateway 时的注意点
1. Gateway 项目不要混用 Spring MVC
Gateway 基于 WebFlux。
不要在 Gateway 模块中同时引入传统 Web MVC 依赖。
否则容易出现启动异常或运行时冲突。
2. 路由路径和 StripPrefix 要配套
如果路由路径是:
/customer-api/**
而后端服务实际路径是:
/customer/**
就需要配置:
- StripPrefix=1
否则转发后的路径可能不匹配后端接口。
3. lb:// 依赖服务发现
如果使用:
lb://goods
需要保证:
项目已经接入注册中心;
goods服务已经成功注册;Gateway 能从注册中心拉取到服务实例。
否则请求无法正确转发。
4. 修改请求对象要使用 mutate
Gateway 中的请求对象是不可变风格。
如果要修改请求头,需要使用:
request.mutate()
不要直接修改:
request.getHeaders()
5. 局部过滤器和全局过滤器职责要分清
局部过滤器适合某个路由专属逻辑。
全局过滤器适合所有请求都需要处理的逻辑。
不要把所有逻辑都堆到全局过滤器里,否则后期维护会比较困难。
结论
Spring Cloud Gateway 是微服务系统中的统一流量入口。
它通过:
Route 定义路由;
Predicate 判断请求是否匹配;
Filter 在请求前后增加处理逻辑;
lb://service-id支持服务发现和负载均衡;局部过滤器处理单个路由逻辑;
全局过滤器处理所有请求通用逻辑。
常见配置方式有两种:
通过 Java 代码配置
RouteLocator;通过
application.yml配置路由规则。
实际项目中,简单路由更推荐放在配置文件中,便于维护和调整;需要复杂逻辑时,可以使用代码方式或自定义过滤器。