背景
Spring Cloud Gateway 是 Spring Cloud 退出的第二代网关框架,我们可以用它来实现 反向代理,路由转发,权限校验等功能,这里介绍一个它的基础功能,通过 Filter 机制实现一个简单的 HTTP 接口处理。
从总体上来看 Spring Cloud Gateway 提供的过滤器可以分为两类,一种是对全局流量都生效的全局过滤器(Global Filter),另外一种是针对特定路径生效的自定义过滤器;通过全局过滤器我们可以实现一些全局请求的处理操作,如请求性能监控,请求日志记录,请求缓存等;通过自定义过滤器我们可以针对特定的请求实现一些特定的请求处理,如添加请求头,添加参数,添加响应头等功能等。
Spring Cloud Gateway 的过滤器处理是经典的 23 种设计模式中的责任链模式(想要学习设计模式的童鞋可以去翻源码)。当一个请求满足路由规则时,负责过滤网络请求的处理器会将全部 GlobalFilter 和特定路由实现的 Filter 添加到过滤器链中,多个 Filter 的排序通过 Ordered 接口实现,用户可以通过手动实现 Ordered 接口中的 getOrder 方法来指定实际执行的顺序, getOrder 返回的数字越小,表示执行的优先级越高,即 Filter 会被越早调用。
当一个请求进入到应用中后,会在请求实际处理前(pre),和结果处理后(post)经过每个 Filter 各一次,我们可以通过 Filter 实现对请求的记录,拦截,日志打印,执行时间记录等功能。
Spring Cloud Gateway 默认已经提供了一些 Filter 供开发者使用,GatewayMetricsFilter 可以用来记录一些指标,通过 spring-boot-starter-actuator 库可以讲这些指标透传出去,可以实现请求内容和响应结果的分析。也可以实现一个告警系统,在系统出现异常请求时及时发起告警。LocalResponseCache 则可以将请求的响应结果缓存下来,这样对于一些重复的请求就可以减少对代理系统的负载,从而提升整体系统的性能。路由转发 Filter ForwardRoutingFilter则可以实现请求的转发,通过修改原始请求路径后,将新的请求转发到目标节点上。NettyRoutingFilter 可以通过 Netty 的 HttpClient 生成下游的请求,然后将响应转发到后续的 Filter 中。除此之外还有一些其他的过滤器,可以通过这些过滤器灵活组合实现想要的功能。
除了 GlobalFilter 之外,还有一类 Filter 的职责没有那么广,他们是专门针对特定的请求实现一些特定的功能,这些 Filter 通过配置的规则作用到目标的请求上,完成一些增强功能。如 AddRequestHeader 可以为进入的请求添加额外的 header ,可以供后面的代理服务器鉴权认证使用;AddRequestParameter 可以添加额外的请求参数,AddResponseHeader 用于添加额外的响应头...诸如此类。
首先用 Spring Starter 创建一个 Spring Cloud Gateway 应用,然后添加配置,为指定 url 增加一个过滤器:
spring:
cloud:
gateway: #网关路由配置
routes:
- id: user-grpc #路由 id,没有固定规则,但唯一,建议与服务名对应
uri: https://[::1]:443 #匹配后提供服务的路由地址
predicates:
#以下是断言条件,必选全部符合条件
- Path=/** #断言,路径匹配 注意:Path 中 P 为大写
- Header=Content-Type,application/json # 断言,请求头匹配:
filters:
- CustomFilter #自定义过滤器名称
@Slf4j
@Component
public class CustomFilterFactory extends AbstractGatewayFilterFactory<Object> {
public CustomFilterFactory(ResourceLoader resourceLoader) {
super(Object.class);
}
@Override
public GatewayFilter apply(Object config) {
GatewayFilter filter = new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
CustomResponseDecorator modifiedResponse = new CustomResponseDecorator(exchange);
ServerWebExchange newExchange = exchange.mutate().response(modifiedResponse).build();
// 跳过后续自动路由到下游代理
ServerWebExchangeUtils.setAlreadyRouted(newExchange);
return modifiedResponse.writeWith(exchange.getRequest().getBody()).then(chain.filter(newExchange));
}
@Override
public String toString() {
return filterToStringCreator(CustomFilterFactory.this).toString();
}
};
// 设置 Filter 优先级在写入响应前
int order = NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
return new OrderedGatewayFilter(filter, order);
}
@Override
public String name() {
return "CustomFilter";
}
@Slf4j
static class CustomResponseDecorator extends ServerHttpResponseDecorator {
/**
* 返回成功的消息
*/
public static final HttpMsg SUCCESS_MESSAGE;
private final ServerWebExchange exchange;
static {
SUCCESS_MESSAGE = new HttpMsg();
SUCCESS_MESSAGE.setCode(200);
SUCCESS_MESSAGE.setMessage("success");
}
public CustomResponseDecorator(ServerWebExchange exchange) {
super(exchange.getResponse());
this.exchange = exchange;
}
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> bodyPub) {
ServerHttpRequest request = exchange.getRequest();
HttpMethod method = request.getMethod();
String path = request.getPath().value();
long contentLength = request.getHeaders().getContentLength();
String clientIp = Optional.ofNullable(request.getRemoteAddress()).map(InetSocketAddress::getAddress)
.map(InetAddress::toString).orElse(null);
Mono<String> reqBodyMono;
// 当请求包含 body 时,解析 body 内容为 string
if (contentLength > 0) {
reqBodyMono = DataBufferUtils.join(bodyPub).mapNotNull(buffer -> {
String str = buffer.toString(StandardCharsets.UTF_8);
// 解析完成后,释放释放 buffer
DataBufferUtils.release(buffer);
return str;
});
} else {
// 不包含 body 时,直接返回空字符串
reqBodyMono = Mono.just("");
}
Mono<DataBuffer> res = reqBodyMono.handle((body, sink) -> {
// 记录请求信息
log.debug("receive request client ip:{} path: {}({}),body: {}", clientIp, path, method, body);
sink.next(SUCCESS_MESSAGE);
}).map(resMsg -> {
getHeaders().setContentType(MediaType.APPLICATION_JSON);
return JSONUtil.toJsonStr(resMsg);
}).map(str -> bufferFactory().wrap(ByteBuffer.wrap(str.getBytes(StandardCharsets.UTF_8))));
return super.writeWith(res);
}
}
}