1.1 Feign
概述
这篇文章主要讲述如何通过Feign
去消费服务,以及Feign
的实现原理的解析。
Feign
是Netflix
开发的声明式、模板化的HTTP
客户端,Feign
可以帮助我们更快捷、优雅地调用HTTP API
。
Feign
是⼀个HTTP
请求的轻量级客户端框架。通过 接口 + 注解的方式发起HTTP
请求调用,面向接口编程,而不是像Java
中通过封装HTTP
请求报文的方式直接调用。服务消费方拿到服务提供方的接⼝,然后像调⽤本地接⼝⽅法⼀样去调⽤,实际发出的是远程的请求。让我们更加便捷和优雅的去调⽤基于 HTTP
的 API
,被⼴泛应⽤在 Spring Cloud
的解决⽅案中。
在前面的文章中可以发现当我们通过RestTemplate
调用其它服务的API
时,所需要的参数须在请求的URL
中进行拼接,如果参数少的话或许我们还可以忍受,一旦有多个参数的话,这时拼接请求字符串就会效率低下,并且显得好傻。
那么有没有更好的解决方案呢?答案是确定的有,Netflix
已经为我们提供了一个框架:Feign
。
Feign
是一个声明式的Web Service
客户端,它的目的就是让Web Service
调用更加简单。Feign
提供了HTTP
请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP
请求的参数、格式、地址等信息。
而Feign
则会完全代理HTTP
请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。Feign
整合了Ribbon
和Hystrix
(关于Hystrix
我们后面再讲),可以让我们不再需要显式地使用这两个组件。
总起来说,Feign
具有如下特性:
- 采用的是基于接口可插拔的注解支持,包括
Feign
注解和JAX-RS
注解; - 支持可插拔的
HTTP
编码器和解码器; - 支持
Hystrix
和它的Fallback
,具有熔断降级的能力; - 支持
Ribbon
的负载均衡,具有负载均衡的能力; - 支持
HTTP
请求和响应的压缩。
这看起来有点像我们Spring MVC
模式的Controller
层的RequestMapping
映射。这种模式是我们非常喜欢的。Feign
是用@FeignClient
来映射服务的。
1.2 为什么使用Feign
Feign
的首要目标就是减少HTTP
调用的复杂性。在微服务调用的场景中,我们调用很多时候都是基于HTTP
协议的服务,如果服务调用只使用提供 HTTP
调用服务的 HTTP Client
框架(e.g. Apache HttpComponnets、HttpURLConnection OkHttp 等),我们需要关注哪些问题呢?
相比这些 HTTP
请求框架,Feign
封装了HTTP
请求调用的流程,而且会强制使用者去养成面向接口编程的习惯(因为 Feign
本身就是要面向接口)。
1.3 Feign
详解
1.3.1 代码示例
首先第一步,在原来的基础上新建一个Feign
模块,接着引入相关依赖,引入Feign
依赖,会自动引入Hystrix
依赖的,如下所示:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
application.yml
配置如下所示:
server:
port: 8083
spring:
application:
name: feign-consumer
eureka:
client:
service-url:
defaultZone: http://localhost:8888/eureka/,http://localhost:8889/eureka/
接着在前面文章中的的的两个provider1
和provider2
两个模块的服务新增几个方法,如下代码所示:
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
System.out.println("访问来1了......");
return "hello1";
}
@RequestMapping("/hjcs")
public List<String> laowangs(String ids){
List<String> list = new ArrayList<>();
list.add("laowang1");
list.add("laowang2");
list.add("laowang3");
return list;
}
//新增的方法
@RequestMapping(value = "/hellol", method= RequestMethod.GET)
public String hello(@RequestParam String name) {
return "Hello " + name;
}
@RequestMapping(value = "/hello2", method= RequestMethod.GET)
public User hello(@RequestHeader String name, @RequestHeader Integer age) {
return new User(name, age);
}
@RequestMapping(value = "/hello3", method = RequestMethod.POST)
public String hello (@RequestBody User user) {
return "Hello "+ user. getName () + ", " + user. getAge ();
}
}
接着是上面代码所需用到的User
类,代码如下所示:
public class User {
private String name;
private Integer age;
//序列化传输的时候必须要有空构造方法,不然会出错
public User() {
}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
//...getter setter省略
}
接下来用Feign
的@FeignClient(“服务名称”)
映射服务调用。代码如下所示:
package hjc;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.*;
//configuration = xxx.class 这个类配置Hystrix的一些精确属性
//value=“你用到的服务名称”
@FeignClient(value = "hello-service",fallback = FeignFallBack.class)
public interface FeignService {
//服务中方法的映射路径
@RequestMapping("/hello")
String hello();
@RequestMapping(value = "/hellol", method= RequestMethod.GET)
String hello(@RequestParam("name") String name) ;
@RequestMapping(value = "/hello2", method= RequestMethod.GET)
User hello(@RequestHeader("name") String name, @RequestHeader("age") Integer age);
@RequestMapping(value = "/hello3", method= RequestMethod.POST)
String hello(@RequestBody User user);
}
接着在Controller
层注入FeiService
这个接口,进行远程服务调用,代码如下:
@RestController
public class ConsumerController {
@Autowired
FeignService feignService;
@RequestMapping("/consumer")
public String helloConsumer(){
return feignService.hello();
}
@RequestMapping("/consumer2")
public String helloConsumer2(){
String r1 = feignService.hello("hjc");
String r2 = feignService.hello("hjc", 23).toString();
String r3 = feignService.hello(new User("hjc", 23));
return r1 + "-----" + r2 + "----" + r3;
}
}
接着在Feign
模块的启动类哪里打上Eureka
客户端的注解@EnableDiscoveryClient
Feign
客户端的注解@EnableFeignClients
,代码如下所示:
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class FeignApplication {
public static void main(String[] args) {
SpringApplication.run(FeignApplication.class, args);
}
}
接着启动启动类,浏览器上输入localhost:8083/consumer
运行结果如下所示:
可以看到负载均衡轮询出现hello1,hello2
。
接着继续在浏览器上输入localhost:8083/consumer2
,运行结果如下:
接下来我们进行Feign
声明式调用服务下的,服务降级的使用,那么我们就必须新建一个FeignFallBack
类来继承FeiService
,代码如下:
package hjc;
import org.springframework.stereotype.Component;
@Component
public class FeignFallBack implements FeignService{
//实现的方法是服务调用的降级方法
@Override
public String hello() {
return "error";
}
@Override
public String hello(String name) {
return "error";
}
@Override
public User hello(String name, Integer age) {
return new User();
}
@Override
public String hello(User user) {
return "error";
}
}
接着我们再把那两个服务提供模块provider1,provider2
模块进行停止,运行结果如下所示:
可以看到我们这几个调用,都进行了服务降级了。
那么如果我们想精确的控制一下Hystrix
的参数也是可以的,比方说跟Hystrix
结合的参数,那么可以在FeignClient
注解里面配置一个Configuration=XXX类.class
属性,在哪个类里面精确的指定一下属性。
或者在application.yml
里面配置,如下:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutinMilliseconds: 5000
ribbon:
connectTimeout: 500
#如果想对单独的某个服务进行详细配置,如下
hello-service:
ribbon:
connectTimeout: 500
1.3.2 参数处理
在Feign
处理远程服务调用时,传递参数是通过HTTP
协议传递的,参数存在的位置是请求头或请求体中。请求头传递的参数必须依赖@RequestParam
注解来处理请求参数,请求体传递的参数必须依赖@RequestBody
注解来处理请求参数。
1.3.2.1 代码环境如下
Contronller
层通过feignClient
调用微服务 获取所有任务
@Controller
@RequestMapping("tsa/task")
public class TaskController{
@Autowired
TaskFeignClient taskFeignClient;
@PostMapping("/getAll")
@ResponseBody
public List<TaskVO> getAll() {
List<TaskVO> 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<TaskVO> getAll();
}
微服务端
@Slf4j
@RestController
@RequestMapping("taskApiController")
public class TaskApiController{
@Autowired
private TaskService taskService;
@PostMapping("/getAll")
public List<TaskVO> getAll() {
log.info("--------getAll-----");
List<TaskVO> all = taskService.getAll();
return all;
}
}
1.3.2.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<TaskVO> getAll(String name);
}
或
@PostMapping(value = "taskApiController/getAll")
List<TaskVO> getAll(@RequestParam String name);
正确写法:
@PostMapping(value = "taskApiController/getAll")
List<TaskVO> getAll(@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<TaskVO> 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<TaskVO> getAll(TaskVO vo);
}
也可以这样写
@PostMapping(value = "taskApiController/getAll")
List<TaskVO> getAll(@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<TaskVO> getAll(@RequestBody TaskVO vo) {
log.info("--------getAll-----");
List<TaskVO> 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
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version> 4.5.3 </version>
</dependency>
发现我们少配置了一个依赖:
<!-- 使用Apache HttpClient替换Feign原生httpclient -->
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>${feign-httpclient}</version>
</dependency>
那我加上这个依赖后,请求通了,但是接口接收到对象里边属性值是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.2.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<String, Object> map);
}
多参数的URL
也可以使用Map
去构建
当目标URL
参数非常多的时候,可使用这种方式简化Feign
接口的编写。
POST请求包含多个参数
下面我们来讨论如何使用Feign
构造包含多个参数的POST
请求。
实际就是坑四,把参数封装成对象传递过去就可以了
1.3.2.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
依赖:
<!-- 使用Apache HttpClient替换Feign原生httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>${feign-httpclient}</version>
</dependency>
然后在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