《Spring Cloud微服务架构实战》--Rest客户端--Feign

​​​​​

在Spring Cloud集群中,各个角色的通信基于REST服务,因此在调用服务时,就不可 避免地需要使用REST服务的请求客户端。

前面的章节中使用了 Spring自带的RestTemplate, RestTemplate使用HttpClient发送请求。

本章中,我们将介绍另一个REST客户端:Feign 

Feign框架已经被集成到Spring Cloud的Netflix项目中,使用该框架可以在Spring Cloud 的集群中更加简单地调用REST服务。

5.1 Rest客户端

  在学习Feign前,我们先了解一下REST客户端。本节将简单地讲述Apache CXF与 Restlet这两款Web Service框架,并使用这两个框架来编写REST客户端,最后再编写一个 Feign的Hello World例子。

通过此过程,大家可以对Feign有一个初步的印象。如已经掌 握这两个REST框架,可直接学习本章后面的内容。

  本章中介绍的各个客户端,将会访问8080端口的/person/(personld)和/hello这两个服 务中的一个,服务端项目使用spring-boot-starter-web进行搭建,本节对应的服务端项目的目录为 codes\05\5. l\rest-server;

5.1.1使用CXF调用Rest服务

  CXF是目前一个较为流行的Web Service框架,是Apache的一个开源项目。使用CXF 可以发布和调用使用各种协议的服务,包括SOAP协议、XML/HTTP等。

当前CXF已经 对REST风格的Web Service提供支持,可以发布或调用REST风格的Web Serviceo由于CXF可以与Spring进行整合使用并且配置简单,因此得到许多开发者的青睐,而笔者以往所在公司的大部分项目,

均使用CXF来发布和调用Web Serviceo;本章所使用的CXF版本为3.1.10,在Maven中加入以下依赖:


  org.apache.cxf
  cxf-core
  3.1.10


  org.apache.cxf
  cxf-rt-rs-client
  3.1.10

  编写代码请求/person/{personld}服务,请见代

  客户端中使用了 WebClient类发送请求,获取响应后读取输入流,获取服务返回的JSON 字符串。运行代码可看到返回的信息

5.1.2使用Restlet调用REST服务

Restlet是一个轻量级的REST框架,使用它可以发布和调用REST风格的Web Service;

本小节中的例子所使用的版本为2.3.10, Maven依赖如下:

1

2

3

4

5

6

7

8

9

10

  org.restlet.jee

  org.restlet

  2.3.10

  org.restlet.jee

  org.restlet.ext.jackson

  2.3.10

  

客户端实现请见代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

public class RestletClient {

  public static void main(String[] args) throws Exception {

    ClientResource client = new ClientResource("http://localhost:8080/person/l");

    //调用get方法,服务端发布的是GET

    Representation response = client.get(MediaType.APPLICATION_JSON);

    //创建JacksonRepresentatiori实例,将响应转换为Map

    JacksonRepresentation jr = new JacksonRepresentation(response,HashMap.class);

    //获取转换后的Map对象

    Map result = (Map) jr.getObject ();

    //输出结果

    System.out.printin(result.get(HidH) +"--"+ result.get("name") +"--"+ result.get(nagen) +"---"+ result.get("message"));

  }

}

  代码清单中使用的Restlet的API较为简单,在此不赘述。但需要注意的是,在 Maven中使用Restlet,要额外配置仓库地址,笔者成书时,在Apache官方仓库中并没有 Restlet的包。

在项目的pom.xml文件中增加以下配置:

1

2

3

4

5

6

7

  

    maven-restlet

    Restlet repository

    http://maven.restlet.org

  

  

5.1.3 Feign框架介绍

  Feign是GitHub上的一个开源项目,目的是简化Web Service客户端的开发。在使用 Feign时,可以使用注解来修饰接口,被注解修饰的接口具有访问Web Service的能力。 这些注解中既包括Feign自带的注解,也支持使用第三方的注解。

除此之外,Feign还支 持插件式的编码器和解码器,使用者可以通过该特性对请求和响应进行不同的封装与解 析。

  Spring Cloud 将 Feign 集成到 Netflix 项目中,当与 Eureka> Ribbon 集成时,Feign 就具 有负载均衡的功能。

Feign本身在使用上的简便性,加上与Spring Cloud的高度整合,使用 该框架在Spring Cloud中调用集群服务,将会大大降低开发的工作量

5.1.4 第一个Feign程序

  先使用Feign编写一个Hello World的客户端,访问服务端的/hello服务,得到返回的 字符串。当前Spring Cloud所依赖的Feign版本为9.5.0,本章案例中的Feign也使用该版 本。

建立名称为fbign-client的Maven项目,加入以下依赖:

1

2

3

4

5

6

7

8

9

10

  io.github.openfeign

  feign-core

  9.5.0

  io.github.openfeign

  feign-gson

  9.5.0

  新建接口 HelioClient,请见代码:

1

2

3

4

public interface HelioClient {

  @RequestLine("GET /hello")

  String sayHello();

}

  

HelioClient表示一个服务接口,在接口的sayHello方法中使用了@RequestLine注解, 表示使用GET方法向/hello发送请求。接下来编写客户端的运行类,请见代码:

1

2

3

4

5

6

7

public class HelloMain {

  public static void main(String[] args) {

    //调用Hello接口

    HelloClient hello = Feign.builder().target(HelioClient.class"http://localhost:8080/");

    System.out.printIn(hello.sayHello());

  }

}

  

在运行类中,使用Feign创建HelloClient接口的实例,最后调用接口定义的方法。运 行代码清单5-4,可以看到返回的“Hello World”字符串,可见接口已经被调用。

熟悉AOP的朋友大概己经猜到,Feign实际上会帮我们动态生成代理类。Feign使用的是JDK的动态代理,生成的代理类会将请求的信息封装,交给feignClient接口发送请求,

而该接口的默认实现类最终会使用java.net.HttpURLConnection来发送HTTP请求。

5.1.5请求参数与返回对象

  本案例中有两个服务,另外一个地址为/person/{personld},需要传入参数并且返回 JSON字符串。编写第二个Feign客户端,调用该服务。

新建PersonClient服务类,定义调 用接口并添加注解,请见代码:

1

2

3

4

5

6

7

8

9

10

11

public interface Personclient {

  @RequestLine("GET /person/{personld}")

  Person findByld(@Param("personld") Integer personId);


  
@Data //为所有属性加上setter和getter等方法

  class Person {

    Integer id;

    String name;

    Integer age;

    String message;

  }

}

  

定义的接口名称为findByld,参数为personld。需要注意的是,由于会返回Person实 例,我们在接口中定义了一个Person的类;为了减少代码量,使用了 Lombok项目,使用 了该项目的@Data注解。

要使用Lombok,需要添加以下Maven依赖:

1

2

3

4

5

  org.projectlombok

  lombok

  l.16.18

  

准备好提供服务的客户端类后,再编写运行类。运行类基本上与前面的Hello World类 似,请见代码:

1

2

3

4

5

6

7

8

9

10

11

12

public class PersonMain {

  public static void main(String[] args) {

    Personclient personService = Feign.builder()

                          .decoder(new GsonDecoder())

                          .target(Personclient.class"http://localhost:8080/");

    Person person = personService.findByld(2);

    System.out.printIn(person.id);

    System.out.printIn(person.name);

    System.out.printin(person.age);

    System.out.printin(person.message);

  }

}

  

在调用Person服务的运行类中添加了解码器的配置,GsonDecoder会将返回的JSON 字符串转换为接口方法返回的对象,关于解码器的内容,将在后面章节中讲述。运行代码可以看到最终的输出。

本节使用了 CXF、Restlet.、Feign来编写REST客户端,在编写客户端的过程中,可以 看到Feign的代码更加“面向对象”,至于是否更加简洁,则见仁见智。

下面的章节,将深 入了解Feign的各项功能。

5.2 使用Feign

本节所有的案例都是单独使用Feign,Feign在Spring Cloud中的使用将在5.3节讲述, 请读者注意该细节。

服务端的项目,以5.1节的rest-server项目为基础,该项目是一个Spring Boot Web 项目。

5.2.1编码器

向服务发送请求的过程中,有些情况需要对请求的内容进行处理。例如服务端发布的 服务接收的是JSON格式的参数,而客户端使用的是对象,这种情况就可以使用编码器, 将对象转换为JSON字符串。

为服务端编写一个REST服务,处理POST请求,请见代码:

1

2

3

4

5

6

public class MyController{

  @RequestMapping(value = "/person/create", method = RequestMethod.POST,
              consumes = MediaType.APPLICATION_JSON_VALUE)

  public String createPerson(@RequestBody Person person) {

    System.out.printin(person.getName() + "--” + person.getAge());
    return "
Success, Person Id: " + person.getld();

  }

}

  在控制器中发布了一个/person/create服务,需要传入JSON格式的请求参数。在客户 端中,要调用该服务,先编写接口,再使用注解进行修饰,请见代码:

1

2

3

4

5

6

7

8

9

10

11

12

public interface Personclient {

  @RequestLine("POST /person/create")

  @Headers("Content-Type: application/json")

  String createPerson(Person person);

  
  
@Data

  class Person {

    Integer id;

    String name;

    Integer age;

    String message;

  }

}

  注意,在客户端的服务接口中,使用7@Headers注解,声明请求的内容类型为JSON, 接下来再编写运行类,如代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public class EncoderTest {

  public static void main(String[] args) {

    //获取服务接口

    Personclient personclient = Feign.builder()

          .encoder(new GsonEncoder())

          .target (Personclient.class, Hhttp://localhost:8080/");

    //创建参数的实例

    Person person = new Person();

    person.id = 1;

    person. name = "Angus'*;

    person.age = 30;

    String response = personclient.createPerson(person);

    System.out.printin(response);

  }

}

  

  在运行类中,在创建服务接口实例时,使用了 encoder方法来指定编码器,本案例使 用了 Feign提供的GsonEncoder类。该类会在发送请求的过程中,将请求的对象转换为JSON 字符串。

Feign支持插件式的编码器,如果Feign提供的编码器无法满足要求,还可以使用 自定义的编码器,这部分内容在后面章节讲述。启动服务,运行代码,可看到服务 已经调用成功,运行后输出如下:

1

Success, Person Id: 1

5.2.2解码器

编码器是对请求的内容进行处理,解码器则会对服务响应的内容进行处理,例如将解 析响应的JSON或者XML字符串,转换为我们所需要的对象,在代码中通过以下代码片断 设置解码器:

1

2

3

Personclient personservice = Feign.builder()

      .decoder(new GsonDecoder())

      .target(Personclient.class"http://localhost:8080/");

  

 5.2.3 XML的编码与解码

  除了支持JSON的处理外,Feign还为XML的处理提供了编码器与解码器,可以使用 JAXBEncoder与JAXBDecoder进行编码与解码。为服务端添加发布XML的接口,请见代码:

1

2

3

4

5

6

public class MyController{

  @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.printin(person.getName() +
"--"+ person.getld());

    return "success";

  }

}

  

  在服务端发布的服务方法中,声明了传入的参数为XML。需要注意的是,服务端项目 rest-server使用spring-boot-starter-web进行构建,默认情况下不支持XML接口,

调用接口 时会得到以下异常信息:

1

{
"timestamp"1502705981406,
"status" 415,
"error""Unsupported Media Type",
"exception""org.springframework.web.HttpMediaTypeNotSupportedException",
"message""Content type 'application/xml;charset=UTF-8 ' not supported",
"path":"/person/createXML"
}

  

为服务端的pom.xml加入以下依赖即可解决该问题:

1

2

3

4

  com.fasterxml.jackson.jaxrs

  jackson-jaxrs-xml-provider
  
2.9.0

  

编写客户端时,先定义好服务接口以及对象,接口请见代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

public interface Personclient {

  @RequestLine("POST /person/createXML")

  @Headers("Content-Type: application/xml")

  Result createPersonXML(Person person);

  @Data

  @XmlRootElement

  class Person {

    @XmlElement

    Integer id;

    @XmlElement

    String name;

    @XmlElement

    Integer age;

    @XmlElement

    String message;

  }

  
  
@Data

  @XmlRootElement

  class Result {

    @XmlElement

    String message;

  }

}

  在接口中,定义了 Content-Type为XML,使用了 JAXB的相关注解来修饰Person与 Resulto接下来,只需调用createPersonXML方法即可请求服务,请见代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public class XMLTest {

  public static void main(String[] args) {

    JZ\XBContextFactory jaxbFactory = new JTkXBContextFactory.Builder().build (); //获取服务接口

    Personclient personclient = Feign.builder()

        .encoder(new JAXBEncoder(jaxbFactory))

        .decoder(new JAXBDecoder(jaxbFactory))

        ・target(Personclient.class"http://localhost:8080/");

    //构建参数

    Person person = new Person();

    person.id = 1;

    person. name = "Angus'*;

    person.age = 30;

    //调用接口并返回结果

    Result result = personclient.createPersonXML(person);

    System.out.printin(result.message);

}

  本小节的请求有一点特殊,请求服务时传入的参数为XML的、返回的结果也是XML 的,目的是使编码与解码一起使用。开启服务,运行代码,可以看到服务端与客 户端的输出

5.2.4自定义编码器与解码器

根据前面两小节的介绍可知,Feign的插件式编码器与解码器可以对请求以及结果进行 处理。对于一些特殊的要求,可以使用自定义的编码器与解码器。实现自定义编码器,需 要实现Encoder接口的encode方法,

而对于解码器,则要实现Decoder接口的decode方法, 例如以下的代码片断:

1

2

3

4

5

public class MyEncoder implements Encoder {

  public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException (

    //实现自己的Encode逻辑

  }

}

  

在使用时,调用Feign的API来设置编码器或者解码器即可,实现较为简单;

5.2.5自定义Feign客户端

Feign使用一个Client接口来发送请求,默认情况下,使用HttpURLConnection连接 HTTP服务。与前面的编码器类似,客户端也釆用插件式设计,也就是说,我们可以实现 自己的客户端。

本小节将使用HttpClient来实现一个简单的Feign客户端。为pom.xml加入 HttpClient 的依赖:

1

2

3

4

5

  org.apache.httpcomponents

  httpclient

  4.5.2

  新建feignClient接口的实现类,具体实现请见代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

public class MyFeignClient implements Client {

  public Response execute(Request request, Options options)throws lOException {

    System.out.printin ("==== 这是自定义的 Feign 客户端");

    try (

      //创建一个默认的客户端

      CloseableHttpClient httpclient = HttpClients.createDefault();

      //获取调用的HTTP方法

      final String method = request.method();

      // 仓U建——个 HttpClient 的 HttpRequest

      HttpRequestBase httpRequest = new HttpRequestBase() {
        
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>())

            .status(httpResponse.getStatusLine().getStatusCode())
            .build();

      return response;

    } catch (Exception e) (

      throw new lOException(e);
    }
  }
}

  

简单讲一下自定义Feign客户端的实现过程。在实现execute方法时,将Feign的Request 实例转换为HttpClient的HttpRequestBaseo再使用CloseableHttpClient来执行请求,得到响 应的HttpResponse实例后,再转换为Feign的Response实例返回。

我们实现的客户端,包 括Feign自带的客户端以及其他扩展的客户端,实际上就是一个对象转换的过程。在运行类中直接使用我们自定义的客户端,请见代码:

1

2

3

4

5

6

7

8

9

10

public static void main(String[] args) {

  //获取服务接口

  Personclient personclient = Feign.builder()

            .encoder(new GsonEncoder())

            .client(new MyFeignClient())

            .target (Personclient.class"http://localhost:8080/");

  // 请求 Hello World 接口

  String result = personclient.sayHello();

  System, out .printin ("接口响应内容:"+ result);

}

  

运行代码清单5-14,输出如下:

1

2

====这是自定义的Feign客户端

接口响应内容:Hello World

  在本例的实现中,笔者简化了实现,自定义的客户端中并没有转换请求头等信息,因此使用本例的客户端,无法请求其他格式的服务。 T

虽然Feign也有HttpClient的实现,但本例的目的主要是向大家展示Feign客户端的原 理。举一反三,如果我们实现一个客户端,在实现中调用Ribbon的API来实现负载均衡的 功能,是完全可以实现的。

幸运的是,Feign已经帮我们实现了 RibbonClient,可以直接使 用,更进一步,Spring Cloud也实现了自己的Client,我们将在后面章节中讲述;

5.2.6使用第三方注解

根据前面章节的介绍可知,通过注解修改的接口方法,可以让接口方法获得访问服务 的能力。除了 Feign自带的方法外,还可以使用第三方的注解。

如果想使用JAXRS规范的 注解,可以使用Feign的feign-jaxrs模块,在pom.xml中加入以下依赖即可:

1

2

3

4

5

6

7

8

9

10

11

12

  io.github.openfeign

  feign-jaxrs

  9.5.0

  javax.ws.rs

  jsr311-api

  l.1.l

  

在使用注解修饰接口时,可以直接使用@GET、@Path等注解,例如想要使用GET方 法调用/hello服务,可以定义以下接口:

1

2

@GET @Path("/hello")

String rsHello();

  

以上修饰接口的,实际上等价于@RequestLine("GET /hello");为了让Feign知道这些 注解的作用,需要在创建服务客户端时调用contract方法来设置JAXRS注解的解析类,

请见以下代码:

1

RSClient rsClient = Feign.builder().contract(new JAXRSContract()).target(RSClient.class"http://localhost:8080/");

  

设置了 JAXRSContract类后,Feign就知道如何处理JAXRS的相关注解了

5.2.7 Feign解析第三方注解

根据前一小节的介绍可知,设置了 JAXRSContract后,Feign就知道如何处理接口中的 JAXRS 注解了。

JAXRSContract 继承了 BaseContract 类,BaseContract 类实现了 Contract 接口,简单来说,一个Contract就相当于一个翻译器,Feign本身并不知道这些第三方注解 的含义,而通过实现一个翻译器(Contract)来告诉Feign,这些注解是做什么的。

为了让读者能够了解其中的原理,本小节将使用一个自定义注解,并且翻译给Feign, 让其去使用。代码清单5-15所示为自定义注解以及客户端接口的代码。

1

2

3

4

5

6

7

8

9

10

11

@Target(METHOD)

@Retention(RUNTIME)

public @interface MyUrl {

  //定义ur丄与method属性

  String url ();

  String method();

}


public interface HelloClient {

  @MyUrl(method = "GET",url = "/hello")

  String myHello();

}

  

接下来,就要将MyUrl注解的作用告诉Feign,新建Contract继承BaseContract类,实现请见代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

public class MyContract extends Contract.BaseContract {

  @Override

  protected void processAnnotationOnClass(MethodMetadata data, Class clz) {

  }

  /**

   * 用于处理方法级的注解

   */

  protected void processAnnotationOnMethod(MethodMetadata data,Annotation annotation, Method method) {

    //是MyUrl注解才进行处理

    if(MyUrl.class.islnstance(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方法即可,请见代码:

1

2

3

4

5

6

7

8

9

10

11

public class ContractTest {

  public static void main(String[] args) {

    //获取服务接口

    HelloClient helloClient = Feign.builder()

        .contract(new MyContract())

        .target(HelloClient.class"http://localhost:8080/");

    // 请求 Hello World 接口

    String result = helloClient.myHello();

    System.out.printIn("接口响应内容:"+ result);

  }

}

  

运行代码,可看到控制台输出如下:

1

接口响应内容:Hello World

  由本例可知,Contract实际上承担的是翻译的作用,将第三方(或者自定义)注解的 作用告诉FeignO在Spring Cloud中,也实现了 Spring的Contract,可以在接口中使用 @RequestMapping 注解。

读者在学习 Spring Cloud 整合 Feign 时,见到使用@RequestMapping 修饰的接口,就可以明白其中的原理。

5.2.8请求拦截器

Feign支持请求拦截器,在发送请求前,可以对发送的模板进行操作,例如设置请求头 的属性等。

自定义请求拦截器,实现Requestinterceptor接口,在创建客户端时,调用相应 的方法设置一个或者多个拦截器,请见以下代码片断:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

//自定义拦截器

public class MyInterceptor implements RequestInterceptor {

  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: 8080/");

  }

}

  在使用时,根据实际情况进行设置即可

5.2.9接口日志

默认情况下,不会记录接口的日志,如果需要很清楚地了解接口的调用情况,可以使 用logLevel方法进行配置,请见以下代码:

1

2

3

4

5

//获取服务接口

Personclient personclient = Feign.builder()

  .logLevel(Logger.Level.FULL)

  .logger(new Logger.JavaLogger().appendToFile("logs/http.log"))

  .target(Personclient.class"http://localhost:8080/");
personclient.sayHello();

  

调用了 logLevel设置接口的日志级别,调用了 logger方法设置日志记录方式,本例是 输出到文件中,运行以上代码,再打开日志文件,可以看到接口的日志如下:

1

2

3

4

5

6

7

8

9

[PersonClient#sayHello] --->GET http://localhost:BOBO/hello HTTP/1 . 1 

[PersonClient#sayHello] - --> END HTTP (0-byte body) 

[PersonClient#sayHello] <--- HTTP/1 1 200 (llOms) 

[PersonClient#sayHello] content- length : 11

[PersonClient#sayHello] content-type : text/plain;charset=UTF-8

[PersonClient#sayHello] date : Wed , 16 Aug 2017 14:58:58 GMT 

[PersonClient#sayHello] 

[PersonClient#sayHello] Hello World 

[PersonClient#sayHello ]〈-- END HTTP ( 11-byte body)

  

以上日志,记录的就是一次请求的过程。设置接口的日志级别,有以下可选值:

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

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

5.3 在 Spring Cloud 中使用 Feign

前一节讲解了 Feign的使用,在了解了如何单独使用Feign后,再学习在Spring Cloud 中使用Feign,将会有非常大的帮助。虽然Spring Cloud对Feign进行了封装,但万变不离 其宗,只要了解其内在原理,使用起来就可以得心应手。

在开始本节的讲解前,先准备Spring Cloud的测试项目。测试案例主要有以下三个项 目

  • spring-feign-server: Eureka 服务器端项目,端口为 8761,代码目录为 codes\05\5.3\ spring-feign-server
  • spring-feign-provider:服务提供者,代码目录为 codes\05\5.3\spring-feign-provider, 该项目可以在控制台中根据输入的端口号启动多个实例,启动8080与8081这两个端口,该项目提供以下两个REST服务。

      第一个地址为/person/(personld}的服务,请求后返回Person实例,Person的 message属性为HTTP请求的URL

      第二个地址为/hello的服务,返回“Hello World”字符串。

  • spring-feign-invoker:服务调用者项目,对外端口为9000, 代码目录为codes\05\5.3\ spring-feign-invoker,本节的例子主要在该项目下使用Feign

5.3.1 Spring Cloud 整合 Feign

 为服务调用者(spring-feign・invoker)的pom.xml文件加入以下依赖:

1

2

3

  org.springframework.cloud
  spring-cloud-starter-feign

  

在服务调用者的启动类中,打开Feign开关,请见代码:

1

2

3

4

5

@EnableEurekaClient

@EnableFeignClients

public class InvokerApplication {

  public static void main(String[] args) {
    SpringApplication.run(InvokerApplication.
class, args);
  }

}

  

接下来,编写客户端接口,与直接使用Feign类似,代码所示为服务端接口:

1

2

3

public interface Personclient {

  @RequestMapping(method = RequestMethod.GET, value = "/hello")
  String hello();

}

  

  与单独使用Feign不同的是,接口使用了@FeignClient注解来修饰,并且声明了需要 调用的服务名称,本例的服务提供者名称为spring-feign-providero另外,接口方法使用了 @RequestMapping来修饰,

根据5.2.7节的介绍可知,通过编写"翻译器(Contract)”,可 以让Feign知道第三方注解的含义,Spring Cloud也提供翻译器,会将@RequestMapping注 解的含义告知Feign,因此我们的服务接口就可以直接使用该注解。

  除了方法的@RequestMapping 注解外,默认还支持@RequestParam、@RequestHeader、 @PathVariable这3个参数注解,也就是说,在定义方法时,可以使用以下方式定义参数:

1

2

@RequestMapping(method = RequestMethod.GET, value = "/hello/{name}")

String sayHello(@PathVariable("name") String name);

  

需要注意的是,使用了 Spring Cloud的“翻译器”后,将不能再使用Feign的默认注解。 接下来,在控制器中调用接口方法,请见代码:

1

2

3

4

5

6

7

8

9

@RestController

@Configuration

public class InvokerController {

  @Autowired

  private Personclient personclient;

  @RequestMapping(value = "/invokeHello", method = RequestMethod.GET)
  
public String invokeHello() {

    return personclient.hello();

  }

}

  

在控制器中,为其注入了 PersonClient的Bean,不难看出,客户端实例的创建及维护, Spring容器都帮我们实现了。

查看本例的效果,请按以下步骤操作:

  • 启动 Eureka 服务器(spring-feign-server)。
  • 启动两个服务提供者(spring-feign-provider),在控制台中分别输入8080与8081端口。
  • 启动一个服务调用者(spring-feign-invoker), 端口为 9000。
  • 在浏览器中输入http://localhost:9000/invokeHello,可以看到服务提供者的/hello服务被调用。

5.3.2 Feign负载均衡

在5.2节,我们尝试过编写自定义的Feign客户端,在Spring Cloud中,同样提供了自 定义的Feign客户端。大家可能已经猜到,如果结合Ribbon使用,Spring Cloud所提供的 客户端会拥有负载均衡的功能。

Spring Cloud实现的Feign客户端,类名为LoadBalancerFeignClient,在该类中,维护着与SpringClientFactory相关的实例。

通过SpringClientFactory可以获取负载均衡器,负载均衡器会根据一定的规则来选取处理请求的服务器,最终实现负载均衡的功能。

接下来, 调用服务提供者的/person/{personld}服务来测试负载均衡,为客户端接口添加内容,请见代码:

1

2

3

public class PersonClient{

  @RequestMapping(method = RequestMethod.GET, value = "/person/{personld)")
  Person getPerson(
@PathVariable("personld") Integer personld);

}

  

为服务调用者的控制器添加方法,请见如下代码:

1

2

3

4

5

6

@RequestMapping(value = "/router", method = RequestMethod.GET,
      produces = MediaType.APPLICATION_JSON_VALUE)

public String router() {

  //调用服务提供者的接口

  Person p = personclient.getPerson(2);

  return p.getMessage();

}

  运行服务调用者,在浏览器中输入http://localhost:9000/router,刷新,可以看到8080 与8081端口被循环调用。

5.3.3默认配置

 Spring Cloud为Feign的使用提供了各种默认属性,例如前面讲到的注解翻译器 (Contract)、Feign客户端。默认情况下,Spring将会为Feign的属性提供以下的Bean。

  • 解码器(Decoder) : Bean 名称为 feignDecoder, ResponseEntityDecoder 类。
  • 编码器(Encoder) : Bean 名称为 fbignEncoder, SpringEncoder 类。
  • 日志(Logger) : Bean 名称为 fbignLogger, Slf4jLogger 类。
  • 注解翻译器(Contract) : Bean 名称为 fbignContract, SpringMvcContract 类。
  • Feign 实例的创建者(Feign.Builder): Bean 名称为 feignBuilder, HystrixFeign.Builder 类。Hystrix框架将在后面章节中讲述。
  • Feign 客户端(Client) : Bean 名称为 feignClient, LoadBalancerFeignClient 类。

一般情况下,Spring提供的这些Bean己经足够我们使用,如果有些更特殊的需求,可以实现自己的Bean,请见下一小节。 

5.3.4自定义配置

 如果需要使用自己提供的Feign实现,可以在Spring的配置类中返回对应的Bean,下 面自定义一个简单的注解翻译器,代码是一个配置类。

1

2

3

4

5

6

7

8

9

10

@Configuration

public class MyConfig {

  /**

   * 返回一个自定义的注解翻译器

   */

  @Bean

  public Contract feignContract() {

    return new MyContract();

  }

}

  配置类中返回了一个MyContract实例,MyContract是我们自定义的“翻译器”,实现 请见代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

/**

 * 自定义Contract

 * @author杨恩雄

 */

public class MyContract extends SpringMvcContract {

  
  
/**
   * 用于处理方法级的注解

   */

  protected void processAnnotationOnMethod(MethodMetadata data,Annotation annotation, Method method) {

    //调用父类的方法,让其支持@RequestMapping注解

    super.processAnnotationOnMethod(data, annotation, method);

    //是MyUrl注解才进行处理

    if(MyUrl.class.islnstance(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);

    }

  }

}

  

在前面的章节中,我们也实现过自定义的Contract,与前面实现的Contract不同的是, 本例的 MyContract 继承了SpringMvcContract,在重写 processAnnotationOnMethod 方法时, 调用了父类的processAnnotationOnMethod;

简单点说,我们实现的这个Contract,除了支 持Spring的注解外,还支持我们自定义的@MyUrl注解。@MyUrl注解与前面章节中介绍 的一致,请见代码

1

2

3

4

5

6

7

@Target(METHOD)

@Retention(RUNTIME)

public @interface MyUrl {

  //定义url与method属性

  String url ();

  String method();

}

  接下来,编写客户端接口,可以使用Spring的@RequestMapping,或者是我们自定义 的@]由1}1'1注解,代码清单5-25所示为客户端接口

1

2

3

4

5

@FeignClient(name = "spring-feign-provider”)
public interface HelloClient {

  @MyUrl(method = "GET”, url = "/hello")

  String myHello();

  @RequestMapping(method = RequestMethod.GET, value = "/hello")
  String springHello();

}

  

在客户端接口中,分别使用了两个注解来调用同一个服务,接下来,在控制器中使用 HelloCliento代码:

1

2

3

4

5

6

7

8

9

10

11

12

public class lnvokerController{

  @Autowired

  private HelloClient helloClient;

  @RequestMapping(value = "/testcontractn, method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)

  @ResponseBody

  public String testcontract() {

    String springResult = helioClient.springHello();

    System.out.printin("使用 @RequestMapping 注解的接口返回结果:" + springResult);

    String myResult = helloClient.myHello();

    System.out.printin("使用 @MyUrl 注解的接口返回结果:"+ myResult);
    
return "";

  }

}

  向控制器注入客户端接口,testContract方法中分别调用两个hello方法。启动集群,访 问服务调用者的地址http://localhost:9000/testContract,可以看到控制台的输出如下:

1

2

使用@RequestMapping注解的接口返回结果:Hello World

使用@MyUrl注解的接口返回结果:Hello World

  

除了自定义的注解翻译器外,还可以自定义其他的Bean,实现过程基本一致

5.3.5 可选配置

在5.3.3节中介绍了若干个配置,Spring为这些配置提供了默认的Beano除了这些配置外,还有如下的配置,

Spring并没有提供默认的Bean

  • Logger.Level:接口日志的记录级别,相当于调用了 Feign.Builder的logLevel方法, 请见5.2.9节。
  • Retryer:重试处理器,相当于调用了 Feign.Builder的retryer方法。
  • ErrorDecoder:异常解码器,相当于调用了Feign.Builder 的 errorDecoder 方法。
  • Request.Options:设置请求的配置项,相当于调用了 Feign.Builder的options方法。
  • Collection:设置请求拦截器,相当于调用了 Feign.Builder 的 requestinterceptors 方法。
  • SetterFactory:该配置与Hystrix框架相关,将在后面章节详细讲述。

以上的配置,如果没有提供对应的Bean,则不会被设置。在此需要注意的是请求拦截 器,由于可以设置多个请求拦截器,在创建Bean时也可以创建多个,返回类型需要为 Requestinterceptor或者实现类。

要设置多个请求拦截器,请见以下代码片断:

1

2

3

4

5

6

7

8

9

10

11

12

@Bean

public RequestInterceptor getRequestlnterceptorsA () {
  
return new Requestinterceptor() {

    public void apply(RequestTemplate template) {
      System.out.printin ("这是第一个请求拦截器”);

    }

  };

}

@Bean

public RequestInterceptor getRequestlnterceptorsB () {
  
return new Requestinterceptor() {

    public void apply(RequestTemplate template) {
      System.out.printin (
"这是第二个请求拦截器");

    }

  };

}

  

5.3.6压缩配置

Feign支持对请求和响应进行压缩处理,默认使用GZIP进行压缩,压缩操作在Feign 的请求拦截器中实现。

可以在配置文件中加入以下配置:

  • feign.compression.request.enabled: 设置为 true 开启请求压缩。
  • feign.compression.response.enabled: 设置为 true 开启响应压缩。
  • feign.compression.request.mime-types:数据类型列表,默认值为 text/xml、application/xml、applicationjson
  • feign.compression.request.min-request-size:设置请求内容的最小阈值,默认值为 2048

5.4本章小结

   本章主要讲述了 Feign框架,Feign框架被集成到Spring Cloud的Netflix项目中,主要 作为REST客户端。

该框架的主要优点在于,它的插件式机制可以灵活地被整合到项目中。

  Spring Cloud对其进行了封装,本来使用就很简单的Feign,在Spring Cloud中使用更为简 单。

  Feign自带Ribbon模块,本身就具有负载均衡的能力,可以访问集群的服务。

5.2节主要以Feign的使用为核心,我们讲述了 Feign的几个重要组成部分。

5.3节, 我们讲述了 Feign在Spring Cloud中的使用。

学习完本章后,读者可以深刻了解Feign的机 制,以及其在Spring Cloud中所扮演的角色。

你可能感兴趣的:(spring,java,spring,开发语言)