一、简介
基于 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-service
和 portal-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>