美文网首页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