SpringCloud OpenFeign Post請求的坑

HiIT青年發表於2020-09-16

在微服務開發中SpringCloud全家桶整合了OpenFeign用於服務呼叫,SpringCloud的OpenFeign使用SpringMVCContract來解析OpenFeign的介面定義。 但是SpringMVCContract的Post介面解析實現有個巨坑,就是如果使用的是@RequestParam傳參的Post請求,引數是直接掛在URL上的。

問題發現與分析

最近線上伺服器突然經常性出現CPU高負載的預警,經過排查發現日誌出來了大量的OpenFeign跨服務呼叫出現400的錯誤(HTTP Status 400)。

一般有兩種情況:

  1. nginx 返回400
  2. java應用返回400

通過分析發現400是java應用返回的,那麼可以確定是OpenFeign客戶端發起跨服務請求時出現異常了。 但是檢視原始碼發現出現這個問題的服務介面非常簡單,就是一個只有三個引數的POST請求介面,這個錯誤並不是必現的錯誤,而是當引數值比較長(String)的時候才會出現。 所以可以初步確認可能是引數太長導致請求400,對於POST請求因引數太長導致400錯誤非常疑惑,POST請求除非把引數掛在URL上,否則不應該出現400才對。

問題排查

為了驗證上面的猜測,手寫了一個簡單的示例,在驗證過程中特意開啟了OpenFeign的debug日誌。

首先編寫服務介面

這是一個簡單的Post介面,僅有一個引數(這裡的程式碼僅用於驗證,非正式程式碼)

@FeignClient(name = "user-service-provider")
public interface HelloService {
    @PostMapping("/hello")
    public String hello(@RequestParam("name") String name);
}

編寫服務

服務這裡隨便寫一個Http介面即可(同上,程式碼僅用於驗證)

@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服務

@Autowired
public HelloService helloService;
	
@RequestMapping("/hello")
public String hello(String name) {
    return helloService.hello(name);
}

通過日誌可以發現,SpringCloud整合OpenFeign的POST請求確實是直接將引數掛在URL上,如下圖:

正是因為這個巨坑,導致了線上伺服器經常性高CPU負載預警。

問題解決

問題知道了,那麼就好解決了,用SpringCloud官方的說法是可以使用@RequestBody來解決這個問題,但是@RequestBody的使用是有限制的,也就是引數只能有一個,而且需要整個呼叫鏈路都做相應的調整,這個代價有點高,這裡不採用這種方式,而是採用RequestInterceptor來處理。

  1. 編寫RequestInterceptor

這裡將request的queryLine取下來放在body中,需要注意的是,只有body沒有值的時候才能這麼做。

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");
        }
    }
}
  1. 配置RequestInterceptor
feign:
  client:
    config:
      default:
        requestInterceptors: cn.com.ava.yaolin.springcloud.demo.PostRequestInterceptor
  1. 在下圖可以看出請求引數不再掛在URL上了

@RequestBody的解決方案

問題雖然解決了,但採用的不是官方推薦的方案,這裡將官方推薦的這種@RequestBody的解決方法也貼出來。 使用@RequestBody解決,需要4個步驟:

  1. 編寫請求引數Bean
public class HelloReqForm {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
  1. 調整介面宣告
@PostMapping("/hello")
public String hello(@RequestBody HelloReqForm form);
  1. 調整服務呼叫
HelloReqForm form = new HelloReqForm();
form.setName(name);
return helloService.hello(form);
  1. 調整服務實現
@RequestMapping("/hello")
public String hello(@RequestBody HelloReqForm form) {
}
  1. 最終呼叫日誌

涉及的Java類

最後列出一些涉及的java類:

  1. SpringMVCContract 服務介面解析
  2. RequestParamParameterProcessor 引數解析
  3. feign.RequestTemplate Rest請求構造器
  4. feign.Request 處理http請求

=========================================================

HiIT青年 關注公眾號,閱讀更多文章。

相關文章