使用 spring cloud openfeign 的一些小技巧

spring cloud openfeign(以下简称 feign) 通过一个额外定义的 interface 文件作为接口定义,可以将对外提供的 HTTP 接口转换为 API 接口,提供方和调用方需要共同依赖接口文件,将隐式的依赖关系显性表示出来。而且在这个接口文件上也可以大作文章,比如配置服务发现、接口拦截操作等。

一个最简单的 feign 接口文件 DemoClient.java

1
2
3
4
5
6
7
8
package com.example.demo;

@FeignClient(name="demo", url="http://127.0.0.1:8081/")
public interface DemoClient {

	@GetMapping("/hello")
	String hello(@RequestParam String name);
}

name 为全局唯一,是这个 FeignClient 的唯一标识,url 为提供方的接口地址。理论上 FeignClient 文件由接口提供方作为合约文件给到调用方,但是即使提供方未提供,只要提供方暴露了 HTTP 接口,那么调用方就可以通过定义 FeignClient 文件将 HTTP 接口调用转换为 API 调用。

调用方使用 DemoClient 示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.example.demo;

@Service
public class DemoService {

	@Autowired
	private DemoClient client;
	
	public String getHelloMessage() {
		String name = "kyon";
		String msg = client.hello(name);
		return msg;
	}
}

怎么样,是不是完全不会意识到 client.hello(name) 居然是在调用 HTTP 接口!

配置服务发现

除了通过 url 获取到调用方的地址,在微服务以及 k8s 如此横行的当下,提供方服务随时会更换 ip,那么引入服务发现机制就很有必要了,这样只要服务名称不变,就可以拿到提供方地址,而不需要再指定 url。更进一步地说,如果提供方有水平部署了多个,feign 还(通过 ribbon)顺便帮忙做了负载。

假设提供方通过引入依赖包 spring-cloud-starter-zookeeper-discovery 开启了基于 zookeeper 的服务发现,那么如下所示将 DemoClient.java 中的 name 设置为提供方的应用名(假设 spring.application.name=demo-app),就可以通过服务发现机制自动获得提供方地址,不再需要配置 url 属性(如果配置了 url 属性的话,就会覆盖掉服务发现提供的地址):

1
2
3
4
5
6
7
package com.example.demo;

@FeignClient(name="demo-app")
public interface DemoClient {
	@GetMapping("/hello")
	String hello(@RequestParam String name);
}

如果提供方需要提供不同的 FeignCliet, 该如何区分呢?可以通过设置不同的 ContextId 做到,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/*DemoClient1.java*/
package com.example.demo;

@FeignClient(name="demo-app", contextId="demo1")
public interface DemoClient1 {
	@GetMapping("/hello")
	String hello(@RequestParam String name);
}

/*DemoClient2.java*/
package com.example.demo;

@FeignClient(name="demo-app", contextId="demo2")
public interface DemoClient2 {
	@GetMapping("/hello")
	String hello(@RequestParam String name);
}

目前这边的使用场景是,对于不同的调用方,提供不同的 client 进行隔离。

配置 FeignClient

有两种配置方式,一种是在配置文件比如 properties 文件中配置,另外一种是通过类文件配置。最开始我是倾向于配置文件中单独配置,因为比较简洁,但是后来遇到不同版本代码共用一个配置中心导致的问题,现在倾向于在代码中通过类文件配置。

通过配置文件配置

首先,需要定义好自定义类比如设置登录校验头的 CustomRequestInterceptor.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package com.example.demo;

public class CustomRequestInterceptor implements RequestInterceptor {  

	@Override  
	public void apply(RequestTemplate template) {
		String headerValue = "Bearer-token:xxx";  
		template.header("Authorization", headerValue);  
	}  
}

配置方式

1
2
3
4
5
## 对于所有 feign client 生效
feign.client.config.default.read-timeout=60000

## 仅对于 demo-app feign client 生效
feign.client.config.demo-app.requestInterceptors=com.example.demo.CustomRequestInterceptor

除了 requestInterceptors 外,feign 还有各种配置项,比如 decoder 等,可以自行上网搜索。

通过类文件配置

新增一个配置文件:

1
2
3
4
5
6
7
8
package com.example.demo;

public class FeignConfig {
	@Bean  
	public RequestInterceptor interceptor() {  
		return new CustomRequestInterceptor();  
	}
}

把 demo-app 的 feign client 拷贝过来,并加上 config 配置:

1
2
3
4
5
6
7
package com.example.demo;

@FeignClient(name="demo-app", configuration = FeignConfig.class)
public interface DemoClient {
	@GetMapping("/hello")
	String hello(@RequestParam String name);
}

这就成了。

打开调试日志

将第三方提供的 HTTP 接口通过 feign client 方式转换为 API 接口进行调用时,比较常见的问题是,可能参数没有传对或者什么,导致接口调用报错,但是 HTTP 原始的报错被 feign 给吞掉了,给调试带来了麻烦,这时候,只要把 feign 的 debug 日志打开,这个问题就迎刃而解了!打开方式如下:

1
2
3
feign.client.config.demo-app.loggerLevel=FULL

logging.level.com.example.demo.DemoClient=DEBUG

总结

目前来说,这边认为 feign 使用起来还是比较顺手的,可能多了一步 FeignClient 接口定义,但是考虑到在调用时省下的代码,强制表明依赖,支持服务发现以及负载均衡等好处,还是可以回本的,而且可以自定义接口拦截,这样即使有登录验证也没问题了。而调试的时候可以打开完整的 trace 日志,可以看到清晰的 HTTP 交谈信息,不会因为包了一层而变得不透明。日常使用起来足够了。