背景
【MQ】一套為海量消息和高并發熱點消息,提供高可用精準延時服務的解決方案
我現在有一個需求,就是監聽 RabbitMQ 一個延時交換機的消息數,而 RabbitTemplate 是不存在對應的方法來獲取的。
而我們在 RabbitMQ 的控制臺卻可以發現延時交換機的消息數,所以其開放的 http-api 里存在我們需要的數據,通過抓包可得:
而我們查看這個包,構造請求(抓包+分析的技巧這里不做介紹)
當然你完全可以去看 RabbitMQ 的 http-api 開放文檔,但是我覺得有點多,還不如直接抓包
URL:
http://rabbithost:15672/api/exchanges/{virtualHost}/{exchange}?msg_rates_age=60&msg_rates_incr=5
Method:
GET
Header:
- Authorization:
"Basic " + EncryptUtil.encodeBase64(String.format("%s:%s", rabbitMQConfig.getUsername(), rabbitMQConfig.getPassword()));
很快我們就能寫一個 OpenFeign 客戶端:
@FeignClient(name = "rabbitmq-service", url = "${okr.mq.http-api}")
public interface RabbitMQHttpFeignClient {@GetMapping("/exchanges/{virtualHost}/{exchange}?msg_rates_age=60&msg_rates_incr=5")DelayExchangeVO getMessagesDelayed(@RequestHeader(HttpHeaders.AUTHORIZATION) String authorization,@PathVariable("virtualHost") String virtualHost,@PathVariable("exchange") String exchange);}
但是你會發現,virtualHost 是帶 /
的,但是最終的 url 并沒有轉義,導致路由出錯報了 404
- 400 是參數未通過驗證、401 未通過身份認證、403 無權限
先說結論!!!
配置一個 Contract (協議,約定),并設置 decodeSlash 為 false !
@Component
public class OpenFeignConfig {@Beanpublic Contract notdecodeSlashContract(){// 無自定義處理器、默認的 ConversionService、取消 %2F -> / 的解碼return new SpringMvcContract(Collections.emptyList(), new DefaultConversionService(), Boolean.FALSE);}}
decodeSlash,直譯就是“斜杠解碼”
encode: /
→ %2F
decode: %2F
→ /
而我們就是阻止 %2F
→ /
,那我們為什么要阻止呢?
問題分析
首先我們可能會想,它是如何轉義的,是傳入的時候轉義,還是最終一起轉義:
如果是最終一起轉義,那 /
必然不能被轉義,否則那些路由都會失效,所以如果是最終轉義,無法滿足我們的需求:
這里寫了個簡單的方法,方便理解
public static <P> String buildUrl(String baseUrl, Map<String, List<String>> queryParams, Map<String, P> pathParams) {queryParams = Optional.ofNullable(queryParams).orElseGet(Map::of);pathParams = Optional.ofNullable(pathParams).orElseGet(Map::of);return UriComponentsBuilder.fromHttpUrl(baseUrl).queryParams(new LinkedMultiValueMap<>(queryParams)).buildAndExpand(pathParams).encode() // 開啟譯碼模式.toUriString();
}
如果在傳入的時候轉義,才能實現我們的效果:
public static <P> String buildUrl(String baseUrl, Map<String, List<String>> queryParams, Map<String, P> pathParams) {queryParams = Optional.ofNullable(queryParams).orElseGet(Map::of);pathParams = Optional.ofNullable(pathParams).orElseGet(Map::of);return UriComponentsBuilder.fromHttpUrl(baseUrl).encode() // 開啟譯碼模式,這里之后路徑參數,/ 也會被轉義為 %2F!.queryParams(new LinkedMultiValueMap<>(queryParams)).buildAndExpand(pathParams).toUriString();
}
那 OpenFeign 是哪種呢?如果我們沒看源碼,我們可能沒法判斷,但我們可以知道,OpenFeign 在解析路徑參數的時候,用的是 PathVariableParameterProcessor
參考文章:文章
通過自定義注解 + 自定義處理器的方式,處理請求,我們通過:
data.indexToExpander().put(context.getParameterIndex(), o -> URLEncoder.encode(String.valueOf(o), Charset.defaultCharset());
我們給 {name} 對應的 index 提供了一個解析器,但是貌似沒啥用,如果進行雙重編碼,導致 %
也也被轉義了,但如果只是一重編碼,最終 /
還是以 /
的形式出現
這一度讓我覺得是玄學!
但我對比了 PathVariableParameterProcessor 類的實現,發現其并沒有專門對字符串進行編碼,所以我猜測底層是定然編碼了的,所以我進行了調試,一步步找到了關鍵代碼:
你會發現,如果傳入 /
會被轉義成 %2F
也就是說,傳入時確實已經編碼了,你甚至可以實現傳入 %2F
但并設置其已編碼,所以不會再次編碼,等等無論如何各種方式讓字符串為 %2F
但是這里有一個屬性 encodeSlash,如果為 false,則將最終結果的 %2F
給重新解碼成 /
- 說實話我完全不知道為啥要這樣,太放劍了🤣
- 如果是路徑參數也是個 uri,也有這樣的編程方式,但是我覺得很不規范
這也是我不熟悉 SpringMvcContract 導致的啦,不知道還有這么一個參數 decodeSlash
new SpringMvcContract(Collections.emptyList(), new DefaultConversionService(), false)
decodeSlash 設置為 false 后,encodeSlash 就為 true,%2F
就不會重新解碼成 /
了,最終也就能達到我們的預期的效果了