目录
1. 什么是 Feign
Feign
的英文表意为“假装,伪装,变形”, 是一个 Http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用。
Feign
通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。
Feign
被广泛应用在 Spring Cloud 的解决方案中,是学习基于 Spring Cloud 微服务架构不可或缺的重要组件。
Feign
开源项目地址:https://github.com/OpenFeign/feign
2. Feign 解决了什么问题
Feign 封装 HTTP 调用流程,面向接口编程。
Feign 本身很简单,但做了大量的适配工作,这也是这个框架存在的意义。
在服务调用的场景中,我们经常调用基于 Http 协议的服务,而我们经常使用到的框架可能有HttpURLConnection、Apache HttpComponnets、OkHttp3 、Netty 等等,这些框架在基于自身的专注点提供了自身特性。而从角色划分上来看,他们的职能是一致的提供 Http 调用服务。具体流程如下:
总结: 根据上图的分析可知:要实现 Feign
客户端,主要是将 Method 方法的参数解析成 Http 请求的请求行、请求行、请求体,然后使用 HttpClient 发送请求。但为了实现这些设想,要解决以下问题:
- REST 声明式规范有以下几种:Feign、JAX-RS 1/2、Spring Web MVC 都需要进行适配。这几种声明式注解的适配接口是
feign.Contract
。 - Http 客户端有 JDK 自带的 HttpURLConnection、Apache HttpComponnets、OkHttp3 、Netty 等,需要适配。这几种客户端的适配接口是
feign.Client
。 - 支持负载均衡与熔断。负载均衡 Ribbon 是对
feign.Client
进行包装。熔断 Hystrix 在 HystrixFeign 中自定义 InvocationHandlerFactory 的实现,创建 HystrixInvocationHandler,对 method.invoke 请求做拦截。 - 各种编解码的适配,实现接口
feign.codec.Decoder
和feign.codec.Encoder
。
下面我们先跟着官网的示例来做一个入门案例,这个案例的功能是访问github中的openfeign的contributor信息.
github中的API列表如下: https://api.github.com/
我们将访问: repository_url: "https://api.github.com/repos/{owner}/{repo} 这个接口, 它里面要加入两个参数 owner, repo, 构成访问: https://api.github.com/repos/OpenFeign/feign/contributors. 访问后的结果页:
1). 引入 maven 依赖
<dependencies>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>10.4.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-gson</artifactId>
<version>10.4.0</version>
</dependency>
</dependencies>
2). 定义查询请求中的参数的封装类
//两个参数, 用于 请求的地址: https://api.github.com/repos/OpenFeign/feign/contributors 返回值的封装
//
public class Contributor
{
String login;
int contributions;
int id;
String node_id;
String avatar_url;
String gravatar_id;
String url;
String html_url;
String followers_url;
// String following_url;
// String gists_url;
// String starred_url;
// String subscriptions_url;
// String organizations_url;
// String repos_url;
// String events_url;
// String received_events_url;
// String type;
@Override
public String toString() {
return "Contributor{" +
"login='" + login + '\'' +
", contributions=" + contributions +
", id=" + id +
", node_id='" + node_id + '\'' +
", avatar_url='" + avatar_url + '\'' +
", gravatar_id='" + gravatar_id + '\'' +
", url='" + url + '\'' +
", html_url='" + html_url + '\'' +
", followers_url='" + followers_url + '\'' +
'}';
}
}
这个类对应 请求地址: https://api.github.com/repos/OpenFeign/feign/contributors 的返回结果的封装
3). 定义接口
public interface GitHub {
// 传入的参数后可以拼接成url: https://api.github.com/repos/OpenFeign/feign/contributors
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
}
4). 测试类
public class MyApp {
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("OpenFeign", "feign");
for (Contributor contributor : contributors) {
System.out.println(contributor);
}
}
}
小结: Feign.target()
实际上是创建了一个 GitHub 的动态代理。通过这个代理来访问url,获取到结果后,包装成对象返回.
3. Feign 声明式注解
Feign 通过 Contract
接口将方法上标注的注解解析成 MethodMetadata,最终将参数解析成 Http 请求的请求行、请求行、请求体,然后使用 HttpClient 发送请求。
Annotation | Interface Target | Usage |
---|---|---|
@RequestLine | Method | 定义HttpMethod 和 UriTemplate. UriTemplate 中使用{} 包裹的表达式,可以通过在方法参数上使用@Param 自动注入 |
@Param | Parameter | 定义模板变量,模板变量的值可以使用名称的方式使用模板注入解析 |
@Headers | Method, Type | 定义头部模板变量,使用@Param 注解提供参数值的注入。如果该注解添加在接口类上,则所有的请求都会携带对应的Header信息;如果在方法上,则只会添加到对应的方法请求上 |
@QueryMap | Parameter | 定义一个Map或 POJO,参数值将会被转换成URL上的 query 字符串上 |
@HeaderMap | Parameter | Map ->Http Headers |
@Body | Method | Defines a Template , similar to a UriTemplate and HeaderTemplate , that uses @Param annotated values to resolve the corresponding Expressions . |
以上注解的基本使用案例:
public interface FeignService {
// @Headers
@RequestLine("GET /api/documents/{contentType}")
@Headers("Accept: {contentType}")
String getDocumentByType(@Param("contentType") String type);
// @QueryMap: Map or POJO
@RequestLine("GET /find")
V find(@QueryMap Map<String, Object> queryMap);
@RequestLine("GET /find")
V find(@QueryMap CustomPojo customPojo);
// @HeaderMap: Map
@RequestLine("POST /")
void post(@HeaderMap Map<String, Object> headerMap);
// @Body
@RequestLine("POST /")
@Headers("Content-Type: application/xml")
@Body("<login \"user_name\"=\"{user_name}\" \"password\"=\"{password}\"/>")
void xml(@Param("user_name") String user, @Param("password") String password);
@RequestLine("POST /")
@Headers("Content-Type: application/json")
@Body("%7B\"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
void json(@Param("user_name") String user, @Param("password") String password);
}