持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第8天,点击查看活动详情
08、Feign的使用
编码器
向服务发送请求的过程中,有些情况需要对请求的内容进行处理。例如服务端发布的 服务接收的是 JSON 格式的参数,而客户端使用的是对象,这种情况就可以使用编码器, 将对象转换为 JSON 字符串。
服务端编写一个 REST 服务:
@RequestMapping(value = "person/create", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public String createPerson(@RequestBody Person person) {
System.out.println(person.getName() + "--" + person.getAge());
return "Success,Person Id : " + person.getId();
}
客户端编写接口:
public interface PersonClient {
@RequestLine("POST /person/create")
@Headers("Content-Type: application/json")
String createPerson(Person person);
//为所有属性加上 setter getter 等方法
@Data
class Person {
Integer id;
String name;
String age;
String message;
}
}
在客户端的服务接口中,使用了@Headers 注解, 声明请求的内容类型为 JSON
public class EncoderTest {
/**
* 在运行类中,在创建服务接口实例时,使用了 encoder 方法来指定编码器,本案例使
* 用了 Feign 提供的 GsonEncoder 。该类会在发送请求的过程中,将请求的对象转换为 JSON
* 字符串。 Feign 支持插件式的编码器,如果 Feign 提供的编码器无法满足要求,还可以使用
* 自定义的编码器
*/
public static void main(String[] args) {
PersonClient personClient = Feign.builder()
.encoder(new GsonEncoder())
.target(PersonClient.class, "http://localhost:8888");
PersonClient.Person person = new PersonClient.Person();
person.id = 1;
person.name = "Angus";
person.age = "30";
String response = personClient.createPerson(person);
System.out.println(response);
}
}
运行后输出如下:Success,Person Id : 1
解码器
编码器是对请求的内容进行处理,解码器则会对服务响应的内容进行处理,例如将解 析响应的 JSON 或者 XML 字符串,转换为我们所需要的对象,在代码中通过以下代码片断 设置解码器:
PersonClient personService = Feign.builder()
.decoder(new GsonDecoder())
.target(PersonClient.class, "http://localhost:8888/");
上一章节中,我们己经使用过 GsonDecoder 解码器
XML 的编码与解码
除了支持 JSON 的处理外, Feign 还为 XML 的处理提供了编码器与解码器,可以使 JAXBEncoder与 JAXBDecoder 进行编码与解码。 服务端发布XML接口:
@RequestMapping(value = "person/createXML", method = RequestMethod.POST,
consumes = MediaType.APPLICATION_XML_VALUE,
produces = MediaType.APPLICATION_XML_VALUE)
public String createXMLPerson(@RequestBody Person person) {
System.out.println(person.getName() + "--" + person.getId());
return "<result><message>success</message></result>";
}
spring-boot starter-web构建的Web项目默认不支持XML接口,需要添加如下依赖
<!-- 支持XML接口,测试 fein xml编码解码 start-->
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-xml-provider</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jaxb</artifactId>
<version>9.5.0</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.2.8</version>
</dependency>
<!-- 支持XML接口,测试 fein xml编码解码 end-->
先定义好服务接口以及对象,编写客户端
/**
* @XmlTransient 不使用此注解,会出现类的两个属性具有相同名称的异常
* 详细见 https://blog.csdn.net/qq_33394088/article/details/72790084
*/
public interface PersonClient {
@RequestLine("POST /person/createXML")
@Headers("Content-Type: application/xml")
Result createPersonXML(Person person);
@XmlRootElement
class Person {
@XmlElement
Integer id;
@XmlElement
String name;
@XmlElement
String age;
@XmlElement
String message;
@XmlTransient
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@XmlTransient
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@XmlTransient
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
}
@XmlRootElement
class Result {
@XmlElement
String message;
@XmlTransient
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
}
客户端:
import cn.com.picc.sbsj.srlinvokerserver.feign.feignUse.PersonClient.Person;
import cn.com.picc.sbsj.srlinvokerserver.feign.feignUse.PersonClient.Result;
import feign.jaxb.JAXBContextFactory;
import feign.jaxb.JAXBDecoder;
import feign.jaxb.JAXBEncoder;
import feign.Feign;
public class XMLTest {
public static void main(String[] args) {
JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder().build();
// 获取服务接口
PersonClient personClient = Feign.builder()
.encoder(new JAXBEncoder(jaxbFactory))
.decoder(new JAXBDecoder(jaxbFactory))
.target(PersonClient.class, "http://localhost:8888/");
// 构建参数
Person person = new Person();
person.id = 1;
person.name = "Angus";
person.age = "30";
// 调用接口并返回结果
PersonClient.Result result = personClient.createPersonXML(person);
System.out.println(result.message);
}
}
本小节的请求有一点特殊,请求服务时传入的参数为 XML 的、返回的结果也是 XML的,目的是使编码与解码一起使用。
运行客户端返回:success
自定义编码器解码器
根据前面两小节的介绍可知 Feign 的插件式编码器与解码器可以对请求以及结果进行 处理 对于一些特殊的要求,可以使用自定义的编码器与解码器。实现自定义编码器,需 要实现 Encoder 接口的 encode 方法,而对于解码器,则要实现 Decoder 接口的 decode 方法 例如以下的代码片断 在使用时,调用 Feign 的AP 来设置编码器或者解码器即可,实现较为简单,在此不 再赘述。
public class MyEncoder implements Encoder {
public void encode(Object object , Type bodyType , RequestTemplate template)
throws EncodeException {
//实现自己的 Encode 逻辑
}
}
##自定义feign客户端 Fei gn 使用 一个Client 接口来发送请求,默认情况下,使用 HttpURLConnection 连接 HTTP 服务。与前面的编码器类似,客户端也采用插件式设计,也就是说,我们可以实现 自己的客户端。本小节将使用 HttpClient 实现 个简单的 Feign 客户端 pom.xml 加入 HttpClient 的依赖:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
新建 feign.Client 接口的实现类
public class MyFeignClient implements Client {
@Override
public Response execute(Request request, Request.Options options) throws IOException {
System.out.println("===== 这是自定义的feign客户端");
try {
//创建一个默认的客户端
CloseableHttpClient httpClient = HttpClients.createDefault();
//获取调用的HTTP方法
final String method = request.method();
//创建一个HttpClient的HttpRequest
HttpRequestBase httpRequest = new HttpRequestBase() {
@Override
public String getMethod() {
return method;
}
};
//设置请求地址
httpRequest.setURI(new URI(request.url()));
//执行请求,获取响应
HttpResponse httpResponse = httpClient.execute(httpRequest);
// 获取响应的主题内容
byte[] body = EntityUtils.toByteArray(httpResponse.getEntity());
//将HttpClient的响应对象转换为Feign的Response
Response response = Response.builder()
.body(body)
.headers(new HashMap<String, Collection<String>>())
.status(httpResponse.getStatusLine().getStatusCode())
.build();
return response;
} catch (Exception e) {
throw new IOException(e);
}
}
}
简单讲 下自定义 Feign 客户端 的实现过程。在实现 execute方法时 ,将Feign Request 实例转换为 HttpClient的 HttpRequestBase 。再使用 CloseableHttp Client 来执行请求,得到响 应的 HttResponse 实例后,再转换为 Feign 的Response 实例返回。 我们实现的客户端,包括 Feign 自带的客户端以及其他扩展的客户端,实际上就是一个对象转换的过程。
本例中简化了实现,自定义的客户端中并没有转换请求头等信息,因此使用本例的客户端, 无法请求其他格式的服务。
在运行类中直接使用我们自定义的客户端
public class MyClientTest {
public static void main(String[] args) {
//获取服务接口
HelloClient helloClient = Feign.builder()
.encoder(new GsonEncoder())
.client(new MyFeignClient())
.target(HelloClient.class, "http://localhost:8888");
String result = helloClient.sayHello();
System.out.println("接口响应内容:" + result);
}
}
输出: ==== 这是自定义的 Feign 客户端 接口响应内容: Hello World
虽然 Feign 也有 HtψClient 的实现,但本例的目的主要是向大家展示 Feign 客户端的原理 ,举一反三,如果我们实现一个客户端,在实现中调用 Ribbon 的 API 来实现负载均衡的 功能,是完全可以实现的。幸运的是, Feign 己经帮我们实现了 RibbonClient ,可以直接使 用,更进一步, Spring Cloud 实现了自己的Client ,我们将在后面章节中讲述。
使用第三方注解
根据前面章节的介绍可知,通过注解修改的接口方法 可以让接口方法获得访问服务 的能力 除了 Feign 自带的方法外 还可以使用第三方的注解。如果想使用 JAXRS 规范的 注解,可以使用 Feign feign-jaxrs 模块,在 pom. xml 中加入以下依赖即可:
<!-- Feign对JAXRS的支持 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jaxrs</artifactId>
<version>9.5.0</version>
</dependency>
<!-- JAXRS -->
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
<version>1.1.1</version>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-slf4j</artifactId>
<version>9.5.0</version>
</dependency>
在使用注解修饰接口时,可以直接使用@GET、@Path 等注解,例如想要使用 GET 法调用/h ello 服务,可以定义以下接口
@Get @Path("/hello")
String srHello();
以上修饰接口的,实际上等价于@RequestLine("GET /hello") 为了让 Feign 知道这 些注解的作用,需要在创建服务客户端时调用 contract 方法来设置 JAXRS 注解的解析类 见以下代码:
RsClient rsClient = Feign.builder()
. contract (new JAXRSContract())
.target(RsClient.class, "http://localhost:8888/");
设置了 JAXRSContract 类后 Feign 就知道如何处理 JAXRS 的相关注解了,下节, 我们将讲解 Feign 是如何处理第二方注解的。
解析注解
**使用了 Spring Cloud 的“翻译器”后,将不能再使用 Feign 的默认注解。 ====== 重要 **
根据前一小节的介绍可知,设置了 JAXRSContract 后, Feign 就知道如何处理接口中的 JAXRS 注解了。 JAXRSContract 继承了 BaseContract 类, BaseContract 类实现了 Contract 接口,简单来说, Contract 就相当于一个翻译器, Feign 本身并不知道这些第三方注解 的含义,而通过实现一个翻译器(Contract )来告诉 Feign ,这些注解是做什么的 为了让读者能够了解其中的原理,本小节将使用一个自定义注解,并且翻译给 Feign 让其去使用。代码清单 5-15 示为自定义注解以及客户端接口的代码
@Target(METHOD)
@Retention(RUNTIME)
public @interface MyUrl {
//定义 url 与 method 属性
String url();
String method() ;
}
public interface HelloClient {
@MyUrl(method = "GET",url = "/hello")
String myUrlHello();
/**
* 使用MyUrl时,此接口必须要注释掉,不然报错, 使用@RequestLine不使用 MyContract
* myUrlHello不注释掉也会报错
* 个人认为:此接口通过默认的contract翻译器翻译,自定义的MyContract翻译不了
* 如果执行不使用MyContact 调用 sayHello 上面的 会包如下错误
* 应该是 需要统一用相同的注解
* java.lang.IllegalStateException: Method myUrlHello not annotated with HTTP method type (ex. GET, POST)
*/
//@RequestLine("GET /hello")
//String sayHello();
}
接下来,就要将 MyUrl 注解的作用 告诉 Feign ,新 建Contract 继承BaseContract 类,实 现请见代码
public class MyContract extends Contract.BaseContract {
/**
* 分别是处理类注解、
*/
@Override
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
}
/**
* 用于处理方法级的注解
*/
@Override
protected void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method) {
//是MyUrl的注解才处理
if (MyUrl.class.isInstance(annotation)){
//获取注解的示例
MyUrl myUrlAnn = method.getAnnotation(MyUrl.class);
// 获取配置的Http 方法
String httpMethod = myUrlAnn.method();
// 获取请求服务的URL
String url = myUrlAnn.url();
//将值设置到模板中
data.template().method(httpMethod);
data.template().append(url);
}
}
/**
* 是参数注解、
*/
@Override
protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
return false;
}
}
在MyContract 类中,需要实现三个方法,分别是处理类注解、处理方法注解、处理 数注解的方法,由于我们只定义了一个方法注解@MyUrl ,因此 实现 processAnnotationOnMethod 即可 在processAnnotationOnMethod 方法中,通过 Method 的getAnnotation 获取 MyUrl 的实 例,将 MyUrl 的url method 属性分别设置到 Feign 的模板中 。在创建客户端时,再调 contract 方法即可,
public class ContractTest {
public static void main(String[] args) {
//获取服务接口
HelloClient helloClient = Feign.builder()
.contract(new MyContract())
.target(HelloClient.class, "http://localhost:8888");
//请求hello 接口
String result = helloClient.myUrlHello();
System.out.println("接口响应内容 :" + result);
}
}
由本例可知, Contract 实际上承担的是翻译的作用,将第三方(或者自定义〉注解的 作用告诉 Feign 。在 Spring Cloud 中,也实现了 Spring的 Contrac 可以在接口中使用 @RequestMapping 注解。读者在学习 Spring Cloud 整合 Feign 时,见到使用@RequestMapping 修饰的接口,就可以明白其中的原理。
请求拦截器
Feign 支持请求拦截器,在发送请求前,可以对发送的模板进行操作,例如设置请求头 的属性等。自定义请求拦截器,实现 Requestlnterceptor 接口,在创建客户端时,调用相应 的方法设置一个或者多个拦截器,请见以下代码片断
import feign.RequestInterceptor;
import feign.RequestTemplate;
// 自定义拦截器
public class MyInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header("Content-Type ", "application/json ");
}
}
public class InterceptorTest {
/**
* 使用自定义的拦截器
*/
public static void main(String[] args) {
//获取服务接口
PersonClient personClient = Feign.builder()
.requestInterceptor(new MyInterceptor())
.target(PersonClient.class, "http://localhost:8888");
}
}
接口日志
默认情况下 ,不会记录接口的日志,如果需要很清楚地了解接口的调用情况,可以使用logLevel 方法进行配置,请见以下代码:
/**
* 设置feign日志
*/
public static void main2(String[] args) {
PersonClient personClient = Feign.builder()
.logLevel(Logger.Level.FULL)
.logger(new Logger.JavaLogger().appendToFile("logs/http.log"))
.target(PersonClient.class, "http://localhost:8888");
}
调用了 logLevel 设置接口的日志级别,调用了 logger 方法设置日志记录方式,本例是
输出到文件中, 运行以上代码,再打开日志文件,可以看到接口的日志如下:
以上日志,记录的就是一次请求的过程。设置接口的日志级别,有以下可选值。
- NONE:默认值,不进行日志记录。
- BASIC:记录请求方法、 URL、响应状态代码和执行时间。
- HEADERS:除了 BASIC 记录的信息外,还包括请求头与响应头。
- FULL :记录全部日志, 包括请求头、请求体、请求与响应的元数据。
记录接口日志的调用过程可以很方便地查找问题,不管在开发环境还是生产环境,都有较大的意义。