Feign入门

Feign入门

Feign是Github上的一个开源项目,目的是简化Web Service客户端的开发。在使用Feign时,可以使用注解来修饰接口,被注解修饰的接口具有访问Web Service的能力。这些注解中既包括Feign自带的注解,也支持使用第三方注解。除此之外,Feign还支持插件式的编码器和解码器,使用者可以通过该特性对请求和响应进行不同和的封装和解封。

第一个Feign程序

Feign-Client 端代码

新建一个Maven项目Feign-Client,作为请求发起端,其pom依赖为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependencies>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>9.5.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>9.5.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-gson</artifactId>
<version>8.18.0</version>
</dependency>
</dependencies>

然后新建一个接口,命名为HelloClient,添加以下代码:

1
2
3
4
public interface HelloClient {
@RequestLine("GET /hello")
String sayHello();
}

注解@RequestLine表示使用GET方法向/hello发送请求。

接着再新建一个接口,命名为PersonClient,添加以下代码:

1
2
3
4
5
6
public interface PersonClient {

@RequestLine("GET /person/{personId}")
Person findById(@Param("personId") Integer personId);
}

同样的,注解@RequestLine表示使用GET方法,向链接/person/{personId}发送请求。

其中Person对象的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
@Data
public class Person {
String name;

int age;

int id;

String message;
}

最后,编写测试代码,

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestClient {
public static void main(String[] args) {
HelloClient test = Feign.builder().target(HelloClient.class, "http://localhost:8080/");
System.out.println(test.sayHello());

PersonClient personClient = Feign.builder().decoder(new GsonDecoder()).target(PersonClient.class, "http://localhost:8080");
Person person = personClient.findById(1);
System.out.println(person.id);
System.out.println(person.age);
System.out.println(person.name);
System.out.println(person.message);
}
}

可以看到,我们使用了Feign创建了HelloClient和PersonClient的实例,接着调用接口定义的方法。Feign就会替我们生成动态代理类,生成的代理类会将请求的信息封装,交给feign.client接口发送请求,而该接口的默认实现类最终会使用java.net.HttpURLConnection来发送HTTP请求。

Feign-Server端代码

接着,创建一个服务端代码,提供/hello和/person/{personId}这两个接口。因为比较简单,就直接贴代码了。

pom文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>

接口Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class FirstCloudController {

@RequestMapping(value = "/person/{personId}", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public Person findPerson(@PathVariable("personId") Integer personId, HttpServletRequest request) {
Person person = new Person(personId, "Crazyit", 30);
person.setMessage(request.getRequestURL().toString());
return person;
}

@RequestMapping(value = "/hello", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public String sayHello() {
return "hello, hello";
}
}

启动类

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
@EnableEurekaClient
public class FirstCloudServiceProvider {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String port = scanner.nextLine();
new SpringApplicationBuilder(FirstCloudServiceProvider.class).properties("server.port=" + port).run(args);
}
}

运行客户端请求

运行Client的主方法,可以得到如下结果:

运行结果

实现自定义的编码和解码器

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

实现自定义encoder

下面是一个自定义编码器的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyEncoder implements Encoder {
private GsonEncoder gsonEncoder;

public MyEncoder() {
gsonEncoder = new GsonEncoder();
}

public void encode(Object o, Type type, RequestTemplate requestTemplate) throws EncodeException {
System.out.println("encode object is class" + type.getClass().getName());

System.out.println("encode object is value" + type);

System.out.println("encode bodyType is class" + type.getClass().getName());

System.out.println("encode bodyType is value" + type);

gsonEncoder.encode(o, type, requestTemplate);
}
}

实现自定义decoder

实现自定义解码器,需要实现Decoder接口的decode方法。下面是一个自定义编码器的简易实现:

1
2
3
4
5
6
7
8
9
10
11
12

public class MyDecoder implements Decoder {
private GsonDecoder gsonDecoder;

public MyDecoder() {
gsonDecoder= new GsonDecoder();
}

public Object decode(Response response, Type type) throws IOException, FeignException {
return gsonDecoder.decode(response,type);
}
}

然后,在测试类中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestClient {
public static void main(String[] args) {
HelloClient test = Feign.builder().encoder(new MyEncoder()).target(HelloClient.class, "http://localhost:8080/");
System.out.println(test.sayHello());

PersonClient personClient = Feign.builder().decoder(new MyDecoder()).target(PersonClient.class, "http://localhost:8080");
Person person = personClient.findById(1);
System.out.println(person.id);
System.out.println(person.age);
System.out.println(person.name);
System.out.println(person.message);
}
}

最后得到运行结果

运行结果

实现自定义的Feign客户端

Feign使用一个Client接口来发送请求,默认情况下,使用HttpURLConnection连接Http服务。与编码器类似,客户端也采用插件式设计,也就是说,我们可以实现自己的客户端。只要实现Feign的Client接口并实现它的execute方法即可。

下面我们自己实现一个简单的HttpClient客户端

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
27
28
29
public class MyFeignClient implements Client {
public Response execute(Request request, Request.Options options) throws IOException {
System.out.println("========== 这是自定义的Feign客户端");

try {
// 创建一个默认的客户端
CloseableHttpClient httpClient = HttpClients.createDefault();
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());
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 Client

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
HelloClient test = Feign.builder().client(new MyFeignClient()).target(HelloClient.class, "http://localhost:8080/");
System.out.println(test.sayHello());

PersonClient personClient = Feign.builder().client(new MyFeignClient()).decoder(new GsonDecoder()).target(PersonClient.class, "http://localhost:8080");
Person person = personClient.findById(1);
System.out.println(person.id);
System.out.println(person.age);
System.out.println(person.name);
System.out.println(person.message);
}

可以看到能正常获取结果

运行结果

使用自定义的注解

通过注解修改的接口方法,可以让接口方法获得访问服务的能力。除了Feign自带的方法外,还可以使用第三方的注解。如果向使用JAXRS贵方的注解,可以使用Feign-jaxrs模块,在Pom文件中加入以下依赖即可:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jaxrs</artifactId>
<version>9.5.0</version>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
<version>1.1.1</version>
</dependency>

在使用注解修饰接口时,可以直接使用@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注解了。JAXRSContract继承了BaseContract类,BaseContract类实现了Contract接口,简单来说,一个Contract就相当于一个翻译器,Feign本身并不知道这些第三方注解的含义,而是通过实现一个翻译器(Contract)来告诉Feign,这些注解是做什么的。

下面实现一个自定义的注解,来解释下,是如何使用自定义注解完成必要的功能的。

1
2
3
4
5
6
7
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyUrl {
String url();
String method();
}

然后我们将HelloClient的RequestLine注解改为自定义的MyUrl注解

1
2
3
4
 public interface HelloClient {
@MyUrl(method = "GET", url = "/hello")
String sayHello();
}

然后实现一个自己的翻译器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
25
26
27
28
29
30
public class MyContract extends Contract.BaseContract {
@Override
protected void processAnnotationOnClass(MethodMetadata methodMetadata, Class<?> aClass) {
// 处理类注解的方法
}

// 处理方法注解的方法
@Override
protected void processAnnotationOnMethod(MethodMetadata methodMetadata, Annotation annotation, Method method) {
// 是MyUrl的注解才进行处理
if (annotation instanceof MyUrl) {
// 获取注解的实例
MyUrl myUrlAnn = method.getAnnotation(MyUrl.class);
// 获取配置的HTTP方法
String httpMethod = myUrlAnn.method();
// 获取服务的URL
String url = myUrlAnn.url();
// 将值设置到模板中
methodMetadata.template().method(httpMethod);
methodMetadata.template().append(url);
}
}

@Override
protected boolean processAnnotationsOnParameter(MethodMetadata methodMetadata, Annotation[] annotations, int i) {
// 处理参数注解的方法
return false;
}
}

最后,修改测试类,将调用HelloClient处插入自定义的Contract。

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
HelloClient test = Feign.builder().contract(new MyContract()).target(HelloClient.class, "http://localhost:8080/");
System.out.println(test.sayHello());

PersonClient personClient = Feign.builder().client(new MyFeignClient()).decoder(new GsonDecoder()).target(PersonClient.class, "http://localhost:8080");
Person person = personClient.findById(1);
System.out.println(person.id);
System.out.println(person.age);
System.out.println(person.name);
System.out.println(person.message);
}

运行结果:

运行结果

接口日志

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.kingwang.feign.client;

import feign.Feign;
import feign.Logger;
import feign.gson.GsonDecoder;

public class TestClient {
public static void main(String[] args) {
HelloClient test = Feign.builder().contract(new MyContract()).logLevel(Logger.Level.HEADERS).logger(new Logger.JavaLogger().appendToFile("D:/log.log")).target(HelloClient.class, "http://localhost:8080/");
System.out.println(test.sayHello());

PersonClient personClient = Feign.builder().client(new MyFeignClient()).decoder(new GsonDecoder()).target(PersonClient.class, "http://localhost:8080");
Person person = personClient.findById(1);
System.out.println(person.id);
System.out.println(person.age);
System.out.println(person.name);
System.out.println(person.message);
}
}

设置日志级别为Level.HEADERS,并将其输出到D盘的log.log文件。

运行结果

设置接口的日志级别,有以下可选值:

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

参考资料

疯狂Spring Cloud微服务架构实战

0%