《Spring Cloud微服务架构实战》--Rest客户端--Feign
《Spring Cloud微服务架构实战》-- 客户端 -- 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}服务,请见代码:
public class CxfClient { public static void main(String[] args) throws Exception ( // 创建 WebClient WebClient client = WebClient.create("http://localhost:8080/person/1"); //获取响应 Response response = client.get(); //获取响应内容 Inputstream ent = (Inputstream) response.getEntity(); String content = lOUtils.readStringFromStream(ent); //输出字符串 System.out.printin(content); } }
客户端中使用了 WebClient类发送请求,获取响应后读取输入流,获取服务返回的JSON 字符串。运行代码可看到返回的信息
5.1.2使用Restlet调用REST服务
Restlet是一个轻量级的REST框架,使用它可以发布和调用REST风格的Web Service;
本小节中的例子所使用的版本为2.3.10, Maven依赖如下:
org.restlet.jee org.restlet 2.3.10 org.restlet.jee org.restlet.ext.jackson 2.3.10
客户端实现请见代码:
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文件中增加以下配置:
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项目,加入以下依赖:
io.github.openfeign feign-core 9.5.0 io.github.openfeign feign-gson 9.5.0
新建接口 HelioClient,请见代码:
public interface HelioClient { @RequestLine("GET /hello") String sayHello(); }
HelioClient表示一个服务接口,在接口的sayHello方法中使用了@RequestLine注解, 表示使用GET方法向/hello发送请求。接下来编写客户端的运行类,请见代码:
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服务类,定义调 用接口并添加注解,请见代码:
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依赖:
org.projectlombok lombok l.16.18
准备好提供服务的客户端类后,再编写运行类。运行类基本上与前面的Hello World类 似,请见代码:
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请求,请见代码:
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格式的请求参数。在客户 端中,要调用该服务,先编写接口,再使用注解进行修饰,请见代码:
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, 接下来再编写运行类,如代码:
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提供的编码器无法满足要求,还可以使用 自定义的编码器,这部分内容在后面章节讲述。启动服务,运行代码,可看到服务 已经调用成功,运行后输出如下:
Success, Person Id: 1
5.2.2解码器
编码器是对请求的内容进行处理,解码器则会对服务响应的内容进行处理,例如将解 析响应的JSON或者XML字符串,转换为我们所需要的对象,在代码中通过以下代码片断 设置解码器:
Personclient personservice = Feign.builder() .decoder(new GsonDecoder()) .target(Personclient.class, "http://localhost:8080/");
5.2.3 XML的编码与解码
除了支持JSON的处理外,Feign还为XML的处理提供了编码器与解码器,可以使用 JAXBEncoder与JAXBDecoder进行编码与解码。为服务端添加发布XML的接口,请见代码:
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接口,
调用接口 时会得到以下异常信息:
{
"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加入以下依赖即可解决该问题:
com.fasterxml.jackson.jaxrs jackson-jaxrs-xml-provider
2.9.0
编写客户端时,先定义好服务接口以及对象,接口请见代码:
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方法即可请求服务,请见代码:
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方法, 例如以下的代码片断:
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 的依赖:
org.apache.httpcomponents httpclient 4.5.2
新建feignClient接口的实现类,具体实现请见代码:
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自带的客户端以及其他扩展的客户端,实际上就是一个对象转换的过程。在运行类中直接使用我们自定义的客户端,请见代码:
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,输出如下:
====这是自定义的Feign客户端 接口响应内容:Hello World
在本例的实现中,笔者简化了实现,自定义的客户端中并没有转换请求头等信息,因此使用本例的客户端,无法请求其他格式的服务。 T
虽然Feign也有HttpClient的实现,但本例的目的主要是向大家展示Feign客户端的原 理。举一反三,如果我们实现一个客户端,在实现中调用Ribbon的API来实现负载均衡的 功能,是完全可以实现的。
幸运的是,Feign已经帮我们实现了 RibbonClient,可以直接使 用,更进一步,Spring Cloud也实现了自己的Client,我们将在后面章节中讲述;
5.2.6使用第三方注解
根据前面章节的介绍可知,通过注解修改的接口方法,可以让接口方法获得访问服务 的能力。除了 Feign自带的方法外,还可以使用第三方的注解。
如果想使用JAXRS规范的 注解,可以使用Feign的feign-jaxrs模块,在pom.xml中加入以下依赖即可:
io.github.openfeign feign-jaxrs 9.5.0 javax.ws.rs jsr311-api l.1.l
在使用注解修饰接口时,可以直接使用@GET、@Path等注解,例如想要使用GET方 法调用/hello服务,可以定义以下接口:
@GET @Path("/hello") String rsHello();
以上修饰接口的,实际上等价于@RequestLine("GET /hello");为了让Feign知道这些 注解的作用,需要在创建服务客户端时调用contract方法来设置JAXRS注解的解析类,
请见以下代码:
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所示为自定义注解以及客户端接口的代码。
@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类,实现请见代码:
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方法即可,请见代码:
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); } }
运行代码,可看到控制台输出如下:
接口响应内容:Hello World
由本例可知,Contract实际上承担的是翻译的作用,将第三方(或者自定义)注解的 作用告诉FeignO在Spring Cloud中,也实现了 Spring的Contract,可以在接口中使用 @RequestMapping 注解。
读者在学习 Spring Cloud 整合 Feign 时,见到使用@RequestMapping 修饰的接口,就可以明白其中的原理。
5.2.8请求拦截器
Feign支持请求拦截器,在发送请求前,可以对发送的模板进行操作,例如设置请求头 的属性等。
自定义请求拦截器,实现Requestinterceptor接口,在创建客户端时,调用相应 的方法设置一个或者多个拦截器,请见以下代码片断:
//自定义拦截器 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方法进行配置,请见以下代码:
//获取服务接口 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方法设置日志记录方式,本例是 输出到文件中,运行以上代码,再打开日志文件,可以看到接口的日志如下:
[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文件加入以下依赖:
org.springframework.cloud
spring-cloud-starter-feign
在服务调用者的启动类中,打开Feign开关,请见代码:
@EnableEurekaClient @EnableFeignClients public class InvokerApplication { public static void main(String[] args) {
SpringApplication.run(InvokerApplication.class, args);
} }
接下来,编写客户端接口,与直接使用Feign类似,代码所示为服务端接口:
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个参数注解,也就是说,在定义方法时,可以使用以下方式定义参数:
@RequestMapping(method = RequestMethod.GET, value = "/hello/{name}") String sayHello(@PathVariable("name") String name);
需要注意的是,使用了 Spring Cloud的“翻译器”后,将不能再使用Feign的默认注解。 接下来,在控制器中调用接口方法,请见代码:
@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}服务来测试负载均衡,为客户端接口添加内容,请见代码:
public class PersonClient{ @RequestMapping(method = RequestMethod.GET, value = "/person/{personld)")
Person getPerson(@PathVariable("personld") Integer personld); }
为服务调用者的控制器添加方法,请见如下代码:
@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,下 面自定义一个简单的注解翻译器,代码是一个配置类。
@Configuration public class MyConfig { /** * 返回一个自定义的注解翻译器 */ @Bean public Contract feignContract() { return new MyContract(); } }
配置类中返回了一个MyContract实例,MyContract是我们自定义的“翻译器”,实现 请见代码:
/** * 自定义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注解与前面章节中介绍 的一致,请见代码
@Target(METHOD) @Retention(RUNTIME) public @interface MyUrl { //定义url与method属性 String url (); String method(); }
接下来,编写客户端接口,可以使用Spring的@RequestMapping,或者是我们自定义 的@]由1}1'1注解,代码清单5-25所示为客户端接口
@FeignClient(name = "spring-feign-provider”)
public interface HelloClient { @MyUrl(method = "GET”, url = "/hello") String myHello();
@RequestMapping(method = RequestMethod.GET, value = "/hello")
String springHello(); }
在客户端接口中,分别使用了两个注解来调用同一个服务,接下来,在控制器中使用 HelloCliento代码:
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,可以看到控制台的输出如下:
使用@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或者实现类。
要设置多个请求拦截器,请见以下代码片断:
@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中所扮演的角色。