08、Feign的使用

277 阅读10分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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 方法设置日志记录方式,本例是 输出到文件中, 运行以上代码,再打开日志文件,可以看到接口的日志如下: image.png 以上日志,记录的就是一次请求的过程。设置接口的日志级别,有以下可选值。

  • NONE:默认值,不进行日志记录。
  • BASIC:记录请求方法、 URL、响应状态代码和执行时间。
  • HEADERS:除了 BASIC 记录的信息外,还包括请求头与响应头。
  • FULL :记录全部日志, 包括请求头、请求体、请求与响应的元数据。

记录接口日志的调用过程可以很方便地查找问题,不管在开发环境还是生产环境,都有较大的意义。