Spring Cloud 之 Feign 调用实例及异常分析

一、简介

基于 Spring Cloud 的微服务架构,各个微服务之间通过 Feign 调用。所有微服务注册在 Eureka 上,Spring Cloud 将它集成在自己的子项目 spring-cloud-netflix 中,实现 Spring Cloud 的「服务发现」功能。
在 Spring Cloud Netflix 栈中,各个微服务都是以 HTTP 接口的形式暴露自身,因此在调用远程服务时就必须使用 HTTP 客户端。我们可以使用 JDK 原生的 URLConnection、Apache 的 Http Client、Netty 的异步 HTTP Client 以及 Spring 的 RestTemplate。当然,用起来最方便的当属 Feign 了。
Feign 是一种声明式、模板化的 HTTP 客户端,包含了 Ribbon 和 Hystrix,支持负载均衡和容错。在 Spring Cloud 中,创建接口并引用 @FeignClient 注解即可引用 Feign,以实现微服务间的远程调用。
Feign 工作原理:Spring Cloud 应用在启动时,先检查配置是否有@EnableFeignClients 注解,如果有该注解,则开启包扫描,扫描标有 @FeignClient 注解的接口,生成代理,并注册到 Spring 容器中。生成代理时 Feign 为每个接口方法创建一个 RequetTemplate 对象,该对象封装了 HTTP 请求需要的全部信息,包括请求参数名、请求方法等信息,Feign 的模板化就体现在这里。

二、Feign 调用实例

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-serviceportal-test-service 是两个注册到同一 Eureka 上的微服务项目,下面演示 paas-test-service 通过 Feign 远程调用 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. 定义调用接口

使用 @FeignClient(name = "服务名") 注解,来指定调用哪个服务。
@FeignClient 注解的常用属性如下:

  • name:指被调用的微服务名称,可省略。@FeignClient(name = "服务名") 亦可写作 @FeignClient( "服务名")。
  • value:和 name 互为别名,也是指被调用的微服务名称。@FeignClient(name = "服务名") 亦可写作 @FeignClient(value= "服务名")。
  • url:直接添加硬编码的路径。一般用于调试,可以手动指定 @FeignClient 调用的地址,此时被调用的服务可以不注册到 Eureka 中心上。
  • configuration:标明 FeignClient 的配置类,使用默认即可。

下面是 paas-test-service 项目中定义的调用接口,其中 @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 项目中被调用的方法,其中 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;
    }

注意两点:

  • 第一,请求方式、请求路径必须与被调用接口保持一致。
  • 第二,虽然 Feign 服务客户端中的接口名、返回对象可以任意定义,但对象中的属性类型和属性名必须与被调用接口保持一致。

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 调用

首先,在 paas-test-service 项目的 PortalTestController.java 类中的消费方法上、portal-test-service 项目的被调用方法上,分别打上断点,如图:


然后,网页上访问 paas-test-service 项目的消费方法:

http://172.21.11.79:9191/paas-test-service/v1/portal_test/users/list?id=1

可以见到,依次经过所设定的断点。也就是说,我们访问 paas-test-service 微服务,然后通过 Feign 的远程调用,实现了对 portal-test-service 的访问。如下图:



nice!页面成功返回数据,测试 Feign 完毕!

三、Feign 调用异常分析

Spring Cloud 之 Feign 作为 HTTP 客户端调用远程服务,常见的异常主要有以下两类。

1. feign.FeignException: status 404 reading

说明找不到被调用的方法,也就是你定义的 Feign 客户端接口与被调用接口不一致。要么是请求方式、请求路径不匹配,要么就是参数不匹配,只要认真核对,不难纠正错误。

下面举一个自己曾经犯错的例子:
portal-test-service 项目中,指定了 context-path 属性,调用 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

调用服务超时。有时候可能数据库数据量大或其他原因,使得远程调用的时间超过 Feign 的默认超时时间,便会抛出该异常。

下面演示一个导致该bug的例子:
往 sys_user 表中插入大量的用户数据,然后访问 paas-test-service,报错如下:


通过设置 Feign 的超时时间可解决问题。Feign 的调用分两层,Ribbon 的调用和 Hystrix 的调用,高版本的 Hystrix 默认是关闭的,所以设置 Ribbon 即可。
(了解更多请参考『Feign 配置详解』。)
配置文件中添加配置如下:

#请求处理的超时时间
#ribbon.ReadTimeout: 120000
portal-test-service.ribbon.ReadTimeout: 120000
#请求连接的超时时间
#ribbon.ConnectTimeout: 30000
portal-test-service.ribbon.ConnectTimeout: 30000

重启项目,再次访问接口,返回数据成功。Perfect!



--------------------------------------我是华丽的分割线--------------------------------------
补充异常

3. status 404 reading 之 Request method 'POST' not supported。

使用 Feign 远程调用 Get 请求不支持通过 @RequestBody 注解传递参数导致。
添加 feign-httpclient 依赖即可(亲测有效,详情参见 「'POST' not supported 」)。

         <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-httpclient</artifactId>
        </dependency>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容