在互網企業當中網關的重要性我就不再贅述了,相信大家都比較清楚。我們公司網關採用的是 Spring Cloud Gateway。並且是通過自定義 RouteLocator 來實現動態路由的。路由規則是請求參數裏面的 bizType
,比如接收 JSON 格式體的請求對象並且業務方請求的是創建支付訂單接口,下面就是業務方需要傳遞的參數:
{
"bizType" : "createOrder",
.... 其它業務參數
}
下面就是讀取 requestBody 裏面的主動參數,然後解析請求對象裏面的 bizType
,來決定它的路由地址:
由於歷史原因,網關不僅需要 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 請求參數裏面帶有 % 就會報以下錯誤:
針對這種問題其實有兩種處理方式:
上面兩種方式當然就第二種方式更加優雅。下面我們來想一想如何在讀取 requestBody 之前獲取到 Http 請求的 MediaType 的。
當我們在路由調用 readBody 的時候其實就是調用下面的方法:
org.springframework.cloud.gateway.route.builder.PredicateSpec#readBody
public <T> BooleanSpec readBody(Class<T> inClass, Predicate<T> predicate) {
return asyncPredicate(getBean(ReadBodyPredicateFactory.class)
.applyAsync(c -> c.setPredicate(inClass, predicate)));
}
Spring Cloud 中 ReadBodyPredicateFactory 的實現方式如下:
public class ReadBodyPredicateFactory
extends AbstractRoutePredicateFactory<ReadBodyPredicateFactory.Config> {
...
@Override
@SuppressWarnings("unchecked")
public AsyncPredicate<ServerWebExchange> 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 然後從解析後的對象獲取路由規則
...
);
}
後面就不會報之前的異常了。
點亮 ,告訴大家你也在看