spring openfeign 自定义路径调用

算算时间竟有半年多没写博客了,其实这些时间也折腾了不少东西,比如体验了下 ChatGPT,把它接入到微信里做机器人,扩展研究了下逆向微信的消息收发,后面有空再写写相关内容吧,本次主要是记录下关于项目中openfeign的自定义使用。

Spring Cloud OpenFeign 是什么?

Spring Cloud OpenFeign 是基于 Feign 的声明式 Web 服务客户端。它使编写 Web 服务客户端变得更加容易。Feign 本身的一些优秀的特性也被继承,如:

  • 支持多种编解码方式, jackson, Gson 等
  • 支持多种调用通讯端,如:Apache HTTP, OK HTTP 等
  • 支持指标监控
  • 日志,断路器等其他杂项功能

关于其使用比较简单,以下是一个官方的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SpringBootApplication
@EnableFeignClients
public class Application {

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

}
@FeignClient("stores")
public interface StoreClient {
@RequestMapping(method = RequestMethod.GET, value = "/stores")
List<Store> getStores();

@RequestMapping(method = RequestMethod.GET, value = "/stores")
Page<Store> getStores(Pageable pageable);

@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
Store update(@PathVariable("storeId") Long storeId, Store store);
}

可以看到接口的定义方式类似 Spring MVC,消除了具体的调用实现,显的更为清楚简洁。

遇到的问题

OpenFeign 的使用是简洁的,但并不是完美的,在使用中会遇到一个问题即:请求地址路径不固定,虽然可以通过 profile 机制在不同的场景下切换路径,但这种方式仍然无法实现路径的热更新。

着手解决

众所周知 Spring 是模式大师,经过其抽象定义,原本的使用方式就会被包装起来,所以要解决路径动态切换的问题可以从Feign的使用中去倒推。

Feign使用中路径的切入比较简单,看看官方的例子

1
Feign.builder().decoder(new GsonDecoder()).target(GitHub.class, "https://api.github.com");

其中的target方法支持传入一个Target类来修改运行时的参数,可以自定义Target类类似如下

1
2
3
4
5
6
7
8
class DynamicUrlTarget<T> implements Target<T> {
@Override
public Request apply(RequestTemplate input) {
String url = dao.findSomeUrl();
input.target(url);
return input.request();
}
}

那么倒推Spring OpenFeign 查询target方法发现在DefaultTargeter中有调用,该接口实现了SpringTargeter接口,这也意味着可以进行自定义替换。这里先扩展Targeter接口方法使其支持路径配置。

1
2
3
4
5
6
7
8
9
10
11
12
public interface ApiCaller extends Targeter {

String url();

void preHandle(RequestTemplate template);

@Override
default <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
Target.HardCodedTarget<T> target) {
return feign.target(new WrapTarget<>(this, target.type()));
}
}

在代码中还用到了WrapTarget这个类,这个类主要用于处理参数配置替换等,相关代码如下

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
31
public class WrapTarget<T> implements Target<T> {
private final ApiCaller caller;
private final Class<T> type;

public WrapTarget(ApiCaller caller, Class<T> type) {
this.caller = caller;
this.type = type;
}

@Override
public Class<T> type() {
return type;
}

@Override
public String name() {
return type.getSimpleName();
}

@Override
public String url() {
return caller.url();
}

@Override
public Request apply(RequestTemplate input) {
input.target(url());
caller.preHandle(input);
return input.request();
}
}

如上便完成了自定义路径的包装,但麻烦又来了,如何让 Spring 加载 ApiCaller 而非默认的DefaultTargeter,这里就需要了解注解@FeignClient是如何工作,整理了下流程大概如下

  • 首先代码定义声明 @FeignClient
  • FeignClientsRegistrar作为注解注册器做了扫描处理, 核心方法registerBeanDefinitions代码值得关注的有两部分代码
    • registerClientConfiguration 注册注解中的 configuration 配置为 FeignClientSpecification Bean
    • registerFeignClient 注册注解为 FeignClientFactoryBean 工厂 Bean
  • FeignAutoConfiguration中将自定义的 FeignClientSpecification 注入到 FeignContext Bean
  • 在实际构建代理类时利用 FeignContext 及相关的 FeignClientSpecification 最终构建出 Feign 对象

而根据最后一步构建Feign实例的代码中 Targeter targeter = get(context, Targeter.class); 可以和之前自定义的ApiCaller衔接起来。

简单的一个示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class DynamicUrlApiCaller implements ApiCaller {
private final Dao dao;

public DynamicUrlApiCaller(Dao dao) {
this.dao = dao;
}

@Override
public String url() {
return dao.findUrl();
}

@Override
public void preHandle(RequestTemplate template) {
}
}

接着将类DynamicUrlApiCaller配置在@FeignClientconfiguration处,类似如下

1
2
3
4
5
@FeignClient(name = "record", configuration = DynamicUrlApiCaller.class)
public interface StoreClient {
@RequestMapping(method = RequestMethod.GET, value = "/stores")
List<Store> getStores();
}

不用担心dao的注入问题,实际上configuration在处理中会自动实例化出一个继承父容器的子容器,所以在实例化中不用担心依赖注入问题大胆使用即可。

写在最后

关于动态路径的切换还是有一定场景的,但是搜遍网上的教程却很少有可以借鉴参考的,这个时候就要动用自己的改造能力站在巨人的肩膀上作出微创新,希望这篇文章能帮到你。