Spring Boot 和 Spring Cloud Feign呼叫服務及傳遞引數踩坑記錄
背景
在Spring Cloud Netflix棧中,各個微服務都是以HTTP介面的形式暴露自身服務的,因此在呼叫遠端服務時就必須使用HTTP客戶端。我們可以使用JDK原生的URLConnection、Apache的Http Client、Netty的非同步HTTP Client, Spring的RestTemplate。但是,用起來最方便、最優雅的還是要屬Feign了。
Feign是一種宣告式、模板化的HTTP客戶端
Contronller層通過feignClient呼叫微服務 獲取所有任務
@Controller
@RequestMapping("tsa/task")
public class TaskController{
@Autowired
TaskFeignClient taskFeignClient;
@PostMapping("/getAll")
@ResponseBody
public List<TaskVO> getAll() {
List<TaskVO> all = taskFeignClient.getAll();
return all;
}
}
@FeignClient
用於通知Feign元件對該介面進行代理(不需要編寫介面實現),使用者可直接通過@Autowired
注入。Spring Cloud應用在啟動時,Feign會掃描標有@FeignClient註解的介面,生成代理,並註冊到Spring容器中。生成代理時Feign會為每個介面方法建立一個RequetTemplate物件,該物件封裝了HTTP請求需要的全部資訊,請求引數名、請求方法等資訊都是在這個過程中確定的,Feign的模板化就體現在這裡。
@FeignClient(qualifier = "taskFeignClient", name = "service-tsa",fallback = TaskFeignClientDegraded.class) public interface TaskFeignClient { @PostMapping(value = "taskApiController/getAll") List<TaskVO> getAll(); }
微服務端
@Slf4j
@RestController
@RequestMapping("taskApiController")
public class TaskApiController{
@Autowired
private TaskService taskService;
@PostMapping("/getAll")
public List<TaskVO> getAll() {
log.info("--------getAll-----");
List<TaskVO> all = taskService.getAll();
return all;
}
}
坑1:
首先再次強調Feign是通過http協議呼叫服務的,重點是要理解這句話
如果FeignClient中的方法有@PostMapping註解 則微服務TaskApiController中對應方法的註解也應當保持一致為@PostMapping
若果不一致,則會報404的錯誤
呼叫失敗後會觸發它的熔斷機制,如果@FeignClient中不寫@FeignClient(fallback = TaskFeignClientDegraded.class),會直接報錯
11:00:35.686 [http-apr-8086-exec-8] DEBUG c.b.p.m.b.c.AbstractBaseController - Got an exception
com.netflix.hystrix.exception.HystrixRuntimeException: TaskFeignClient#getAll() failed and no fallback available.
at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:819)
at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:804)
坑2:這個是最慘的了
自己寫好的微服務沒有執行起來,然後自己的客戶端呼叫這個服務怎麼也呼叫不成功還不知道問題在哪
當時自己微服務執行後,控制檯如下
Process finished with exit code 0
我以前以為Process finished with exit code 1才是執行失敗的意思 ,
所以只要出現 Process finished with exit code 就說明執行失敗
服務成功啟動的標誌為
11:29:16.483 [restartedMain] INFO c.b.p.ms.tsa.TsaServiceApplication - Started TsaServiceApplication in 37.132 seconds (JVM running for 39.983)
坑3、RequestParam.value() was empty on parameter 0
如果在FeignClient中的方法有引數傳遞一般要加@RequestParam(“xxx”)註解
錯誤寫法
@FeignClient(qualifier = "taskFeignClient", name = "service-tsa",fallback = TaskFeignClientDegraded.class)
public interface TaskFeignClient {
@PostMapping(value = "taskApiController/getAll")
List<TaskVO> getAll(String name);
}
或
@PostMapping(value = "taskApiController/getAll")
List<TaskVO> getAll(@RequestParam String name);
正確寫法
@PostMapping(value = "taskApiController/getAll")
List<TaskVO> getAll(@RequestParam("name") String name);
微服務那邊可以不寫這個註解
參考 https://blog.csdn.net/jxm007love/article/details/80109974
疑問
在 SpringMVC 和 Springboot 中都可以使用 @RequestParam 註解,不指定 value 的用法,為什麼到了 Spring cloud 中的 Feign 這裡就不行了呢?
這是因為和 Feign 的實現有關。Feign 的底層使用的是 httpclient,在低版本中會產生這個問題,聽說高版本中已經對這個問題修復了。
- Fiegn Client with Spring Boot: RequestParam.value() was empty on parameter 0
- Feign bug when use @RequestParam but not have value
- https://www.xttblog.com/ 業餘草
坑四 FeignClient中post傳遞物件和consumes = "application/json"
按照坑三的意思,應該這樣寫
@FeignClient(qualifier = "taskFeignClient", name = "service-tsa",fallback = TaskFeignClientDegraded.class)
public interface TaskFeignClient {
@PostMapping(value = "taskApiController/getAll")
List<TaskVO> getAll(@RequestParam("vo") TaskVO vo);
}
很意外報錯
16:00:33.770 [http-apr-8086-exec-1] DEBUG c.b.p.a.s.PrimusCasAuthenticationFilter - proxyReceptorRequest = false
16:00:33.770 [http-apr-8086-exec-1] DEBUG c.b.p.a.s.PrimusCasAuthenticationFilter - proxyTicketRequest = false
16:00:33.770 [http-apr-8086-exec-1] DEBUG c.b.p.a.s.PrimusCasAuthenticationFilter - requiresAuthentication = false
16:00:34.415 [hystrix-service-tsa-2] DEBUG c.b.p.m.b.f.PrimusSoaFeignErrorDecoder -
error json:{
"timestamp":1543564834395,
"status":500,
"error":"Internal Server Error",
"exception":"org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException",
"message":"Failed to convert value of type 'java.lang.String' to required type 'com.model.tsa.vo.TaskVO';
nested exception is java.lang.IllegalStateException:
Cannot convert value of type 'java.lang.String' to required type 'com.model.tsa.vo.TaskVO':
no matching editors or conversion strategy found","path":"/taskApiController/getAll" }
開著錯誤資訊想了半天突然想明白了
Feign本質是通過http 請求的,http怎麼能直接傳遞物件呢,一般都是把物件轉換為json通過post請求傳遞的
正確寫法應當如下
@FeignClient(qualifier = "taskFeignClient", name = "service-tsa",fallback = TaskFeignClientDegraded.class)
public interface TaskFeignClient {
@PostMapping(value = "taskApiController/getAll",,consumes = "application/json")
List<TaskVO> getAll(TaskVO vo);
}
也可以這樣寫
@PostMapping(value = "taskApiController/getAll",consumes = "application/json")
List<TaskVO> getAll(@RequestBody TaskVO vo);
但是第一種寫法最正確的 因為FeignClient是在我們本地直接呼叫的,根本不需要這個註解,Controller呼叫方法的時候就是直接將物件傳給FeignClient,而FeignClient通過http呼叫服務時則需要將物件轉換成json傳遞。
微服務這邊如下
@Slf4j
@RestController
@RequestMapping("taskApiController")
public class TaskApiController{
@Autowired
private TaskService taskService;
@PostMapping("/getAll")
public List<TaskVO> getAll(@RequestBody TaskVO vo) {
log.info("--------getAll-----");
List<TaskVO> all = taskService.getAll();
return all;
}
}
我第一次寫這個的時候方法引數裡面什麼註解都沒加,可以正常跑通,但是傳過去的物件卻為初始值,實際上那是因為物件根本就沒傳
如果用get方法傳遞物件呢?
https://blog.csdn.net/cuiyaoqiang/article/details/81215483
https://blog.csdn.net/u014281502/article/details/72896182
參考 https://blog.csdn.net/neosmith/article/details/52449921
當然還是推薦使用post請求傳遞物件的
5、傳遞物件的另一種方法和多參傳遞
來源:https://www.jianshu.com/p/7ce46c0ebe9d
http://www.itmuch.com/spring-cloud-sum/feign-multiple-params/
1、GET請求多引數的URL
假設我們請求的URL包含多個引數,例如http://microservice-provider-user/get?id=1&username=張三 ,要怎麼辦呢?
我們知道Spring Cloud為Feign添加了Spring MVC的註解支援,那麼我們不妨按照Spring MVC的寫法嘗試一下:
@FeignClient("microservice-provider-user")
public interface UserFeignClient {
@RequestMapping(value = "/get", method = RequestMethod.GET)
public User get0(User user);
}
然而我們測試時會發現該寫法不正確,我們將會收到類似以下的異常:
feign.FeignException: status 405 reading UserFeignClient#get0(User); content:
{"timestamp":1482676142940,"status":405,"error":"Method Not Allowed","exception":"org.springframework.web.HttpRequestMethodNotSupportedException","message":"Request method 'POST' not supported","path":"/get"}
由異常可知,儘管指定了GET方法,Feign依然會發送POST請求。
正確寫法如下:
(1) 方法一
@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
@RequestMapping(value = "/get", method = RequestMethod.GET)
public User get1(@RequestParam("id") Long id, @RequestParam("username") String username);
}
這是最為直觀的方式,URL有幾個引數,Feign介面中的方法就有幾個引數。使用@RequestParam註解指定請求的引數是什麼。
(2) 方法二
@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
@RequestMapping(value = "/get", method = RequestMethod.GET)
public User get2(@RequestParam Map<String, Object> map);
}
多引數的URL也可以使用Map去構建
當目標URL引數非常多的時候,可使用這種方式簡化Feign介面的編寫。
POST請求包含多個引數
下面我們來討論如何使用Feign構造包含多個引數的POST請求。
實際就是坑四,把引數封裝成物件傳遞過去就可以了
最後總結一下
Feign的Encoder、Decoder和ErrorDecoder
Feign將方法簽名中方法引數物件序列化為請求引數放到HTTP請求中的過程,是由編碼器(Encoder)完成的。同理,將HTTP響應資料反序列化為java物件是由解碼器(Decoder)完成的。
預設情況下,Feign會將標有@RequestParam註解的引數轉換成字串新增到URL中,將沒有註解的引數通過Jackson轉換成json放到請求體中。
注意,如果在@RequetMapping中的method將請求方式指定為get,那麼所有未標註解的引數將會被忽略,例如:
@RequestMapping(value = "/group/{groupId}", method = RequestMethod.GET)
void update(@PathVariable("groupId") Integer groupId, @RequestParam("groupName") String groupName, DataObject obj);
此時因為宣告的是GET請求沒有請求體,所以obj引數就會被忽略。
在Spring Cloud環境下,Feign的Encoder*只會用來編碼沒有添加註解的引數*。如果你自定義了Encoder, 那麼只有在編碼obj引數時才會呼叫你的Encoder。對於Decoder, 預設會委託給SpringMVC中的MappingJackson2HttpMessageConverter類進行解碼。只有當狀態碼不在200 ~ 300之間時ErrorDecoder才會被呼叫。ErrorDecoder的作用是可以根據HTTP響應資訊返回一個異常,該異常可以在呼叫Feign介面的地方被捕獲到。我們目前就通過ErrorDecoder來使Feign介面丟擲業務異常以供呼叫者處理。
Feign的HTTP Client
https://blog.csdn.net/neosmith/article/details/52449921
Feign在預設情況下使用的是JDK原生的URLConnection傳送HTTP請求,沒有連線池,但是對每個地址會保持一個長連線,即利用HTTP的persistence connection 。我們可以用Apache的HTTP Client替換Feign原始的http client, 從而獲取連線池、超時時間等與效能息息相關的控制能力。Spring Cloud從Brixtion.SR5版本開始支援這種替換,首先在專案中宣告Apache HTTP Client和feign-httpclient依賴:
<!-- 使用Apache HttpClient替換Feign原生httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>${feign-httpclient}</version>
</dependency>
然後在application.properties中新增:
feign.httpclient.enabled=true
通過Feign, 我們能把HTTP遠端呼叫對開發者完全透明,得到與呼叫本地方法一致的編碼體驗。這一點與阿里Dubbo中暴露遠端服務的方式類似,區別在於Dubbo是基於私有二進位制協議,而Feign本質上還是個HTTP客戶端。如果是在用Spring Cloud Netflix搭建微服務,那麼Feign無疑是最佳選擇。