背景
我们使用了springcloud gateway作为也给路由转发功能,由于历史遗留问题,不仅仅需要根据path转发,还需要根据get或者post中的参数进行转发
解决方案
这里我们使用自定义的Predicate进行转发
简介
这里简单介绍下相关术语 (1)Filter(过滤器):
和Zuul的过滤器在概念上类似,可以使用它拦截和修改请求,并且对上游的响应,进行二次处理。过滤器为org.springframework.cloud.gateway.filter.GatewayFilter类的实例。
(2)Route(路由):
网关配置的基本组成模块,和Zuul的路由配置模块类似。一个Route模块由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配,目标URI会被访问。
(3)Predicate(断言):
这是一个 Java 8 的 Predicate,可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。断言的输入类型是一个 ServerWebExchange。
这里我们会使用自定义的断言来实现,常用的断言有如下几个:
详细信息可以参考下面链接:https://www.jianshu.com/p/d2c3b6851e1d?utm_source=desktop&utm_medium=timeline
GET请求转发
在常用断言中就有支持根据get参数转发,所以这里需要同时使用path以及query断言,可以根据如下配置
spring:
cloud:
gateway:
routes:
- id: blog
uri: http://blog.yuqiyu.com
predicates:
- Path=/api/demo
- Query=xxx, zzz
根据上面配置,我们限定了参数xxx必须为zzz时才会被成功转发,否则会出现404抓发失败,根据上面配置就可以根据get参数转发
POST请求转发
post参数转发,没有现成的转发断言,这里我们需要参考readbody断言来实现,下面是ReadBodyPredicateFactory 的源码
public class ReadBodyPredicateFactory extends AbstractRoutePredicateFactory<ReadBodyPredicateFactory.Config> {
protected static final Log log = LogFactory.getLog(ReadBodyPredicateFactory.class);
private static final String TEST_ATTRIBUTE = "read_body_predicate_test_attribute";
private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";
private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
public ReadBodyPredicateFactory() {
super(ReadBodyPredicateFactory.Config.class);
}
public AsyncPredicate<ServerWebExchange> applyAsync(ReadBodyPredicateFactory.Config config) {
return (exchange) -> {
Class inClass = config.getInClass();
Object cachedBody = exchange.getAttribute("cachedRequestBodyObject");
if (cachedBody != null) {
try {
boolean test = config.predicate.test(cachedBody);
exchange.getAttributes().put("read_body_predicate_test_attribute", test);
return Mono.just(test);
} catch (ClassCastException var6) {
if (log.isDebugEnabled()) {
log.debug("Predicate test failed because class in predicate does not match the cached body object", var6);
}
return Mono.just(false);
}
} else {
return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange, (serverHttpRequest) -> {
return ServerRequest.create(exchange.mutate().request(serverHttpRequest).build(), messageReaders).bodyToMono(inClass).doOnNext((objectValue) -> {
exchange.getAttributes().put("cachedRequestBodyObject", objectValue);
}).map((objectValue) -> {
return config.getPredicate().test(objectValue);
});
});
}
};
}
public Predicate<ServerWebExchange> apply(ReadBodyPredicateFactory.Config config) {
throw new UnsupportedOperationException("ReadBodyPredicateFactory is only async.");
}
public static class Config {
private Class inClass;
private Predicate predicate;
private Map<String, Object> hints;
public Config() {
}
public Class getInClass() {
return this.inClass;
}
public ReadBodyPredicateFactory.Config setInClass(Class inClass) {
this.inClass = inClass;
return this;
}
public Predicate getPredicate() {
return this.predicate;
}
public ReadBodyPredicateFactory.Config setPredicate(Predicate predicate) {
this.predicate = predicate;
return this;
}
public <T> ReadBodyPredicateFactory.Config setPredicate(Class<T> inClass, Predicate<T> predicate) {
this.setInClass(inClass);
this.predicate = predicate;
return this;
}
public Map<String, Object> getHints() {
return this.hints;
}
public ReadBodyPredicateFactory.Config setHints(Map<String, Object> hints) {
this.hints = hints;
return this;
}
}
}
这个只是把post参数读入到缓存,配置如下
predicates:
- Path=/card/api/**
- name: ReadBodyPredicateFactory #使用ReadBodyPredicateFactory断言,将body读入缓存
args:
inClass: '#{T(String)}'
predicate: '#{@bodyPredicate}' #注入实现predicate接口类
但是这个暂时不能满足要求,我们需要参考ReadBodyPredicateFactory自定义一个predicatefactory来实现我们的需求
@Component()
@Slf4j
public class MyReadBodyPredicateFactory extends AbstractRoutePredicateFactory<MyReadBodyPredicateFactory.Config> {
private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies
.withDefaults().messageReaders();
public MyReadBodyPredicateFactory() {
super(MyReadBodyPredicateFactory.Config.class);
}
public MyReadBodyPredicateFactory(Class<MyReadBodyPredicateFactory.Config> configClass) {
super(configClass);
}
@Override
@SuppressWarnings("unchecked")
public AsyncPredicate<ServerWebExchange> applyAsync(MyReadBodyPredicateFactory.Config config) {
return new AsyncPredicate<ServerWebExchange>() {
@Override
public Publisher<Boolean> apply(ServerWebExchange exchange) {
Object cachedBody = exchange.getAttribute(MyFacadeConstants.CACHE_REQUEST_BODY_OBJECT_KEY);
if (cachedBody != null) {
try {
boolean test = match(config.sceneIds, (MycRequest) cachedBody);
return Mono.just(test);
} catch (ClassCastException e) {
if (log.isDebugEnabled()) {
log.debug("Predicate test failed because class in predicate "
+ "does not match the cached body object", e);
}
}
return Mono.just(false);
} else {
return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange,
(serverHttpRequest) -> ServerRequest.create(exchange.mutate().request(serverHttpRequest).build(), messageReaders)
.bodyToMono(MycRequest.class)
.doOnNext(objectValue -> exchange.getAttributes().put(MyFacadeConstants.CACHE_REQUEST_BODY_OBJECT_KEY,objectValue))
.map(objectValue -> { return match(config.sceneIds, objectValue);}));
}
}
};
}
private boolean match(String params, MycRequest mycRequest) {
if("others".equals(params)){
return true;
}
String[] paramArray = params.split(",");
if (ArrayUtils.contains(paramArray, mycRequest.getRouteId)) {
return true;
} else {
return false;
}
}
@Override
@SuppressWarnings("unchecked")
public Predicate<ServerWebExchange> apply(MyReadBodyPredicateFactory.Config config) {
throw new UnsupportedOperationException(
"MyReadBodyPredicateFactory is only async.");
}
public static class Config {
private String params;
public MyReadBodyPredicateFactory.Config setParams(params) {
this.params = params;
return this;
}
public String getParams() {
return params;
}
}
}
这里我们可以根据将参数转为MyRequest,然后再进行判断是否路由,当然这里我们同样也需要使用到path断言,配置如下:
spring:
cloud:
gateway:
routes:
- id: route1
uri: http://host1:8080
predicates:
- Path=/api/demo
- name: MyReadBodyPredicateFactory
args:
params: "23,22"
- id: route2
uri: http://host2:8080
predicates:
- Path=/api/demo
- name: RecommendReadBodyPredicateFactory
args:
params: "44,56"
这样就可以根据post参数路由转发了,如下监控: