基于技术分享,记录一下对feign的理解:
目录
- 关于feign
- 如何应用
- 分析过程及结构
首先讲一下feign本是netfix下的开源组件,springcloud整合它并加上自己的自定义实现一个基于spring cloud下的服务调度组件.本节讲的是基于1.4X系列的feign, spring cloud相应的也是1.5X系列的.
关于feign
- 声明式的Web服务客户端
- http请求调用的轻量级框架
- 支持可插拔编码器和解码器
- Feign默认集成了Ribbon
1.使得Web服务客户端的写入更加方便 要使用Feign创建一个界面并对其进行注释.(客户端是灵活性的接口,默认的是java的hettpurlConnect连接,当然官网也推荐其他的了, OkHttp , Http2),在基于springcloud应用中把底层封装了resttemplate,它其实对HTTP请求技术框架(如OkHttp/HttpClient)又进行了再一层的封装,并没有重写http请求。
2.可以以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直接调用。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。
3. 在源码中就是Encode和decode,但是他是两个接口,而不是实现类,因为这个是结合与其他的开源工具使用的,官方推荐的有两种: GSON 和 Jackson 哪种都可以因为都他们都包含了一个可以与JSON API一起使用的编码器和 解码器,在基于spring cloud配置使用feign的时候Spring Cloud增加了对Spring MVC注释的支持,并使用Spring Web中默认使用的HttpMessageConverters
4.和Eureka结合,默认实现了负载均衡的效果(需要开启@LoadBalanced注解)。注意:负载均衡是加在客户端中,而不是服务端中。就是客户端的负载均衡, 什么是客户端的负载均衡就是在使用Ribbon会从Eureka注册中心去获取服务端列表,然后进行轮询访问以到达负载均衡的作用(这个轮询只是其中的一个策略,有好几中,比如根据权重比计算,不细讲),客户端负载均衡中也需要心跳机制去维护服务端清单的有效性,当然这个过程需要配合服务注册中心一起完成
搭建一个feign的demo分两种,一种就是直接应用githup提供netfix的demo示例(现更新到9.5系列版本),就是第一种,第二种就是基于springcloud封装过后的feign的demo(是在目前项目应用的1.4.x springcloudfeign版本),
为了方便记忆,我会把代码import的包也会放在上面
因为我总是发现,查资料时查看到了,但是有时候包就是不知道导成什么样的,所以还会配有截图,也方便自己记录
import feign.Feign;
import feign.Param;
import feign.RequestLine;
import java.util.List;
//@RestController
public class Test {
public static void main(String[] args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
// 获取贡献者列表,并打印其登录名以及贡献次数
List<Contributor> contributors = github.contributors("netflix", "feign");
for (Contributor contributor : contributors) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
}
}
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
}
class Contributor {
String login;
int contributions;
}
给出一个图的原因是,因为本身解码器使用的是Gson所以,先配上图,标明我用的是这些,然后下面配置代码,以方便下次自己使用的时候不用再找了
package com.example.demo.test;
import com.google.gson.Gson;
import com.google.gson.JsonIOException;
import com.google.gson.TypeAdapter;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
import java.util.Collections;
import feign.Response;
import feign.Util;
import feign.codec.Decoder;
import static feign.Util.ensureClosed;
public class GsonDecoder implements Decoder {
private final Gson gson;
public GsonDecoder(Iterable<TypeAdapter<?>> adapters) {
this(GsonFactory.create(adapters));
}
public GsonDecoder() {
this(Collections.<TypeAdapter<?>>emptyList());
}
public GsonDecoder(Gson gson) {
this.gson = gson;
}
@Override
public Object decode(Response response, Type type) throws IOException {
if (response.status() == 404) return Util.emptyValueOf(type);
if (response.body() == null) return null;
Reader reader = response.body().asReader();
try {
return gson.fromJson(reader, type);
} catch (JsonIOException e) {
if (e.getCause() != null && e.getCause() instanceof IOException) {
throw IOException.class.cast(e.getCause());
}
throw e;
} finally {
ensureClosed(reader);
}
}
}
package com.example.demo.test;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.Map;
import static feign.Util.resolveLastTypeParameter;
final class GsonFactory {
private GsonFactory() {
}
/**
* Registers type adapters by implicit type. Adds one to read numbers in a {@code Map<String,
* Object>} as Integers.
*/
static Gson create(Iterable<TypeAdapter<?>> adapters) {
GsonBuilder builder = new GsonBuilder().setPrettyPrinting();
builder.registerTypeAdapter(new TypeToken<Map<String, Object>>() {
}.getType(), new GsonTypeAdapter()).create();
for (TypeAdapter<?> adapter : adapters) {
Type type = resolveLastTypeParameter(adapter.getClass(), TypeAdapter.class);
builder.registerTypeAdapter(type, adapter);
}
return builder.create();
}
}
package com.example.demo.test;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class GsonTypeAdapter extends TypeAdapter<Object> {
@Override
public Object read(JsonReader in) throws IOException {
// 反序列化
JsonToken token = in.peek();
switch (token) {
case BEGIN_ARRAY:
List<Object> list = new ArrayList<Object>();
in.beginArray();
while (in.hasNext()) {
list.add(read(in));
}
in.endArray();
return list;
case BEGIN_OBJECT:
Map<String, Object> map = new HashMap<String, Object>();
in.beginObject();
while (in.hasNext()) {
map.put(in.nextName(), read(in));
}
in.endObject();
return map;
case STRING:
return in.nextString();
case NUMBER:
/**
* 改写数字的处理逻辑,将数字值分为整型与浮点型。
*/
double dbNum = in.nextDouble();
// 数字超过long的最大值,返回浮点类型
if (dbNum > Long.MAX_VALUE) {
return dbNum;
}
// 判断数字是否为整数值
long lngNum = (long) dbNum;
if (dbNum == lngNum) {
return lngNum;
} else {
return dbNum;
}
case BOOLEAN:
return in.nextBoolean();
case NULL:
in.nextNull();
return null;
default:
throw new IllegalStateException();
}
}
@Override
public void write(JsonWriter out, Object value) throws IOException{
// 序列化不处理
}
}
接下来是spring cloud下面的一个例子
这个跟上个一样,不用看清楚,就是知道有这个东西就行
简单说一下,假设是A,B两个工程,B,工程向A工程访问,上面的代码就是配置,而那些被红色框框圈着的就是需要加的注解,主要注解就这两个.第一个是加载当前工程的启动方法上的,第二个就是自己写的一个类似于service层的接口一样,只不过上面的service注解变成了Feign的注解,name就是你访问的那个工程的名字,他这个实现原理就是feign去注册列表里面查找对应名子,所以他夸服务访问的时候会按照这个工程名字去从注册列表里面寻找.
关于这个,还有一个点要说的是,通常基于spring cloud各个微服务都是会有登陆校验机制的,所以说在访问的时候,要有一个自己定义的一个单独配置文件,就是feign Config,名字可以自己取但如果用的话要在这个注解里面加上配置的, 关于这个配置,这个是feign提供的一个自定义拦截器一样的东西,就是方便用户自己定义请求头或body等等.
分析过程
- 组成结构
- spring cloud怎么封装
首先来说一下feign,它封装了Http调用流程,之所以好用就是更适合面向接口化。但其实他也是一个服务调用,那么在服务调用的场景中,我们经常调用基于Http协议的服务,先大致的有个图铺垫
我们经常使用到的框架可能有HttpURLConnection、Apache HttpComponnets、OkHttp3 、Netty等等,这些框架在基于自身的专注点提供了自身特性。而从角色划分上来看,他们的职能是一致的提供Http调用服务。具体流程如图,
讲完这些再feign是如何设计的
- 首先是发生请求
- 基于面向接口的动态代理方式生成实现类
- 根据接口的注解声明对着,然后解析出底层MethodHandler
- ecoder将bean包装成请求
- 拦截器负责对请求和返回进行装饰处理(requestIntercepter)
- 紧接着就是日志记录(日志记录分几种方式,默认的是none没有记录当然可以修改的)
- 下面这个retryter就是基于重试器发送http请求, 可以基于不同的http框架处理
接下来会逐个讲解一下每层,首先说的是target动态代理
在使用feign 时,会定义对应的接口类,在接口类上使用Http相关的注解,标识HTTP请求参数信息,通过基于面向接口的动态代理方式生成实现类,将请求调用委托到动态代理实现类,基本原理如下
简单对应着说一下这个reflectiveFeign继承的是feign重写实例方法,生成一个个的handler处理,利用java的动态代理,讲所有请求都转换给这些invocationhandler处理
接下来是 Contract协议规则
根据Contract协议规则,
中文意义是合约, parseAndValidatateMetadata的作用是将我们传入的接口进行解析验证,看注解的使用是否符合规范,然后返回给我们接口上各种相应的元数据。这个contact是一个接口,BaseContract (抽象方法)子类实现它,主要逻辑是验证我们使用的时候是否符合了规范。至于抽取元数据的逻辑留给了子类,
解析接口类的注解信息,这里面有两个指向,一个是默认的,就是feign官网提供的,这里面有一个d名字叫efault类去继承baseContract去实现抽取数据的逻辑.实现了以下3个方法的具体逻辑,负责抽取元信息processAnnotationOnClassprocessAnnotationOnMethodprocessAnnotationsOnParameter
另外一个就是spring cloud自己封装springMVC的,这也是为了方便使用,官降低学习成本吧.但是其内部也是SpringMvcContract通过继承这个baseContract校验规范,和自己内部自定义实现抽取元数据处理.
通过Contract,BaseContract,Default 爷孙仨人的模板模式共同完成了一个任务,将我们定义的API接口“targetType”中包含的所有关于HTTP的附加元信息抽取成结果集返回
默认有一套自己的协议规范,规定了一些注解,可以映射成对应的Http请求,如官方的一个例子,就是自定义生成一个contract类,然后里面自己定义的属性,接收参数的时候回封装到里面,具体FeignContract 是如何解析的,具体,可以去git上看代码:
https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/Contract.java
Feign 默认的协议规范
接下来看一下,基于Spring MVC的协议规范 SpringMvcContract
他才是当前Spring Cloud 微服务解决方案,采用了Spring MVC的部分注解来完成 请求协议解析,也就是说 ,写客户端请求接口和像写服务端代码一样:客户端和服务端可以通过SDK的方式进行约定,然后这边只需要引入服务端发布的SDK API,就可以使用面向接口的编码方式对接服务,
当然,目前的Spring MVC的注解并不是可以完全使用的,因为Spring Cloud 没有基于Spring MVC 全部注解来做Feign 客户端注解协议解析,比如@GetMapping,@PutMapping 等,仅支持使用@RequestMapping 等,如果使用这个注解,然后加上你的参数去访问会报错是支持post请求,我看了一下源代码里面有一个output方法中时判断这个访问方式如果是get会默认转成post,但是因为你注解上面加的是get,然后两者会冲突,就会抛异常, 这个是要注意到就好,如果真的需要可以自己写增强类,(另外注解继承性方面也有些问题)
RequestBean
基于 RequestBean,动态生成Request,根据传入的Bean对象和注解信息,从中提取出对应的值,来构造Http Request 对象:在前面讲按个自定义配置feign文件的时候,实现了一个requestIntercepter接口,然后里面重写的那个apply方法里的里面的那个参数就是这个requestTemplate
编码器解码器
使用Encoder 将Bean转换成 Http报文正文(消息解析和转码逻辑),在接口定义上Feign做的比较简单,抽象出了Encoder 和decoder 接口
目前Feign 有以下实现:
拦截器负责对请求和返回进行装饰处理
在请求转换的过程中,Feign 抽象出来了拦截器接口,用于自定义对请求的操作:比如,我们在访问每个微服务时要有登陆校验,所以要把这些请求头信息和body都先封装到里面.
日志记录
默认是NONE,这个没什么讲的,就是根据自己配置需要去设置日志输出
基于重试器发送HTTP请求
Feign 内置了一个重试器,当HTTP请求出现IO异常时,Feig
重试器几个控制参数 n会有一个最大尝试次数发送请求,
发送Http请求
Feign 真正发送HTTP请求是委托给 feign.Client 来做的:Feign 默认底层通过JDK 的 java.net.HttpURLConnection 实现了feign.Client接口类,在每次发送请求的时候,都会创建新的HttpURLConnection 链接,
可以通过拓展该接口,使用Apache HttpClient 或者OkHttp等基于连接池的高性能Http客户端,我做过这个OkHttpdemo,相对来说是不错的,
Feign 整体框架非常小巧,在处理请求转换和消息解析的过程中,基本上没什么时间消耗。真正影响性能的,是处理Http请求的环节。默认情况下,Feign采用的是JDK的HttpURLConnection,如果对性能要求高的话,可以在这里做一个优化.
首先在POM中添加如下依赖
在application.properties中添加配置:
feign.httpclient.enabled=false
feign.okhttp.enabled=true
还有一些其他的讲述:可根据自己需要
1,通常不建议在服务器和客户端之间共享接口。它引入了紧耦合,并且实际上并不适用于当前形式的Spring MVC(方法参数映射不被继承)。
2.所有用到Ribbon负载均衡的均要有@LoadBalanced注解,配置文件也很简单,就是ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule,
然后这个负载均衡的策略也可以自定义,默认会使用轮询的策略,也有随机的,和权重的
https://cloud.tencent.com/developer/article/1332634
3.Spring建议可以选择加入的方式,当电路断开或出现错误时执行的默认代码路径。要给加@FeignClient启用回退,将fallback属性设置为实现回退的类名.在Feign中执行回退以及Hystrix回退的工作方式存在局限性。当前返回com.netflix.hystrix.HystrixCommand和rx.Observable的方法目前不支持回退。
对于feign的理解就先是这些,有错误可以指点!!!