一、簡介
基于 Spring Cloud 的微服務(wù)架構(gòu)星著,各個微服務(wù)之間通過 Feign 調(diào)用。所有微服務(wù)注冊在 Eureka 上,Spring Cloud 將它集成在自己的子項目 spring-cloud-netflix 中甘有,實現(xiàn) Spring Cloud 的「服務(wù)發(fā)現(xiàn)」功能。
在 Spring Cloud Netflix 棧中葡缰,各個微服務(wù)都是以 HTTP 接口的形式暴露自身亏掀,因此在調(diào)用遠程服務(wù)時就必須使用 HTTP 客戶端忱反。我們可以使用 JDK 原生的 URLConnection、Apache 的 Http Client滤愕、Netty 的異步 HTTP Client 以及 Spring 的 RestTemplate温算。當然,用起來最方便的當屬 Feign 了间影。
Feign 是一種聲明式注竿、模板化的 HTTP 客戶端,包含了 Ribbon 和 Hystrix魂贬,支持負載均衡和容錯巩割。在 Spring Cloud 中,創(chuàng)建接口并引用 @FeignClient 注解即可引用 Feign付燥,以實現(xiàn)微服務(wù)間的遠程調(diào)用宣谈。
Feign 工作原理:Spring Cloud 應用在啟動時,先檢查配置是否有@EnableFeignClients 注解键科,如果有該注解闻丑,則開啟包掃描,掃描標有 @FeignClient 注解的接口勋颖,生成代理嗦嗡,并注冊到 Spring 容器中。生成代理時 Feign 為每個接口方法創(chuàng)建一個 RequetTemplate 對象牙言,該對象封裝了 HTTP 請求需要的全部信息酸钦,包括請求參數(shù)名、請求方法等信息咱枉,F(xiàn)eign 的模板化就體現(xiàn)在這里卑硫。
二、Feign 調(diào)用實例
portal-test-service
項目配置:
spring:
profiles:
active: test
application:
name: portal-test-service
version: 1.0.0
eureka:
client:
service-url:
defaultZone: http://172.21.11.79:9091/eureka
status:
open: true
instance:
preferIpAddress: true
instance-id: ${spring.cloud.client.ipAddress}:${server.port}
leaseRenewalIntervalInSeconds: 1
leaseExpirationDurationInSeconds: 2
server:
port: 9990
paas-test-service
項目配置:
spring:
profiles:
active: test
application:
name: paas-test-service
version: 1.0.0
eureka:
status:
open: ture
client:
service-url:
defaultZone: http://172.21.11.79:9091/eureka
instance:
preferIpAddress: true
instance-id: ${spring.cloud.client.ipAddress}:${server.port}
leaseRenewalIntervalInSeconds: 1
leaseExpirationDurationInSeconds: 2
server:
port: 8890
paas-test-service
和 portal-test-service
是兩個注冊到同一 Eureka 上的微服務(wù)項目蚕断,下面演示 paas-test-service
通過 Feign 遠程調(diào)用 portal-test-service
中的接口欢伏。
1. 添加依賴
在 paas-test-service
的 pom.xml 文件中添加 spring-cloud-starter-feign
依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
如圖所示:
2. 開啟 Feign
在 paas-test-service
項目的啟動類中,通過@EnableFeignClients 注解開啟 Feign 的功能:
3. 定義調(diào)用接口
使用 @FeignClient(name = "服務(wù)名") 注解亿乳,來指定調(diào)用哪個服務(wù)硝拧。
@FeignClient 注解的常用屬性如下:
- name:指被調(diào)用的微服務(wù)名稱,可省略葛假。@FeignClient(name = "服務(wù)名") 亦可寫作 @FeignClient( "服務(wù)名")障陶。
- value:和 name 互為別名,也是指被調(diào)用的微服務(wù)名稱聊训。@FeignClient(name = "服務(wù)名") 亦可寫作 @FeignClient(value= "服務(wù)名")抱究。
- url:直接添加硬編碼的路徑。一般用于調(diào)試带斑,可以手動指定 @FeignClient 調(diào)用的地址鼓寺,此時被調(diào)用的服務(wù)可以不注冊到 Eureka 中心上勋拟。
- configuration:標明 FeignClient 的配置類,使用默認即可妈候。
下面是 paas-test-service
項目中定義的調(diào)用接口敢靡,其中 @GetMapping 注解與 @RequestMapping 注解兩種寫法均可:
@FeignClient(name = "portal-test-service")
public interface IPortalInterface {
//@RequestMapping(value = "/system/v1/SysUserWsg/queryList", method = RequestMethod.GET)
@GetMapping("/system/v1/SysUserWsg/queryList")
@ResponseBody
PortalResult queryListByObj(@RequestParam("id") String id);
}
示例如圖:
下面是 portal-test-service
項目中被調(diào)用的方法,其中 PortalResult 對象與 WsgResult 對象屬性一致:
@RequestMapping(value = "/system/v1/SysUserWsg/queryList", method = RequestMethod.GET)
@ResponseBody
public WsgResult queryListByObj(@RequestParam String id) {
SysUserDTO sysUserDTO = BeanConvertor.getCopyObject(SysUserDTO.class, new SysUserVO());
WsgResult restRe = new WsgResult();
List<SysUserDTO> list = new ArrayList<SysUserDTO>();
try {
list = sysUserAppImpl.queryListByObj(sysUserDTO);
} catch (PortalBaseException e) {
e.printStackTrace();
restRe.setRetCode(e.getRetCode());
restRe.setRetMsg(e.getRetMsg());
}
restRe.setData(new AppData(list));
return restRe;
}
注意兩點:
- 第一苦银,請求方式啸胧、請求路徑必須與被調(diào)用接口保持一致。
- 第二幔虏,雖然 Feign 服務(wù)客戶端中的接口名吓揪、返回對象可以任意定義,但對象中的屬性類型和屬性名必須與被調(diào)用接口保持一致所计。
4. 添加消費方法
聲明接口之后,在代碼中通過 @Resource 或 @Autowired 注入即可使用团秽。
在 paas-test-service
項目中主胧,新建一個 PortalTestController.java 類,引用 @Resource 注解引入上面定義的 IPortalInterface 接口习勤,代碼示例如下:
5. 啟動項目
本地啟動這兩個項目踪栋,啟動成功如下:
2018-11-05 23:33:20.142 [main] INFO [org.apache.coyote.http11.Http11NioProtocol] - Initializing ProtocolHandler ["http-nio-9990"]
2018-11-05 23:33:20.198 [main] INFO [org.apache.coyote.http11.Http11NioProtocol] - Starting ProtocolHandler ["http-nio-9990"]
2018-11-05 23:33:20.249 [main] INFO [org.apache.tomcat.util.net.NioSelectorPool] - Using a shared selector for servlet write/read
2018-11-05 23:33:20.448 [main] INFO [org.jboss.resteasy.resteasy_jaxrs.i18n] - RESTEASY002225: Deploying javax.ws.rs.core.Application: class com.sitech.fw.core.spring.boot.autoconfigure.ResteasyApplication
2018-11-05 23:33:20.451 [main] INFO [org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer] - Tomcat started on port(s): 9990 (http)
2018-11-05 23:33:20.461 [main] INFO [org.springframework.cloud.netflix.eureka.serviceregistry.EurekaAutoServiceRegistration] - Updating port to 9990
2018-11-05 23:33:20.477 [main] INFO [com.sitech.cmap.wsg.system.PortalLoginServiceApplication] - Started PortalLoginServiceApplication in 74.286 seconds (JVM running for 78.392)
登錄 Eureka 中心,可看到這兩個項目已成功注冊上去:
6. 測試 Feign 調(diào)用
首先图毕,在 paas-test-service
項目的 PortalTestController.java 類中的消費方法上夷都、portal-test-service
項目的被調(diào)用方法上,分別打上斷點予颤,如圖:
然后囤官,網(wǎng)頁上訪問
paas-test-service
項目的消費方法:
http://172.21.11.79:9191/paas-test-service/v1/portal_test/users/list?id=1
可以見到,依次經(jīng)過所設(shè)定的斷點蛤虐。也就是說党饮,我們訪問 paas-test-service
微服務(wù),然后通過 Feign 的遠程調(diào)用驳庭,實現(xiàn)了對 portal-test-service
的訪問刑顺。如下圖:
nice!頁面成功返回數(shù)據(jù)饲常,測試 Feign 完畢蹲堂!
三、Feign 調(diào)用異常分析
Spring Cloud 之 Feign 作為 HTTP 客戶端調(diào)用遠程服務(wù)贝淤,常見的異常主要有以下兩類柒竞。
1. feign.FeignException: status 404 reading
說明找不到被調(diào)用的方法,也就是你定義的 Feign 客戶端接口與被調(diào)用接口不一致霹娄。要么是請求方式能犯、請求路徑不匹配鲫骗,要么就是參數(shù)不匹配,只要認真核對踩晶,不難糾正錯誤执泰。
下面舉一個自己曾經(jīng)犯錯的例子:
在 portal-test-service
項目中,指定了 context-path 屬性渡蜻,調(diào)用 portal-test-service
接口會加上 /portalWsg
前綴术吝。
server:
port: 9990
context-path: /portalWsg
然而,我在定義 Feign 接口的時候茸苇,忘記加上 /portalWsg
前綴排苍,代碼如下:
@FeignClient(name = "portal-test-service")
public interface IPortalInterface {
@RequestMapping(value = "/system/v1/SysUserWsg/queryList", method = RequestMethod.GET)
@ResponseBody
PortalResult queryListByObj(@RequestParam("id") String id);
}
于是報錯,截取部分信息如下:
feign.FeignException: status 404 reading IPortalInterface#queryListByObj(String)
at feign.FeignException.errorStatus(FeignException.java:62) ~[feign-core-9.5.0.jar:?]
at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:91) ~[feign-core-9.5.0.jar:?]
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:138) ~[feign-core-9.5.0.jar:?]
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:76) ~[feign-core-9.5.0.jar:?]
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103) ~[feign-core-9.5.0.jar:?]
at com.sun.proxy.$Proxy296.queryListByObj(Unknown Source) ~[?:?]
at com.sitech.cmap.paasplatform.wsg.controller.workorder.PortalTestController.listUsers(PortalTestController.java:33) ~[classes/:?]
at com.sitech.cmap.paasplatform.wsg.controller.workorder.PortalTestController$$FastClassBySpringCGLIB$$b7108cc2.invoke(<generated>) ~[classes/:?]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.11.RELEASE.jar:4.3.11.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:738) ~[spring-aop-4.3.11.RELEASE.jar:4.3.11.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.11.RELEASE.jar:4.3.11.RELEASE]
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:85) ~[spring-aop-4.3.11.RELEASE.jar:4.3.11.RELEASE]
at com.sitech.cmap.wsg.common.aspect.WsgResultAspect.handlerControllerMethod(WsgResultAspect.java:36) [wsg-extension-3.1.0-SNAPSHOT.jar:?]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_91]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_91]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:629) [spring-aop-4.3.11.RELEASE.jar:4.3.11.RELEASE]
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:618) [spring-aop-4.3.11.RELEASE.jar:4.3.11.RELEASE]
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70) [spring-aop-4.3.11.RELEASE.jar:4.3.11.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) [spring-aop-4.3.11.RELEASE.jar:4.3.11.RELEASE]
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) [spring-aop-4.3.11.RELEASE.jar:4.3.11.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) [spring-aop-4.3.11.RELEASE.jar:4.3.11.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:673) [spring-aop-4.3.11.RELEASE.jar:4.3.11.RELEASE]
at com.sitech.cmap.paasplatform.wsg.controller.workorder.PortalTestController$$EnhancerBySpringCGLIB$$dd10d04f.listUsers(<generated>) [classes/:?]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_91]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_91]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
路徑同時加上 /portalWsg
前綴学密,問題便得到解決淘衙!
@FeignClient(name = "portal-test-service")
public interface IPortalInterface {
@RequestMapping(value = "/portalWsg/system/v1/SysUserWsg/queryList", method = RequestMethod.GET)
@ResponseBody
PortalResult queryListByObj(@RequestParam("id") String id);
}
2. Read timed out executing
調(diào)用服務(wù)超時。有時候可能數(shù)據(jù)庫數(shù)據(jù)量大或其他原因腻暮,使得遠程調(diào)用的時間超過 Feign 的默認超時時間彤守,便會拋出該異常。
下面演示一個導致該bug的例子:
往 sys_user 表中插入大量的用戶數(shù)據(jù)哭靖,然后訪問 paas-test-service
具垫,報錯如下:
通過設(shè)置 Feign 的超時時間可解決問題。Feign 的調(diào)用分兩層试幽,Ribbon 的調(diào)用和 Hystrix 的調(diào)用筝蚕,高版本的 Hystrix 默認是關(guān)閉的,所以設(shè)置 Ribbon 即可铺坞。
(了解更多請參考『Feign 配置詳解』起宽。)
配置文件中添加配置如下:
#請求處理的超時時間
#ribbon.ReadTimeout: 120000
portal-test-service.ribbon.ReadTimeout: 120000
#請求連接的超時時間
#ribbon.ConnectTimeout: 30000
portal-test-service.ribbon.ConnectTimeout: 30000
重啟項目,再次訪問接口康震,返回數(shù)據(jù)成功燎含。Perfect!
--------------------------------------我是華麗的分割線--------------------------------------
補充異常
3. status 404 reading 之 Request method 'POST' not supported腿短。
使用 Feign 遠程調(diào)用 Get 請求不支持通過 @RequestBody 注解傳遞參數(shù)導致屏箍。
添加 feign-httpclient 依賴即可(親測有效,詳情參見 「'POST' not supported 」)橘忱。
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>