这篇文章主要讲述如何通过Feign去消费服务,以及Feign的实现原理的解析。
Feign是Netflix开发的声明式、模板化的HTTP客户端,Feign可以帮助我们更快捷、优雅地调用HTTP API。
Feign是⼀个HTTP请求的轻量级客户端框架。通过 接口 + 注解的方式发起HTTP请求调用,面向接口编程,而不是像Java中通过封装HTTP请求报文的方式直接调用。服务消费方拿到服务提供方的接⼝,然后像调⽤本地接⼝⽅法⼀样去调⽤,实际发出的是远程的请求。让我们更加便捷和优雅的去调⽤基于 HTTP的 API,被⼴泛应⽤在 Spring Cloud的解决⽅案中
Feign 采用的是基于接口的注解Feign 整合了Ribbon,具有负载均衡的能力整合了Hystrix,具有熔断的能力 1.2 为什么使用Feign
Feign 的首要目标就是减少HTTP 调用的复杂性。在微服务调用的场景中,我们调用很多时候都是基于HTTP协议的服务,如果服务调用只使用提供 HTTP调用服务的 HTTP Client框架(e.g. Apache HttpComponnets、HttpURLConnection OkHttp 等),我们需要关注哪些问题呢?
相比这些 HTTP请求框架,Feign封装了HTTP 请求调用的流程,而且会强制使用者去养成面向接口编程的习惯(因为 Feign 本身就是要面向接口)。
以获取Feign的 GitHub开源项目的 Contributors 为例,原生方式使用 Feign 步骤有如下三步:
1、第一步: 引入相关依赖
com.netflix.feign feign-core 8.18.0 runtime com.netflix.feign feign-jackson 8.18.0 com.netflix.feign feign-slf4j 8.18.0
2、第二步: 声明 HTTP 请求接口
使用Java的接口和 Feign的原生注解 @RequestLine声明 HTTP 请求接口,从这里就可以看到 Feign 给使用者封装了HTTP的调用细节,极大的减少了HTTP调用的复杂性,只要定义接口即可。
第三步: 配置初始化 Feign 客户端
最后一步配置初始化客户端,这一步主要是设置请求地址、编码(Encoder)、解码(Decoder)等。
通过定义接口,使用注解的方式描述接口的信息,就可以发起接口调用。最后请求结果如下:
新建一个spring-boot工程,取名为serice-feign,在它的pom文件引入Feign的起步依赖spring-cloud-starter-feign、Eureka的起步依赖spring-cloud-starter-netflix-eureka-client、Web的起步依赖spring-boot-starter-web,代码如下:
第一步: 引入相关 starter 依赖
4.0.0 com.forezp service-feign 0.0.1-SNAPSHOT jar service-feign Demo project for Spring Boot com.forezp sc-f-chapter3 0.0.1-SNAPSHOT org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-openfeign
在工程的配置文件application.yml文件,指定程序名为service-feign,端口号为8765,服务注册地址为http://localhost:8761/eureka/,代码如下:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 8765
spring:
application:
name: service-feign
在程序的启动类ServiceFeignApplication ,加上@EnableFeignClients注解开启Feign客户端功能:
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@EnableFeignClients
public class ServiceFeignApplication {
public static void main(String[] args) {
SpringApplication.run( ServiceFeignApplication.class, args );
}
}
定义一个feign接口,通过@ FeignClient(“服务名”),来指定调用哪个服务。比如在代码中调用了service-hi服务的“/hi”接口,代码如下:
@FeignClient(value = "service-hi")
public interface SchedualServiceHi {
@RequestMapping(value = "/hi",method = RequestMethod.GET)
String sayHiFromClientOne(@RequestParam(value = "name") String name);
}
官网给的示例图如下:
现在我们是通过 @FeignClient注解配置的Feign客户端属性,同时请求的 URL也是使用的 Spring MVC 提供的注解
在Web层的controller层,对外暴露一个"/hi"的API接口,通过上面定义的Feign客户端SchedualServiceHi来消费服务。代码如下所示:
@RestController
public class HiController {
//编译器报错,无视。 因为这个Bean是在程序启动的时候注入的,编译器感知不到,所以报错。
@Autowired
SchedualServiceHi schedualServiceHi;
@GetMapping(value = "/hi")
public String sayHi(@RequestParam String name) {
return schedualServiceHi.sayHiFromClientOne( name );
}
}
启动程序,多次访问http://localhost:8765/hi?name=forezp,浏览器交替显示:
1.3.3 参数处理hi forezp,i am from port:8762
hi forezp,i am from port:8763
在Feign处理远程服务调用时,传递参数是通过HTTP协议传递的,参数存在的位置是请求头或请求体中。请求头传递的参数必须依赖@RequestParam注解来处理请求参数,请求体传递的参数必须依赖@RequestBody注解来处理请求参数。
1.3.3.1 代码环境如下Contronller层通过feignClient调用微服务 获取所有任务
@Controller
@RequestMapping("tsa/task")
public class TaskController{
@Autowired
TaskFeignClient taskFeignClient;
@PostMapping("/getAll")
@ResponseBody
public List getAll() {
List all = taskFeignClient.getAll();
return all;
}
}
@FeignClient用于通知Feign组件对该接口进行代理(不需要编写接口实现),使用者可直接通过@Autowired注入。
Spring Cloud应用在启动时,Feign会扫描标有@FeignClient注解的接口,生成代理,并注册到Spring容器中。生成代理时Feign会为每个接口方法创建一个RequetTemplate对象,该对象封装了HTTP请求需要的全部信息,请求参数名、请求方法等信息都是在这个过程中确定的,Feign的模板化就体现在这里。
@FeignClient(qualifier = "taskFeignClient", name = "service-tsa",fallback = TaskFeignClientDegraded.class)
public interface TaskFeignClient {
@PostMapping(value = "taskApiController/getAll")
List getAll();
}
微服务端
@Slf4j
@RestController
@RequestMapping("taskApiController")
public class TaskApiController{
@Autowired
private TaskService taskService;
@PostMapping("/getAll")
public List getAll() {
log.info("--------getAll-----");
List all = taskService.getAll();
return all;
}
}
1.3.3.2 几个坑
1、坑一
首先再次强调Feign是通过http协议调用服务的,重点是要理解这句话
如果FeignClient中的方法有@PostMapping注解 则微服务TaskApiController中对应方法的注解也应当保持一致为@PostMapping
如果不一致,则会报404的错误
调用失败后会触发它的熔断机制,如果@FeignClient中不写@FeignClient(fallback = TaskFeignClientDegraded.class),会直接报错:
11:00:35.686 [http-apr-8086-exec-8] DEBUG c.b.p.m.b.c.AbstractbaseController - Got an exception
com.netflix.hystrix.exception.HystrixRuntimeException: TaskFeignClient#getAll() failed and no fallback available.
at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:819)
at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:804)
2、坑2:这个是最惨的了
自己写好的微服务没有运行起来,然后自己的客户端调用这个服务怎么也调用不成功还不知道问题在哪,当时自己微服务运行后,控制台如下:
Process finished with exit code 0
我以前以为Process finished with exit code 1才是运行失败的意思 ,所以只要出现 Process finished with exit code就说明运行失败
服务成功启动的标志为:
11:29:16.483 [restartedMain] INFO c.b.p.ms.tsa.TsaServiceApplication - Started TsaServiceApplication in 37.132 seconds (JVM running for 39.983)
3、坑3、RequestParam.value() was empty on parameter 0
如果在FeignClient中的方法有参数传递一般要加@RequestParam(“xxx”)注解
错误写法:
@FeignClient(qualifier = "taskFeignClient", name = "service-tsa",fallback = TaskFeignClientDegraded.class)
public interface TaskFeignClient {
@PostMapping(value = "taskApiController/getAll")
List getAll(String name);
}
或
@PostMapping(value = "taskApiController/getAll") ListgetAll(@RequestParam String name);
正确写法:
@PostMapping(value = "taskApiController/getAll") ListgetAll(@RequestParam("name") String name);
在微服务那边可以不写这个注解,这个也是自己开发的时候烦的小错误,吸取教训。
疑问
在 SpringMVC 和 Springboot 中都可以使用 @RequestParam 注解,不指定 value的用法,为什么到了 Spring cloud中的Feign 这里就不行了呢?
这是因为和 Feign的实现有关。Feign 的底层使用的是httpclient,在低版本中会产生这个问题,听说高版本中已经对这个问题修复了。
4、 坑四 FeignClient中post传递对象和`consumes = "application/json"
按照坑三的意思,应该这样写
@FeignClient(qualifier = "taskFeignClient", name = "service-tsa",fallback = TaskFeignClientDegraded.class)
public interface TaskFeignClient {
@PostMapping(value = "taskApiController/getAll")
List getAll(@RequestParam("vo") TaskVO vo);
}
很意外报错
16:00:33.770 [http-apr-8086-exec-1] DEBUG c.b.p.a.s.PrimusCasAuthenticationFilter - proxyReceptorRequest = false
16:00:33.770 [http-apr-8086-exec-1] DEBUG c.b.p.a.s.PrimusCasAuthenticationFilter - proxyTicketRequest = false
16:00:33.770 [http-apr-8086-exec-1] DEBUG c.b.p.a.s.PrimusCasAuthenticationFilter - requiresAuthentication = false
16:00:34.415 [hystrix-service-tsa-2] DEBUG c.b.p.m.b.f.PrimusSoaFeignErrorDecoder -
error json:{
"timestamp":1543564834395,
"status":500,
"error":"Internal Server Error",
"exception":"org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException",
"message":"Failed to convert value of type 'java.lang.String' to required type 'com.model.tsa.vo.TaskVO';
nested exception is java.lang.IllegalStateException:
Cannot convert value of type 'java.lang.String' to required type 'com.model.tsa.vo.TaskVO':
no matching editors or conversion strategy found","path":"/taskApiController/getAll" }
看着错误信息想了半天突然想明白了:
Feign本质是通过http 请求的,http怎么能直接传递对象呢,一般都是把对象转换为json通过post请求传递的。
正确写法应当如下
@FeignClient(qualifier = "taskFeignClient", name = "service-tsa",fallback = TaskFeignClientDegraded.class)
public interface TaskFeignClient {
@PostMapping(value = "taskApiController/getAll",,consumes = "application/json")
List getAll(TaskVO vo);
}
也可以这样写
@PostMapping(value = "taskApiController/getAll") ListgetAll(@RequestBody TaskVO vo);
此时不用,consumes = "application/json"
但是第一种写法最正确的 因为FeignClient是在我们本地直接调用的,根本不需要这个注解,Controller调用方法的时候就是直接将对象传给FeignClient,而FeignClient通过http调用服务时则需要将对象转换成json传递。
微服务代码如下所示:
@Slf4j
@RestController
@RequestMapping("taskApiController")
public class TaskApiController{
@Autowired
private TaskService taskService;
@PostMapping("/getAll")
public List getAll(@RequestBody TaskVO vo) {
log.info("--------getAll-----");
List all = taskService.getAll();
return all;
}
}
我第一次写这个的时候方法参数里面什么注解都没加,可以正常跑通,但是传过去的对象却为初始值,实际上那是因为对象根本就没传。
当然还是推荐使用post请求传递对象的:
在使用Feign来调用Get请求接口时,如果方法的参数是一个对象,例如:
@FeignClient ( "microservice-provider-user" )
public interface UserFeignClient {
@RequestMapping (value = "/user" , method = RequestMethod.GET)
public User get0(User user);
}
那么在调试的时候你会一脸懵逼,因为报了如下错误:
feign.FeignException: status 405 reading UserFeignClient#get0(User); content:
{ "timestamp" : 1482676142940 , "status" : 405 , "error" : "Method Not Allowed" , "exception" : "org.springframework.web.HttpRequestMethodNotSupportedException" , "message" : "Request method 'POST' not supported" , "path" : "/user" }
明明定义的Get请求,怎么被转换成了Post?
调整不用对象传递,一切OK,没毛病,可仔细想想,你想写一堆长长的参数吗?用一个不知道里边有什么鬼的Map吗?或者转换为post?这似乎与REST风格不太搭,会浪费url资源,我们还需要在url定义上来区分Get或者Post。
我很好奇,我定义的Get请求怎么就被转成了Post,于是就开始逐行调试,直到我发现了这个:
private synchronized OutputStream getOutputStream0() throws IOException {
try {
if (! this .doOutput) {
throw new ProtocolException( "cannot write to a URLConnection if doOutput=false - call setDoOutput(true)" );
} else {
if ( this .method.equals( "GET" )) {
this .method = "POST" ;
}
这段代码是在 HttpURLConnection 中发现的,jdk原生的http连接请求工具类,这个是Feign默认使用的连接工具实现类,但我记得我们的工程用的是apach的httpclient替换掉了原生的UrlConnection,我们用了如下配置:
feign:
httpclient:
enabled: true
同时在依赖中引入apache的httpclient
org.apache.httpcomponents httpclient 4.5.3
发现我们少配置了一个依赖:
com.netflix.feign feign-httpclient ${feign-httpclient}
那我加上这个依赖后,请求通了,但是接口接收到对象里边属性值是NULL;再看下边的定义是不是少点什么
@RequestMapping (value = "/user" , method = RequestMethod.GET) public User get0(User user);
对,少了一个注解:@RequestBody,既然使用对象传递参数,那传入的参数会默认放在RequesBody中,所以在接收的地方需要使用@RequestBody来解析,最终就是如下定义:
@RequestMapping (value = "/user" , method = RequestMethod.GET,consumer="application/json") public User get0( @RequestBody User user);1.3.3.3 传递对象的另一种方法和多参传递
1、GET请求多参数的URL
假设我们请求的URL包含多个参数,例如http://microservice-provider-user/get?id=1&username=张三 ,要怎么办呢?
我们知道Spring Cloud为Feign添加了Spring MVC的注解支持,那么我们不妨按照Spring MVC的写法尝试一下:
@FeignClient("microservice-provider-user")
public interface UserFeignClient {
@RequestMapping(value = "/get", method = RequestMethod.GET)
public User get0(User user);
}
然而我们测试时会发现该写法不正确,我们将会收到类似以下的异常:
feign.FeignException: status 405 reading UserFeignClient#get0(User); content:
{"timestamp":1482676142940,"status":405,"error":"Method Not Allowed","exception":"org.springframework.web.HttpRequestMethodNotSupportedException","message":"Request method 'POST' not supported","path":"/get"}
由异常可知,尽管指定了GET方法,Feign依然会发送POST请求。
正确写法如下:
(1) 方法一
@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
@RequestMapping(value = "/get", method = RequestMethod.GET)
public User get1(@RequestParam("id") Long id, @RequestParam("username") String username);
}
这是最为直观的方式,URL有几个参数,Feign接口中的方法就有几个参数。使用@RequestParam注解指定请求的参数是什么。
(2) 方法二
@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
@RequestMapping(value = "/get", method = RequestMethod.GET)
public User get2(@RequestParam Map map);
}
多参数的URL也可以使用Map去构建
当目标URL参数非常多的时候,可使用这种方式简化Feign接口的编写。
POST请求包含多个参数
下面我们来讨论如何使用Feign构造包含多个参数的POST请求。
实际就是坑四,把参数封装成对象传递过去就可以了
1.3.3.4 最后总结一下Feign的Encoder、Decoder和ErrorDecoder
Feign将方法签名中方法参数对象序列化为请求参数放到HTTP请求中的过程,是由编码器(Encoder)完成的。同理,将HTTP响应数据反序列化为java对象是由解码器(Decoder)完成的。
默认情况下,Feign会将标有@RequestParam注解的参数转换成字符串添加到URL中,将没有注解的参数通过Jackson转换成json放到请求体中。
注意,如果在@RequetMapping中的method将请求方式指定为get,那么所有未标注解的参数将会被忽略,例如:
@RequestMapping(value = "/group/{groupId}", method = RequestMethod.GET)
void update(@PathVariable("groupId") Integer groupId, @RequestParam("groupName") String groupName, DataObject obj);
此时因为声明的是GET请求没有请求体,所以obj参数就会被忽略。
在Spring Cloud环境下,Feign的Encoder只会用来编码没有添加注解的参数。如果你自定义了Encoder, 那么只有在编码obj参数时才会调用你的Encoder。对于Decoder, 默认会委托给SpringMVC中的MappingJackson2HttpMessageConverter类进行解码。只有当状态码不在200 ~ 300之间时ErrorDecoder才会被调用。ErrorDecoder的作用是可以根据HTTP响应信息返回一个异常,该异常可以在调用Feign接口的地方被捕获到。我们目前就通过ErrorDecoder来使Feign接口抛出业务异常以供调用者处理。
Feign的HTTP Client
Feign在默认情况下使用的是JDK原生的URLConnection发送HTTP请求,没有连接池,但是对每个地址会保持一个长连接,即利用HTTP的persistence connection。我们可以用Apache的HTTP Client替换Feign原始的http client, 从而获取连接池、超时时间等与性能息息相关的控制能力。Spring Cloud从Brixtion.SR5版本开始支持这种替换,首先在项目中声明Apache HTTP Client和feign-httpclient依赖:
org.apache.httpcomponents
httpclient
com.netflix.feign
feign-httpclient
${feign-httpclient}
然后在application.properties中添加:
feign.httpclient.enabled=true
通过Feign, 我们能把HTTP远程调用对开发者完全透明,得到与调用本地方法一致的编码体验。这一点与阿里Dubbo中暴露远程服务的方式类似,区别在于Dubbo是基于私有二进制协议,而Feign本质上还是个HTTP客户端。如果是在用Spring Cloud Netflix搭建微服务,那么Feign无疑是最佳选择。
1.4 调用原理解析Feign远程调用,核心就是通过一系列的封装和处理,将以JAVA注解的方式定义的远程调用API接口,最终转换成HTTP的请求形式,然后将HTTP的请求的响应结果,解码成JAVA Bean,放回给调用者。Feign远程调用的基本流程,大致如下图所示。
从上图可以看到,Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的 Request请求。通过Feign以及JAVA的动态代理机制,使得Java开发人员,可以不用通过HTTP框架去封装HTTP请求报文的方式,完成远程服务的HTTP调用。
Feign优化
(1)GZIP压缩
gzip是一种数据格式,采用deflate算法压缩数据。当Gzip压缩到一个纯文本数据时,可以减少70%以上的数据大小。
gzip作用:网络数据经过压缩后实际上降低了网络传输的字节数,最明显的好处就是可以加快网页加载的速度。
只配置Feign请求-应答的GZIP压缩
# feign gzip # 局部配置。只配置feign技术相关的http请求-应答中的gzip压缩。 # 配置的是application client和application service之间通讯是否使用gzip做数据压缩。 # 和浏览器到application client之间的通讯无关。 # 开启feign请求时的压缩, application client -> application service feign.compression.request.enabled=true # 开启feign技术响应时的压缩, application service -> application client feign.compression.response.enabled=true # 设置可以压缩的请求/响应的类型。 feign.compression.request.mime-types=text/xml,application/xml,application/json # 当请求的数据容量达到多少的时候,使用压缩。默认是2048字节。 feign.compression.request.min-request-size=512 配置全局的GZIP压缩 # spring boot gzip # 开启spring boot中的gzip压缩。就是针对和当前应用所有相关的http请求-应答的gzip压缩。 server.compression.enabled=true # 哪些客户端发出的请求不压缩,默认是不限制 server.compression.excluded-user-agents=gozilla,traviata # 配置想压缩的请求/应答数据类型,默认是 text/html,text/xml,text/plain server.compression.mime-types=application/json,application/xml,text/html,text/xml,text/plain # 执行压缩的阈值,默认为2048 server.compression.min-response-size=512



