1. 引言

在这篇文章中,我们将探讨如何向Feign 客户端接口提供目标 URL。

2. 概述

为了快速入门,我们将使用来自JSONPlaceholder网站的模拟响应,来处理“专辑”,“帖子”和“待办事项”对象。

首先看下“专辑”类:

public class Album {
    
    private Integer id;
    private Integer userId;
    private String title;
    
   // standard getters and setters
}

接下来是“帖子”类:

public class Post {
    
    private Integer id;
    private Integer userId;
    private String title;
    private String body;
    
    // standard getters and setters
}

最后是“待办事项”类:

public class Todo {
    
    private Integer id;
    private Integer userId;
    private String title;
    private Boolean completed;
    
    // standard getters and setters
}

3. 在注解中添加基础 URL

我们可以在客户端接口的@FeignClient注解的url属性中设置基础 URL。然后,我们会根据相关的HTTP动词注解方法,并添加所需的端点:

@FeignClient(name = "albumClient", url = "https://jsonplaceholder.typicode.com/albums/")
public interface AlbumClient {
    
    @GetMapping(value = "/{id}")
    Album getAlbumById(@PathVariable(value = "id") Integer id);
}

让我们添加一个REST控制器来测试我们的客户端:

@RestController
public class ConfigureFeignUrlController {
    
    private final AlbumClient albumClient;
    // standard constructor
    
    @GetMapping(value = "albums/{id}")
    public Album getAlbumById(@PathVariable(value = "id") Integer id) {
        return albumClient.getAlbumById(id);
    }
    
   // other controller methods
}

当目标 URL在整个应用程序生命周期内保持静态时,这个选项很有用。

4. 使用配置属性

另一种选择是,对于Spring Cloud 2022.0.1及以上版本,我们可以使用application.properties文件为Feign客户端接口提供URL。使用spring.cloud.openfeign.client.config.<interface-name>.url这个属性。这里的<interface-name>是我们@FeignClient注解中name属性的值:

@FeignClient(name = "postClient")
public interface PostClient {
    
    @GetMapping(value = "/{id}")
    Post getPostById(@PathVariable(value = "id") Integer id);
}

我们在application.properties中添加基础 URL:

spring.cloud.openfeign.client.config.postClient.url=https://jsonplaceholder.typicode.com/posts/

对于Spring Cloud 2022.0.1以下版本,我们可以在@FeignClienturl属性中设置值,以便从application.properties中读取:

@FeignClient(name = "postClient", url = "${spring.cloud.openfeign.client.config.postClient.url}")

接下来,我们将这个客户端注入到我们之前创建的控制器中:

@RestController
public class FeignClientController {
    private final PostClient postClient;
    
    // other attributes and standard constructor
    
    @GetMapping(value = "posts/{id}")
    public Post getPostById(@PathVariable(value = "id") Integer id) {
        return postClient.getPostById(id);
    }
    
   // other controller methods
}

如果目标 URL根据应用环境变化,这种选项会很有用。例如,在开发环境中可能使用模拟服务器,而在生产环境中则使用实际服务器。

5. 使用@RequestLine

Spring Cloud 提供了一种功能,允许我们在运行时重写目标 URL或直接提供URL。这是通过使用@RequestLine注解,并手动使用Feign构建器API创建Feign客户端以注册客户端实现的:

@FeignClient(name = "todoClient")
public interface TodoClient {
    
    @RequestLine(value = "GET")
    Todo getTodoById(URI uri);
}

我们需要在控制器中手动创建这个Feign客户端:

@RestController
@Import(FeignClientsConfiguration.class)
public class FeignClientController {
    
    private final TodoClient todoClient;
    
    // other variables
    
    public FeignClientController(Decoder decoder, Encoder encoder) {
        this.todoClient = Feign.builder().encoder(encoder).decoder(decoder).target(Target.EmptyTarget.create(TodoClient.class));
        // other initialisation
   }
    
    @GetMapping(value = "todo/{id}")
    public Todo getTodoById(@PathVariable(value = "id") Integer id) {
        return todoClient.getTodoById(URI.create("https://jsonplaceholder.typicode.com/todos/" + id));
    }
    
    // other controller methods
}

在这里,我们首先通过FeignClientsConfiguration.class导入默认的Feign客户端配置。然后使用Feign.Builder自定义这些接口属性,如编码器、解码器、连接超时、读取超时、认证等。

target属性定义了这些属性将应用于哪个接口。这个接口有两个实现:EmptyTarget(这里使用),以及HardCodedTargetEmptyTarget类在编译时不需要URL,而HardCodedTarget需要。

值得注意的是,作为参数提供的URI将覆盖@feignClient注解中的URL以及配置属性中的URL。同样,@FeignClient注解中的URL会覆盖配置属性中的URL。

6. 使用RequestInterceptor

另一种在运行时提供目标URL的方法是为Feign.Builder提供自定义RequestInterceptor。这里,我们将重写RestTemplatetarget属性,以更新通过requestInterceptor提供的URL:

public class DynamicUrlInterceptor implements RequestInterceptor {
    private final Supplier<String> urlSupplier;
    // standard constructor

    @Override
    public void apply(RequestTemplate template) {
        String url = urlSupplier.get();
        if (url != null) {
            template.target(url);
        }
    }
}

AlbumClient.java中添加另一个方法:

@GetMapping(value = "/{id}")
Album getAlbumByIdAndDynamicUrl(@PathVariable(name = "id") Integer id);

我们不在构造函数中使用Builder,而是在ConfigureFeignUrlController的方法中创建AlbumClient实例:

@RestController
@Import(FeignClientsConfiguration.class)
public class ConfigureFeignUrlController {
    
    private final ObjectFactory<HttpMessageConverters> messageConverters;
    
    private final ObjectProvider<HttpMessageConverterCustomizer> customizers;
    
    // other variables, standard constructor and other APIs
    
    @GetMapping(value = "/dynamicAlbums/{id}")
    public Album getAlbumByIdAndDynamicUrl(@PathVariable(value = "id") Integer id) {
        AlbumClient client = Feign.builder()
          .requestInterceptor(new DynamicUrlInterceptor(() -> "https://jsonplaceholder.typicode.com/albums/"))
          .contract(new SpringMvcContract())
          .encoder(new SpringEncoder(messageConverters))
          .decoder(new SpringDecoder(messageConverters, customizers))
          .target(Target.EmptyTarget.create(AlbumClient.class));
     
        return client.getAlbumByIdAndDynamicUrl(id);
    }
}

这里,我们添加了一个上面创建的名为DynamicUrlInterceptor的拦截器,它接受一个URL以覆盖AlbumClient的默认URL。我们还配置了客户端使用SpringMvcContractSpringEncoderSpringDecoder

最后两个选项将在我们应用中需要支持Webhook时派上用场,因为每个客户端的目标URL都会有所不同。

7. 总结

在这篇文章中,我们了解了如何以不同的方式配置Feign客户端接口的目标URL。如需查看完整的实现,请访问GitHub