在Spring Cloud集群中,各个角色的通信基于REST服务,因此在调用服务时,就不可 避免地需要使用REST服务的请求客户端。
前面的章节中使用了 Spring自带的RestTemplate, RestTemplate使用HttpClient发送请求。
本章中,我们将介绍另一个REST客户端:Feign
Feign框架已经被集成到Spring Cloud的Netflix项目中,使用该框架可以在Spring Cloud 的集群中更加简单地调用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 |
|
客户端实现请见代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
代码清单中使用的Restlet的API较为简单,在此不赘述。但需要注意的是,在 Maven中使用Restlet,要额外配置仓库地址,笔者成书时,在Apache官方仓库中并没有 Restlet的包。
在项目的pom.xml文件中增加以下配置:
1 2 3 4 5 6 7 |
|
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 |
|
新建接口 HelioClient,请见代码:
1 2 3 4 |
|
HelioClient表示一个服务接口,在接口的sayHello方法中使用了@RequestLine注解, 表示使用GET方法向/hello发送请求。接下来编写客户端的运行类,请见代码:
1 2 3 4 5 6 7 |
|
在运行类中,使用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 |
|
定义的接口名称为findByld,参数为personld。需要注意的是,由于会返回Person实 例,我们在接口中定义了一个Person的类;为了减少代码量,使用了 Lombok项目,使用 了该项目的@Data注解。
要使用Lombok,需要添加以下Maven依赖:
1 2 3 4 5 |
|
准备好提供服务的客户端类后,再编写运行类。运行类基本上与前面的Hello World类 似,请见代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
在调用Person服务的运行类中添加了解码器的配置,GsonDecoder会将返回的JSON 字符串转换为接口方法返回的对象,关于解码器的内容,将在后面章节中讲述。运行代码可以看到最终的输出。
本节使用了 CXF、Restlet.、Feign来编写REST客户端,在编写客户端的过程中,可以 看到Feign的代码更加“面向对象”,至于是否更加简洁,则见仁见智。
下面的章节,将深 入了解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 |
|
在控制器中发布了一个/person/create服务,需要传入JSON格式的请求参数。在客户 端中,要调用该服务,先编写接口,再使用注解进行修饰,请见代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
注意,在客户端的服务接口中,使用7@Headers注解,声明请求的内容类型为JSON, 接下来再编写运行类,如代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
在运行类中,在创建服务接口实例时,使用了 encoder方法来指定编码器,本案例使 用了 Feign提供的GsonEncoder类。该类会在发送请求的过程中,将请求的对象转换为JSON 字符串。
Feign支持插件式的编码器,如果Feign提供的编码器无法满足要求,还可以使用 自定义的编码器,这部分内容在后面章节讲述。启动服务,运行代码,可看到服务 已经调用成功,运行后输出如下:
1 |
|
5.2.2解码器
编码器是对请求的内容进行处理,解码器则会对服务响应的内容进行处理,例如将解 析响应的JSON或者XML字符串,转换为我们所需要的对象,在代码中通过以下代码片断 设置解码器:
1 2 3 |
|
5.2.3 XML的编码与解码
除了支持JSON的处理外,Feign还为XML的处理提供了编码器与解码器,可以使用 JAXBEncoder与JAXBDecoder进行编码与解码。为服务端添加发布XML的接口,请见代码:
1 2 3 4 5 6 |
|
在服务端发布的服务方法中,声明了传入的参数为XML。需要注意的是,服务端项目 rest-server使用spring-boot-starter-web进行构建,默认情况下不支持XML接口,
调用接口 时会得到以下异常信息:
1 |
|
为服务端的pom.xml加入以下依赖即可解决该问题:
1 2 3 4 |
|
编写客户端时,先定义好服务接口以及对象,接口请见代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
在接口中,定义了 Content-Type为XML,使用了 JAXB的相关注解来修饰Person与 Resulto接下来,只需调用createPersonXML方法即可请求服务,请见代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
本小节的请求有一点特殊,请求服务时传入的参数为XML的、返回的结果也是XML 的,目的是使编码与解码一起使用。开启服务,运行代码,可以看到服务端与客 户端的输出
5.2.4自定义编码器与解码器
根据前面两小节的介绍可知,Feign的插件式编码器与解码器可以对请求以及结果进行 处理。对于一些特殊的要求,可以使用自定义的编码器与解码器。实现自定义编码器,需 要实现Encoder接口的encode方法,
而对于解码器,则要实现Decoder接口的decode方法, 例如以下的代码片断:
1 2 3 4 5 |
|
在使用时,调用Feign的API来设置编码器或者解码器即可,实现较为简单;
5.2.5自定义Feign客户端
Feign使用一个Client接口来发送请求,默认情况下,使用HttpURLConnection连接 HTTP服务。与前面的编码器类似,客户端也釆用插件式设计,也就是说,我们可以实现 自己的客户端。
本小节将使用HttpClient来实现一个简单的Feign客户端。为pom.xml加入 HttpClient 的依赖:
1 2 3 4 5 |
|
新建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 |
|
简单讲一下自定义Feign客户端的实现过程。在实现execute方法时,将Feign的Request 实例转换为HttpClient的HttpRequestBaseo再使用CloseableHttpClient来执行请求,得到响 应的HttpResponse实例后,再转换为Feign的Response实例返回。
我们实现的客户端,包 括Feign自带的客户端以及其他扩展的客户端,实际上就是一个对象转换的过程。在运行类中直接使用我们自定义的客户端,请见代码:
1 2 3 4 5 6 7 8 9 10 |
|
运行代码清单5-14,输出如下:
1 2 |
|
在本例的实现中,笔者简化了实现,自定义的客户端中并没有转换请求头等信息,因此使用本例的客户端,无法请求其他格式的服务。 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 |
|
在使用注解修饰接口时,可以直接使用@GET、@Path等注解,例如想要使用GET方 法调用/hello服务,可以定义以下接口:
1 2 |
|
以上修饰接口的,实际上等价于@RequestLine("GET /hello");为了让Feign知道这些 注解的作用,需要在创建服务客户端时调用contract方法来设置JAXRS注解的解析类,
请见以下代码:
1 |
|
设置了 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 |
|
接下来,就要将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 |
|
在MyContract类中,需要实现三个方法,分别是处理类注解、处理方法注解、处理参 数注解的方法,由于我们只定义了 一个方法注解@MyUrl ,因此实现 processAnnotationOnMethod 即可。
在 processAnnotationOnMethod 方法中,通过 Method的 getAnnotation 获取 MyUrl 的实例,将MyUrl的url、method属性分别设置到Feign的模板中。
在创建客户端时,再调用 contract方法即可,请见代码:
1 2 3 4 5 6 7 8 9 10 11 |
|
运行代码,可看到控制台输出如下:
1 |
|
由本例可知,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 |
|
在使用时,根据实际情况进行设置即可
5.2.9接口日志
默认情况下,不会记录接口的日志,如果需要很清楚地了解接口的调用情况,可以使 用logLevel方法进行配置,请见以下代码:
1 2 3 4 5 |
|
调用了 logLevel设置接口的日志级别,调用了 logger方法设置日志记录方式,本例是 输出到文件中,运行以上代码,再打开日志文件,可以看到接口的日志如下:
1 2 3 4 5 6 7 8 9 |
|
以上日志,记录的就是一次请求的过程。设置接口的日志级别,有以下可选值:
记录接口日志的调用过程可以很方便地查找问题,不管在开发环境还是生产环境,都有较大的意义。
前一节讲解了 Feign的使用,在了解了如何单独使用Feign后,再学习在Spring Cloud 中使用Feign,将会有非常大的帮助。虽然Spring Cloud对Feign进行了封装,但万变不离 其宗,只要了解其内在原理,使用起来就可以得心应手。
在开始本节的讲解前,先准备Spring Cloud的测试项目。测试案例主要有以下三个项 目
第一个地址为/person/(personld}的服务,请求后返回Person实例,Person的 message属性为HTTP请求的URL
第二个地址为/hello的服务,返回“Hello World”字符串。
5.3.1 Spring Cloud 整合 Feign
为服务调用者(spring-feign・invoker)的pom.xml文件加入以下依赖:
1 2 3 |
|
在服务调用者的启动类中,打开Feign开关,请见代码:
1 2 3 4 5 |
|
接下来,编写客户端接口,与直接使用Feign类似,代码所示为服务端接口:
1 2 3 |
|
与单独使用Feign不同的是,接口使用了@FeignClient注解来修饰,并且声明了需要 调用的服务名称,本例的服务提供者名称为spring-feign-providero另外,接口方法使用了 @RequestMapping来修饰,
根据5.2.7节的介绍可知,通过编写"翻译器(Contract)”,可 以让Feign知道第三方注解的含义,Spring Cloud也提供翻译器,会将@RequestMapping注 解的含义告知Feign,因此我们的服务接口就可以直接使用该注解。
除了方法的@RequestMapping 注解外,默认还支持@RequestParam、@RequestHeader、 @PathVariable这3个参数注解,也就是说,在定义方法时,可以使用以下方式定义参数:
1 2 |
|
需要注意的是,使用了 Spring Cloud的“翻译器”后,将不能再使用Feign的默认注解。 接下来,在控制器中调用接口方法,请见代码:
1 2 3 4 5 6 7 8 9 |
|
在控制器中,为其注入了 PersonClient的Bean,不难看出,客户端实例的创建及维护, Spring容器都帮我们实现了。
查看本例的效果,请按以下步骤操作:
5.3.2 Feign负载均衡
在5.2节,我们尝试过编写自定义的Feign客户端,在Spring Cloud中,同样提供了自 定义的Feign客户端。大家可能已经猜到,如果结合Ribbon使用,Spring Cloud所提供的 客户端会拥有负载均衡的功能。
Spring Cloud实现的Feign客户端,类名为LoadBalancerFeignClient,在该类中,维护着与SpringClientFactory相关的实例。
通过SpringClientFactory可以获取负载均衡器,负载均衡器会根据一定的规则来选取处理请求的服务器,最终实现负载均衡的功能。
接下来, 调用服务提供者的/person/{personld}服务来测试负载均衡,为客户端接口添加内容,请见代码:
1 2 3 |
|
为服务调用者的控制器添加方法,请见如下代码:
1 2 3 4 5 6 |
|
运行服务调用者,在浏览器中输入http://localhost:9000/router,刷新,可以看到8080 与8081端口被循环调用。
5.3.3默认配置
Spring Cloud为Feign的使用提供了各种默认属性,例如前面讲到的注解翻译器 (Contract)、Feign客户端。默认情况下,Spring将会为Feign的属性提供以下的Bean。
一般情况下,Spring提供的这些Bean己经足够我们使用,如果有些更特殊的需求,可以实现自己的Bean,请见下一小节。
5.3.4自定义配置
如果需要使用自己提供的Feign实现,可以在Spring的配置类中返回对应的Bean,下 面自定义一个简单的注解翻译器,代码是一个配置类。
1 2 3 4 5 6 7 8 9 10 |
|
配置类中返回了一个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,与前面实现的Contract不同的是, 本例的 MyContract 继承了SpringMvcContract,在重写 processAnnotationOnMethod 方法时, 调用了父类的processAnnotationOnMethod;
简单点说,我们实现的这个Contract,除了支 持Spring的注解外,还支持我们自定义的@MyUrl注解。@MyUrl注解与前面章节中介绍 的一致,请见代码
1 2 3 4 5 6 7 |
|
接下来,编写客户端接口,可以使用Spring的@RequestMapping,或者是我们自定义 的@]由1}1'1注解,代码清单5-25所示为客户端接口
1 2 3 4 5 |
|
在客户端接口中,分别使用了两个注解来调用同一个服务,接下来,在控制器中使用 HelloCliento代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
向控制器注入客户端接口,testContract方法中分别调用两个hello方法。启动集群,访 问服务调用者的地址http://localhost:9000/testContract,可以看到控制台的输出如下:
1 2 |
|
除了自定义的注解翻译器外,还可以自定义其他的Bean,实现过程基本一致
5.3.5 可选配置
在5.3.3节中介绍了若干个配置,Spring为这些配置提供了默认的Beano除了这些配置外,还有如下的配置,
Spring并没有提供默认的Bean
以上的配置,如果没有提供对应的Bean,则不会被设置。在此需要注意的是请求拦截 器,由于可以设置多个请求拦截器,在创建Bean时也可以创建多个,返回类型需要为 Requestinterceptor或者实现类。
要设置多个请求拦截器,请见以下代码片断:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
5.3.6压缩配置
Feign支持对请求和响应进行压缩处理,默认使用GZIP进行压缩,压缩操作在Feign 的请求拦截器中实现。
可以在配置文件中加入以下配置:
本章主要讲述了 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中所扮演的角色。