在微服務開發中SpringCloud全家桶集成了OpenFeign用于服務調用,SpringCloud的OpenFeign使用SpringMVCContract來解析OpenFeign的接口定義。 但是SpringMVCContract的Post接口解析實現有個巨坑,就是如果使用的是@RequestParam傳參的Post請求,參數是直接掛在URL上的。
問題發現與分析
最近線上服務器突然經常性出現CPU高負載的預警,經過排查發現日志出來了大量的OpenFeign跨服務調用出現400的錯誤(HTTP Status 400)。
一般有兩種情況:
- nginx 返回400
- java應用返回400
通過分析發現400是java應用返回的,那么可以確定是OpenFeign客戶端發起跨服務請求時出現異常了。 但是查看源碼發現出現這個問題的服務接口非常簡單,就是一個只有三個參數的POST請求接口,這個錯誤并不是必現的錯誤,而是當參數值比較長(String)的時候才會出現。 所以可以初步確認可能是參數太長導致請求400,對于POST請求因參數太長導致400錯誤非常疑惑,POST請求除非把參數掛在URL上,否則不應該出現400才對。
問題排查
為了驗證上面的猜測,手寫了一個簡單的示例,在驗證過程中特意開啟了OpenFeign的debug日志。
首先編寫服務接口
這是一個簡單的Post接口,僅有一個參數(這里的代碼僅用于驗證,非正式代碼)
1
2
3
4
5
|
@FeignClient (name = "user-service-provider" ) public interface HelloService { @PostMapping ( "/hello" ) public String hello( @RequestParam ( "name" ) String name); } |
編寫服務
服務這里隨便寫一個Http接口即可(同上,代碼僅用于驗證)
1
2
3
4
5
6
7
8
9
10
11
12
|
@SpringBootApplication @RestController public class Starter { @RequestMapping ( "/hello" ) public String hello(String name) { return "User服務返回值:Hello " + String.valueOf(name); } public static void main(String[] args) { SpringApplication.run(Starter. class , args); } } |
服務注冊并調用
將服務注冊到注冊中心上,通過調用hello服務
1
2
3
4
5
6
|
@Autowired public HelloService helloService; @RequestMapping ( "/hello" ) public String hello(String name) { return helloService.hello(name); } |
通過日志可以發現,SpringCloud集成OpenFeign的POST請求確實是直接將參數掛在URL上,如下圖:
正是因為這個巨坑,導致了線上服務器經常性高CPU負載預警。
問題解決
問題知道了,那么就好解決了,用SpringCloud官方的說法是可以使用@RequestBody來解決這個問題,但是@RequestBody的使用是有限制的,也就是參數只能有一個,而且需要整個調用鏈路都做相應的調整,這個代價有點高,這里不采用這種方式,而是采用RequestInterceptor來處理。
編寫RequestInterceptor
這里將request的queryLine取下來放在body中,需要注意的是,只有body沒有值的時候才能這么做。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class PostRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { if ( "post" .equalsIgnoreCase(template.method()) && template.body() == null ) { String query = template.queryLine(); template.queries( new HashMap<>()); if (StringUtils.hasText(query) && query.startsWith( "?" )) { template.body(query.substring( 1 )); } template.header( "Content-Type" , "application/x-www-form-urlencoded;charset=UTF-8" ); } } } |
配置RequestInterceptor
feign:
client:
config:
default:
requestInterceptors: cn.com.ava.yaolin.springcloud.demo.PostRequestInterceptor
在下圖可以看出請求參數不再掛在URL上了
@RequestBody的解決方案
問題雖然解決了,但采用的不是官方推薦的方案,這里將官方推薦的這種@RequestBody的解決方法也貼出來。 使用@RequestBody解決,需要4個步驟:
編寫請求參數Bean
1
2
3
4
5
6
7
8
9
|
public class HelloReqForm { private String name; public String getName() { return name; } public void setName(String name) { this .name = name; } } |
調整接口聲明
@PostMapping("/hello")
public String hello(@RequestBody HelloReqForm form);
調整服務調用
HelloReqForm form = new HelloReqForm();
form.setName(name);
return helloService.hello(form);
調整服務實現
@RequestMapping("/hello")
public String hello(@RequestBody HelloReqForm form) {
}
最終調用日志
涉及的Java類
最后列出一些涉及的java類:
- SpringMVCContract 服務接口
- RequestParamParameterProcessor 參數
- feign.RequestTemplate Rest請求構造器
- feign.Request 處理http請求
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:http://www.manongjc.com/detail/19-fxjynmrashzgysy.html