快速集成
新创建服务:gateway-service
引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
修改 bootstrap.yaml
server:
port: 5000
spring:
main:
allow-bean-definition-overriding: true
cloud:
# 网关配置
gateway:
routes:
- id: order_route # 路由的唯一标识
uri: http://localhost:6200 # 转发地址
predicates: # 断言规则,用于路由匹配
- Path=/order-service/**
filters:
- StripPrefix=1 # 剔除 url 中的第一层路径: /order-service/
分别启动:order-service、gateway-service
order-service 含有一个接口:
@RestController
public class OrderController {
@Value("${server.port}")
private String port;
@GetMapping(value = "/echo/{string}")
public String echo(@PathVariable String string) {
return String.format("Order service %s %s", port, string);
}
}
浏览器访问:localhost:5000/order-service/echo/cqq
浏览器输出:Order service 6200 cqq
gateway 集成 nacos
引入 nacos 服务治理与服务配置:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
修改 bootstrap.yaml
spring:
main:
allow-bean-definition-overriding: true
cloud:
nacos:
# 通用配置
# server-addr: 127.0.0.1:8000 # 1. nacos 集群服务地址
server-addr: 127.0.0.1:8100 # 1. nacos 服务地址
username: nacos
password: nacos
# 服务治理
discovery:
namespace: 0298b122-a60d-47f5-9be3-9ea149f17185
group: DEFAULT_GROUP
service: nacos-gateway-service
cluster-name: gateway-service-cluster
weight: 1
# 服务配置
config:
namespace: 0298b122-a60d-47f5-9be3-9ea149f17185
group: DEFAULT_GROUP
name: gateway-service
file-extension: yaml
refresh-enabled: true
根据服务名称转发
修改 nacos 远程配置文件中的路由配置:负载均衡转发、使用服务名作为 uri
server:
port: 5000
logging:
level:
root: info
spring:
mvc:
path-match:
matching-strategy: ANT_PATH_MATCHER
application:
name: gateway-service
cloud:
config:
override-none: true
allow-override: true
override-system-properties: false
# 网关配置
gateway:
routes:
- id: order_route # 路由的唯一标识
uri: lb://nacos-order-service # lb: load-balance 启用负载均衡,并根据服务名称自动转发
predicates: # 断言规则,用于路由匹配
- Path=/order-service/**
filters:
- StripPrefix=1 # 剔除 url 中的第一层路径: /order-service/
查看一下 nacos 中已注册的服务列表:
浏览器访问:http://localhost:5000/order-service/echo/cqq
浏览器输出:Order service 6200 cqq
定位器模式
启用 discovery.locator.enabled
开启定位器模式后,可以帮助我们简化路由的配置。即使不配置路由转发,默认也可以通过服务名访问到服务了。
比如订单服务接口:localhost:6200/echo/{str}
,可以被映射为 localhost:5000/nacos-order-service/echo/{str}
,即使我们没有配置任何路由。
修改 nacos 远程配置文件中的路由配置:
# 网关配置
gateway:
discovery:
locator:
enabled: true
routes:
- id: order_route # 路由的唯一标识
uri: lb://nacos-order-service # lb: load-balance 启用负载均衡,并根据服务名称自动转发
predicates: # 断言规则,用于路由匹配
- Path=/route-order-service/**
filters:
- StripPrefix=1 # 剔除 url 中的第一层路径: /order-service/
并且定位器模式兼容手动路由配置
,注意,我有意将手动路由中的断言配置改为了:/route-order-service/**
,也就是说此时根据:http://localhost:5000/route-order-service/echo/cqq
或 http://localhost:5000/nacos-order-service/echo/cqq
都可以访问到 order-service。
断言
内置断言工厂
gateway 提供了许多内置的路由断言方式,上面我们使用的 Path=/uri 即是其中的一种:PathRoutePredicateFactory
。
官网文档地址,里面介绍了所有内置断言工厂的配置。其实根据断言工厂名称就可以推测出是根据什么进行断言匹配的。
自定义断言工厂
spring:
cloud:
gateway:
routes:
predicates:
- Path=/route-order-service/**
在创建自定义工厂前,先介绍一下快捷配置。Path = /route-order-service/**
就是一个快捷配置。其中,Path 会被拼接 RoutePredicateFactory
后在 IOC 容器中寻找对应的断言工厂 Bean 进行断言 Bean 的注册。每次请求进入后,就会执行该断言。其次,/route-order-service/**
会作为配置值注入到 PathRoutePredicateFactory
中,辅助进行断言。
下面我们创建一个自己的断言工厂:
- 必须是一个 Spring Bean
- 必须以 RoutePredicateFactory 作为类名后缀
- 继承 AbstractRoutePredicateFactory
- 设置配置类,声明配置属性接收配置文件中对应的断言信息
- 重写断言方法逻辑
// 1. 声明为 Bean
// 2、3. 以 RoutePredicateFactory 作为类名后缀,并继承 AbstractRoutePredicateFactory
@Component
public class MyPredicateRoutePredicateFactory extends AbstractRoutePredicateFactory<MyPredicateRoutePredicateFactory.Config> {
// 4. 设置配置类
public MyPredicateRoutePredicateFactory() {
super(MyPredicateRoutePredicateFactory.Config.class);
}
// 5.断言逻辑
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return (GatewayPredicate) serverWebExchange -> config.getMyPredicate().equals("cqq");
}
// 配置注入 Config 配置类的配置字段值时,字段的获取循序
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("key2", "key1");
}
@Validated
@Data
public static class Config {
private String key1;
private String key2;
}
}
新增断言配置(多个配置项,逗号分隔):
spring:
cloud:
gateway:
routes:
predicates:
- MyPredicate=key1,key2
访问:http://localhost:5000/route-order-service/echo/123
由于 key2 != cqq,故响应 404。
过滤器
- 作用:在请求的传递过程中对请求和响应做一些处理
- 处理阶段:
- Pre 请求服务节点前:可用来实现选择集群中的服务节点进行调用、记录调试信息等
- Post 请求服务节点后:可用来实现为响应添加 HTTP Response Header、收集统计信息和指标等。
- 类型:局部过滤器(作用在某一个路由上)全局过滤器(作用在全部路由上)
处理阶段:Pre、Post
内置局部过滤器
官方提供了许多内置的局部过滤器:官网文档地址
过滤器工厂 | 作用 | 参数 |
---|---|---|
AddRequestHeader | 为原始请求添加Header | Header的名称及值 |
AddRequestParameter | 为原始请求添加请求参数 | 参数名称及值 |
AddResponseHeader | 为原始响应添加Header | Header的名称及值 |
DedupeResponseHeader | 剔除响应头中重复的值 | 需要去重的Header名称及去重策略 |
Hystrix | 为路由引入Hystrix的断路器保护 | Hystrixcommand的名称 |
FallbackHeaders | 为fallbackUri的请求头中添加具体的异常信息 | Header的名称 |
PrefixPath | 为原始请求路径添加前缀 | 前缀路径 |
PreserveHostHeader | 为请求添加一个preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host | 无 |
RequestRateLimiter | 用于对请求限流,限流算法为令牌桶 | keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus |
RedirectTo | 将原始请求重定向到指定的URL | http状态码及重定向的url |
RemoveHopByHopHeadersFilter | 为原始请求删除IETF组织规定的一系列Header | Header名称 |
RemoveResponseHeader | 为原始请求删除某个Header | Header的名称 |
RewritePath | 重写原始的请求路径 | 原始路径正则表达式以及重写后路径的正则表达式 |
RewriteResponseHeader | 重写原始响应中的某个Header | Header名称,值的正则表达式,重写后的值 |
SaveSession | 在转发请求之前,强制执行websession::save操作 | 无 |
secureHeaders | 为原始响应添加一系列起安全作用的响应头 | 无,支持修改这些安全响应头的值 |
SetPath | 修改原始的请求路径 | 修改后的路径 |
SetResponseHeader | 修改原始响应中某个Header的值 | Header名称,修改后的值 |
SetStatus | 修改原始响应的状态码 | HTTP状态码,可以是数字,也可以是字符串 |
StripPrefix | 用于截断原始请求的路径 | 使用数字表示要截断的路径的数量 |
Retry | 针对不同的响应进行重试 | retries、statuses、methods、 series |
RequestSize | 设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返413Payload Too Large | 请求包大小,单位为字节,默认值为5M |
ModifyRequestBody | 在转发请求之前修改原始请求体内容 | 修改后的请求体内容 |
ModifyResponseBody | 修改原始响应体的内容 | 修改后的响应体内容 |
简单配置一个 AddRequestHeader Filter 进行演示:
修改配置文件(与配置断言一样):
spring:
cloud:
gateway:
routes:
filters:
- StripPrefix=1
- AddRequestHeader=my-header,mh
修改一下原来的接口,打印一下 header:
@RestController
public class OrderController {
@Value("${server.port}")
private String port;
@GetMapping(value = "/echo/{string}")
public String echo(
@RequestHeader(value = "my-header", required = false) String mh,
@PathVariable String string) {
return String.format("Order service %s %s, header %s", port, string, mh);
}
}
访问订单服务:http://localhost:5000/route-order-service/echo/123
浏览器输出:Order service 6200 123, header mh
自定义局部过滤器
- 必须是一个 Spring Bean
- 继承 AbstractGatewayFilterFactory
- 设置配置类,声明配置属性接收配置文件中对应的断言信息
- 重写拦截方法逻辑
@Component
public class MyFilter extends AbstractGatewayFilterFactory<MyFilter.Config> {
public MyFilter() {
super(MyFilter.Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("name", "value");
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
exchange.getRequest().mutate().header(config.getName(), config.getValue());
return chain.filter(exchange);
};
}
@Validated
@Data
public static class Config {
private String name;
private String value;
}
}
新增过滤器配置(多个配置项,逗号分隔):
spring:
cloud:
gateway:
routes:
filters:
- StripPrefix=1
# - AddRequestHeader=my-header,mh
- MyFilter=my-header,mh
访问订单服务:http://localhost:5000/route-order-service/echo/123
浏览器输出:Order service 6200 123, header mh
内置全局过滤器
官方提供了许多默认生效的全局过滤器:官网文档地址
比如前面配置 uri 的 lb scheme,可以起到服务负载均衡调用的效果:
gateway:
discovery:
locator:
enabled: true
routes:
- id: order_route
uri: lb://nacos-order-service
实现类就是 LoadBalancerClientFilter
,内部根据 LoadBalancerClient
规范接口的实现类,进行负载均衡调用。
自定义全局过滤器
- 必须是一个 Spring Bean
- 实现 GlobalFilter
@Slf4j
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
//配置执行优先级
@Override
public int getOrder() {
return 0;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Mono<Void> filter = chain.filter(exchange);
log.info("log request, uir: {}", exchange.getRequest().getURI());
return filter;
}
}
跨域配置
配置文件
spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决 options 请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的可以跨域请求, '*' 表示所有
- "https://localhost:8001"
- "https://localhost:8002"
- "https://localhost:8003"
allowedMethods: # 允许的跨域的请求方式, '*' 表示所有
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的请求头, '*' 表示所有
allowCredentials: true # 是否允许携带 cookie
maxAge: 360000 # 跨域检测的有效期
配置类
@Configuration
public class GlobalCorsConfig {
private final List<String> allowOrigins = Arrays.asList("http://localhost:8001", "http://localhost:8002", "http://localhost:8003");
private final List<String> allowMethods = Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS");
private final List<String> allowHeaders = Arrays.asList("header1", "header2");
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
allowOrigins.forEach(config::addAllowedOrigin);
allowMethods.forEach(config::addAllowedMethod);
allowHeaders.forEach(config::addAllowedHeader);
config.setAllowCredentials(true);
config.setMaxAge(360000L);
config.addExposedHeader("*"); // 暴露头部信息
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
gateway 整合 sentinel
集成配置
添加依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
Sentinel 的配置依旧分为代码、管控台配置两种。后面使用管控台的方式进行配置,所以需要在 bootstrap.yaml 中配置一下 Sentinel:
spring:
cloud:
# 服务监控
sentinel:
transport:
# 控制台的地址(ip:port)
dashboard: 127.0.0.1:8400
# 与控制台通讯的端口,默认是8719,不可用会一直+1
# 用于客户端暴露 sentinel 原生 api 的访问端口,用于管控台获取客户端簇点链路、监控数据、更新规则等
port: 5001
# 和控制台保持心跳的ip地址
client-ip: 127.0.0.1
# 发送心跳的周期,默认是10s
heartbeat-interval-ms: 3000
# 禁止收敛URL的入口 context
web-context-unify: false
# 饿加载
eager: true
- 启动 sentinel server
- 启动 order-service(内置接口如下)
@RestController
public class OrderController {
@Value("${server.port}")
private String port;
@GetMapping(value = "/echo/{string}")
public String echo(
@RequestHeader(value = "my-header", required = false) String mh,
@PathVariable String string) {
return String.format("Order service %s %s, header %s", port, string, mh);
}
@GetMapping(value = "/get")
public String get() {
return String.format("Order service %s Get %s ", port, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
}
}
- 启动 gateway-service
- 访问:
http://localhost:5000/route-order-service/echo/123
- 查看 Sentinel 管控台
网关服务与普通服务在 Sentinel 中的不同
普通服务菜单:
我们启动一个集成了 Sentinel 的服务:
网关服务菜单:
减少了:
- 热点规则
- 授权规则
- 集群流控
新增了:
- API 管理
并且流控规则的配置也多了很多属性(熔断降级依旧保持不变):
那我们就着重关注一下网关的流控规则配置 与 API 管理。
API 管理菜单
配置需要防护的接口组,当配置流控规则时进行关联。
一个 API 分组中可以配置多个匹配模式,匹配规则一目了然,上面分别配置了精确与前缀匹配(原本想通过正则配置 /echo/**
的,不知为何只有配置前缀模式中才生效)。
网关的流控规则
配置好了 API 组后,我们在配置一下流控规则,并绑定刚刚创建好的 API 组:
对比之前普通服务的流控规则,网关服务多出了很多配置:
- API 类型:
- Route ID:配置文件中的路由主键
- API 分组:API 管理中的 API 组
- 针对请求属性 & 属性值匹配:更细粒度的配置需要流控的请求,效果等价于配置文件中的路由断言。这两个属性是必须要同时配置的,这样才会有意义。
- 请求属性:
- Client IP:匹配指定的客户端 IP
- Remote Host:匹配指定的客户端域名
- Header:匹配指定名称的请求头
- URL 参数:匹配指定名称的 URL 请求参数
- Cookie:匹配指定名称的 Cookie
- 匹配模式:
- 精确
- 子串:
like {匹配串}%
- 正则
- 间隔:熔断规则中的时间窗口
- Brust size:达到 QPS 阈值时,最多可在容忍的请求数。
全局异常处理器
@Configuration
public class SentinelHandlerConfig implements InitializingBean {
@Data
@Builder(toBuilder = true)
@Accessors(chain = true)
public static class R<T> {
private long code;
private T data;
private String message;
public R() {
}
public R(long code, T data, String message) {
super();
this.code = code;
this.data = data;
this.message = message;
}
}
@Override
public void afterPropertiesSet() throws Exception {
GatewayCallbackManager.setBlockHandler(
(exchange, throwable) ->
ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(
BodyInserters.fromValue(
R.builder().code(HttpStatus.TOO_MANY_REQUESTS.value()).message("已限流").build()
)
));
}
}