美文网首页Spring cloud
Spring Cloud 之 Feign 调用实例及异常分析

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

作者: 星光下的胖子 | 来源:发表于2018-11-06 01:17 被阅读120次

一、简介

基于 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>

相关文章

网友评论

    本文标题:Spring Cloud 之 Feign 调用实例及异常分析

    本文链接:https://www.haomeiwen.com/subject/htnjxqtx.html