springcloudgateway动态路由_Spring Cloud Gateway 自定义 ReadBodyPredicateFactory 实现动态路由...

在互网企业当中网关的重要性我就不再赘述了,相信大家都比较清楚。我们公司网关采用的是 Spring Cloud Gateway。并且是通过自定义 RouteLocator 来实现动态路由的。路由规则是请求参数里面的 bizType,比如接收 JSON 格式体的请求对象并且业务方请求的是创建支付订单接口,下面就是业务方需要传递的参数:

{
"bizType" : "createOrder",
.... 其它业务参数
}

下面就是读取 requestBody 里面的主动参数,然后解析请求对象里面的 bizType,来决定它的路由地址:53677ccd8b1e1d536651f7e28f4a1044.png

由于历史原因,网关不仅需要 application/json 这种 Json 格式 MediaType 的请求对象,还需要支持 MediaType 为application/x-www-form-urlencoded 这种请求。而网关之前的处理方式比较粗暴,当有请求来临的时候因为有可能是 application/x-www-form-urlencoded所以直接 URLDecoder :

将请求中特殊字符转义

   public static String requestDecode(String requestBody){
try {
return URLDecoder.decode(convertStringForAdd(requestBody), "UTF-8");
} catch (UnsupportedEncodingException e) {
log.error("requestBody decode error: {}", e);
}
return requestBody;
}

这种处理方式导致的问题就是如果 JSON 请求参数里面带有 % 就会报以下错误:

6b7997807b307cd7cf8d158ecbc96346.png
针对这种问题其实有两种处理方式:

  • 把对象进行转换成 JSON,如果转换成功就OK,否则就先 UrlEncode,然后再用 & 分离 Key/value。

  • 还有一种方式就是在进行读取 requestBody 之前获取到它的 MediaType

上面两种方式当然就第二种方式更加优雅。下面我们来想一想如何在读取 requestBody 之前获取到 Http 请求的 MediaType 的。

当我们在路由调用 readBody 的时候其实就是调用下面的方法:

org.springframework.cloud.gateway.route.builder.PredicateSpec#readBody

public  BooleanSpec readBody(Class inClass, Predicate predicate) {
return asyncPredicate(getBean(ReadBodyPredicateFactory.class)
.applyAsync(c -> c.setPredicate(inClass, predicate)));
}

Spring Cloud 中 ReadBodyPredicateFactory 的实现方式如下:

public class ReadBodyPredicateFactory
extends AbstractRoutePredicateFactory {
...
@Override
@SuppressWarnings("unchecked")
public AsyncPredicate applyAsync(Config config) {
return exchange -> {
Class inClass = config.getInClass();
Object cachedBody = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY);
Mono> modifiedBody;
// We can only read the body from the request once, once that happens if we
// try to read the body again an exception will be thrown. The below if/else
// caches the body object as a request attribute in the ServerWebExchange
// so if this filter is run more than once (due to more than one route
// using it) we do not try to read the request body multiple times
if (cachedBody != null) {
try {
boolean test = config.predicate.test(cachedBody);
exchange.getAttributes().put(TEST_ATTRIBUTE, test);
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(inClass)
.doOnNext(objectValue -> exchange.getAttributes()
.put(CACHE_REQUEST_BODY_OBJECT_KEY, objectValue))
.map(objectValue -> config.getPredicate()
.test(objectValue)));
}
};
}
...
}

我们可以看到这里使用了对象 ServerWebExchange,而这个对象就是 Spring webflux 定义的 Http 请求对象。上面的代码逻辑是判断 exchange 中的属性中是否包含属性为 cachedRequestBodyObject 的 requestBody 对象,如果不包含就解析并添加cachedRequestBodyObject 到 exchange。在这里可以看到我们对 ReadBodyPredicateFactory 对象并不可以扩展,所以唯一的方式就是继承这个类,因为在读取 MediaType 的时候参数只有 requestBody:String,所以我们只有通过 ThreadLocal 来进行参数传递。在真正 PredicateSpec#readBody获取到 MediaType,就可以很好的解析 requestBody。下面就是具体的代码实现:

1、GatewayContext.java

GatewayContext 定义网关上下文,保存 MediaType 用于 readBody 时解析。

GatewayContext.java

@Getter
@Setter
public class GatewayContext {

private MediaType mediaType;

}

2、GatewayContextHolder.java

GatewayContextHolder 通过 ThreadLocal 传递 GatewayContext ,在请求对象解析时使用。

GatewayContextHolder.java

public class GatewayContextHolder {

private static Logger logger = LoggerFactory.getLogger(GatewayContextHolder.class);

private static ThreadLocal<GatewayContext> tl = new ThreadLocal<>();

public static GatewayContext get() {
if (tl.get() == null) {
logger.error("gateway context not exist");
throw new RuntimeException("gateway context is null");
}
return tl.get();
}

public static void set(GatewayContext sc) {
if (tl.get() != null) {
logger.error("gateway context not null");
tl.remove();
}
tl.set(sc);
}

public static void cleanUp() {
try {
if (tl.get() != null) {
tl.remove();
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}

}

3、CustomReadBodyPredicateFactory.java

CustomReadBodyPredicateFactory 继承 ReadBodyPredicateFactory ,在原有解析 requestBody 的情况下,添加获取 MediaType 的逻辑。

CustomReadBodyPredicateFactory.java

public class CustomReadBodyPredicateFactory extends ReadBodyPredicateFactory {

protected static final Log log = LogFactory.getLog(CustomReadBodyPredicateFactory.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 CustomReadBodyPredicateFactory() {
super();
}

@Override
public AsyncPredicate<ServerWebExchange> applyAsync(ReadBodyPredicateFactory.Config config) {
return exchange -> {
Class inClass = config.getInClass();

Object cachedBody = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY);

// 获取 MediaType
MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
GatewayContext context = new GatewayContext();
context.setMediaType(mediaType);
GatewayContextHolder.set(context);
// We can only read the body from the request once, once that happens if we
// try to read the body again an exception will be thrown. The below if/else
// caches the body object as a request attribute in the ServerWebExchange
// so if this filter is run more than once (due to more than one route
// using it) we do not try to read the request body multiple times
if (cachedBody != null) {
try {
boolean test = config.getPredicate().test(cachedBody);
exchange.getAttributes().put(TEST_ATTRIBUTE, test);
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(inClass)
.doOnNext(objectValue -> exchange.getAttributes()
.put(CACHE_REQUEST_BODY_OBJECT_KEY, objectValue))
.map(objectValue -> config.getPredicate()
.test(objectValue)));
}
};
}

}

4、GatewayBeanFactoryPostProcessor.java

通过 Spring framework 的 BeanDefinitionRegistryPostProcessor 扩展在实例化对象之前,把 readBody 的原有操作类ReadBodyPredicateFactory 删除,替换成我们自定义类 CustomReadBodyPredicateFactory

GatewayBeanFactoryPostProcessor.java

@Component
public class GatewayBeanFactoryPostProcessor implements BeanDefinitionRegistryPostProcessor {

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// do nothing
}

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
registry.removeBeanDefinition("readBodyPredicateFactory");
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(CustomReadBodyPredicateFactory.class)
.setScope(BeanDefinition.SCOPE_SINGLETON)
.setRole(BeanDefinition.ROLE_SUPPORT)
.getBeanDefinition();
registry.registerBeanDefinition("readBodyPredicateFactory", beanDefinition);
}

}

下面就是修改后的自定义路由规则。

public RouteLocatorBuilder.Builder route(RouteLocatorBuilder.Builder builder) {
return builder.route(r -> r.readBody(String.class, requestBody -> {
MediaType mediaType = GatewayContextHolder.get().getMediaType();
// 通过 mediaType 解析 requestBody 然后从解析后的对象获取路由规则
...
);
}

后面就不会报之前的异常了。

f04247b71c87c57cc15eea4a1dcc92a6.gif

各大互联网企业Java面试题汇总,如何成功拿到百度的offer

阿里面试官:HashMap中的8和6的关系(1)

Spring反射+策略模式Demo

Java之Redis队列+Websocket+定时器实现跑马灯实时刷新

深入理解JVM垃圾收集机制,下次面试你准备好了吗

JAVA架构师成功拿到阿里P7offer,全靠这份2020最新面试题

大专程序员面试了25家公司,总结出来的痛苦经验!

程序员的十个升职的好习惯

交流/资源分享

OMG关注它

b9991d3e674cb7cf09666c6c401654d0.png

点亮 62c3d27d3435fbc73520ad53301fc6d6.png,告诉大家你也在看 12c603baf3890e7fd9e0bff65198b74b.gif

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Spring Cloud Gateway动态路由是指在Spring Cloud Gateway网关中,路由规则可以在运行时动态地进行添加、删除、修改等操作。相比于静态路由,动态路由可以根据实际情况进行动态调整,从而更加灵活、方便地进行流量控制和负载均衡。例如,在服务上线、下线或者进行扩容缩容的时候,可以通过修改路由规则,动态地将流量引导到不同的服务实例中,从而实现动态负载均衡和容错能力。 ### 回答2: Spring Cloud Gateway动态路由是指在Spring Cloud Gateway网关中,根据某些条件动态地将请求路由到不同的目标服务实例上。传统的静态路由需要事先配置好路由规则,但是在微服务架构中,服务的实例会动态地增加、减少、更新,因此需要一种能够动态适应变化的路由机制。 Spring Cloud Gateway动态路由实现需要依赖于服务注册与发现组件,比如Eureka或Consul。当服务实例注册到服务注册中心时,Spring Cloud Gateway会订阅服务注册中心的变化,当有新的服务实例上线或下线时会自动更新路由规则。 动态路由可以根据多种条件进行判断和匹配,如路径、域名、Header、请求参数等。可以根据业务需求动态地配置路由规则,使得请求能够被准确地路由到目标服务实例上。动态路由能够实现动态扩展和负载均衡,提高系统的灵活性和可伸缩性。 Spring Cloud Gateway动态路由的配置通常以YAML或JSON的形式进行,可以通过配置文件、配置中心或接口的方式进行配置。支持多种动态路由的配置方式,如断言(Predicate)、过滤器(Filter)、转发(Forwarding)、重定向(Redirecting)等,可以根据具体需求实现各种功能。 总之,Spring Cloud Gateway动态路由是一种能够根据条件动态路由请求到不同服务实例的机制,具有灵活、可扩展、高效的特点,是构建微服务架构中的网关的重要特性。 ### 回答3: Spring Cloud Gateway动态路由是一种基于Spring Cloud Gateway框架的动态路由功能。传统的静态路由是在网关的配置文件中预先定义好所有的路由规则,而动态路由可以在运行时根据业务需要实时插入、修改和删除路由规则,实现灵活的请求转发和负载均衡。通过动态路由,可以根据不同的路径或者请求头等匹配条件,将请求转发到指定的目标服务,从而实现微服务架构中的请求路由和负载均衡功能。动态路由的配置可以通过网关的API接口或者命令行工具进行管理,使得路由的配置更加灵活和方便。同时,动态路由还支持动态修改和重载路由规则,可以根据实际情况动态调整路由策略,提高系统的可用性和弹性。Spring Cloud Gateway动态路由实现是基于Spring Framework中的路由器和过滤器的概念,通过使用reactive编程模型处理请求,并且支持使用各种插件来扩展网关的功能,例如服务发现、熔断器、限流等。总之,Spring Cloud Gateway动态路由提供了一种灵活、易用且高性能的路由解决方案,适用于构建微服务架构的API网关。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值