feign官方文档

feign makes writing java http clients easier

为什么使用Feign框架而不是其他的?

feigh 使用 jersey或CXF这样的框架来为REST服务或SOAP服务提供http客户端。而且,feign允许你在http的jar包基础上编写代码,就像Apache HttpComponents,feign以最小的开销爸代码连接到HTTP APIS,并且通过自定义的解码和错误处理,写给任何基于文本的http api。

feign是如何工作的

feign通过将注解处理为模板请求的方式来工作。在输出之前参数以一种简单的方式直接应用到模板上。尽管feign只支持text-base APIS,但是它极大的简化了系统方面,比如重放请求。另外,feign让单元测试更加方便。

功能概述

  • clients: java.net.URL Apache HTTP Apache HC5 Google HTTP Java 11 Http2 OK Http Ribbon
  • async clients: java.net.URL Apache HC5
  • contracts: Feign JAX-RS JAX-RS 2 Spring 4 SOAP Spring boot(3rd party)
  • encoder/decoders: GSON Jackson 1 Jackson 2 Jackson JAXB Sax
  • metrics: Dropwizard Metric 5 Mircrometer
  • extras: Hystrix SLF4J MOCK

Feign 11及后续功能

短期的,目前真在做的

  • Response 缓存:支持API response的缓存。允许用户定义在什么条件下进行缓存,用什么缓存机制。支持内存缓存和用EhCache,Google,Spring,etc…来扩展缓存实现
  • 完整的URI模板表达式的支持
  • logger API重构
  • Retry API重构

中期的,接下来要做的

  • 通过CompletableFuture支持异步调用

基本使用

interface GitHub {
    
    
	@RequestLine("GET /repos/{owner}/{repo}/contributors")
	List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);

	@RequestLine("POST /repos/{owner}/{repo}/issues")
	void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);
}

public static class Contributor {
    
    
	String login;
	int contributions;
}

public static class Issue {
    
    
	String title;
	String body;
	List<String> assignees;
	int milestone;
	List<String> lables;
}

public class MyApp{
    
    
	public static void main(String... args){
    
    
		GitHub github = Feign.builder()
							 .decoder(new GsonDecoder())
							 .targer(GitHub.class, "http://api.github.com");
	// 获取并打印此库的贡献者列表
	List<Contributor> contributors = github.contributors("OpenFeign", "feign");
	for(Contributor contributor : contributors){
    
    
		System.out.println(contributor.login + "(" + contributor.contributions + ")");
	}    
	}
}

接口注解

Feign的注解在接口和它的客户端如何工作之间定义了契约,Feign默认的注解如下:

  1. @RequestLine 方法注解 定义request 的 HttpMethod(get还是post)和UriTemplate,表达式的值
    使用{}括起来,表达式的值由@Param注解的参数来解析
  2. @Param 参数注解 解析模板中的表达式的值
  3. @Headers 方法/类型注解 定义一个Header模板,UriTemplate的变体。它用@Param注解的值来解析相应的表达式。当在Type 上使用模板时,该应用将应用在每一个请求上,当应用在Method上时,这个模板只对注解的方法起作用。
  4. @QueryMap 参数注解 定义一个键值对的映射,或者 POJO对象,去扩展查询的字符串
  5. @HeaderMap 参数注解 定义一个键值对的映射,去扩展Http Headers
  6. @Body 方法注解 定义一个模板,类似uritemplate和headertemplate,它用@Param注解的值来解析对应的表达式
    Overriding the request Line
    如果需要将请求定向到不同的主机,则在创建feign客户端的时候用提供的目标主机信息,或者希望为每个请求提供目标主机,包括java.net.URI参数,Feign用该值来作为请求目标。
@RequestLine("POST /repos/{owner}/{repo}/issues")
void createIssue(URI host, Issur issue, @Param("owner") String owner, @Param("repo") String repo);

模板和表达式
Feign表达式表明了一个简单的字符串表达式(Level 1) RFC 6570定义的URI Template. 表达式被对应的@Param注解的参数填充
基本使用中的代码示例

/* owner和repository的参数值将用来填充RequestLine中定义的owner和expressions
 * 结果的uri是https://api.github.com/repos/OpenFeign/feign/contributors
 */
github.contributors("OpenFeign", "feign");

表达式必须被大括号括起来,表达式中也可以包含正则表达式来限制解析值,使用时通过:分隔,例如owner必须是字母。{owner:[a-zA-Z]*}

请求参数说明

RequestLine和QueryMap模板遵循URI Template - RFC 6570 level1 的规范,具体规范如下:

  • 忽略掉无法解析的表达式
  • 如果没有对字符串或变量进行编码,或者是没有通过@Param注解标记编码,将使用pct-encoded来编码

未定义和空值

未定义的表达式是明确值为null或值不能提供的表达式。根据URI Template - RFC6570,给表达式提供一个空值是有可能的。当feign解析表达式的时候,会先判断值是否被定义来决定是否保留对应的查询参数。如果值没有被定义,则查询参数会被移除。看下面的示例:
Empty String

public void test(){
    
    
	Map<String, Object> parameters = new LinkedHashMap();
	parameters.put("param", "");
	this.demoClient.test(parameters);
}

Result http://localhost:8080/test?param=

Missing

public void test(){
    
    
	Map<String, Object> parameters = new LinkedHashMap();
	this.demoClient.test(parameters);
}

Result http://localhost:8080/test

Undefined

扫描二维码关注公众号,回复: 12383057 查看本文章
public void test(){
    
    
	Map<String, Object> parameters = new LinkedHashMap();
	parameters.put("param", null);
	this.demoClient.test(parameters);
}

Result http://localhost:8080/test

See Advanced Usage for more examples
关于 /
@RequestLine 默认不对 / 进行编码,如果要改变这个行为,在@RequestLine上设置decodeSlash 为false
关于 +
根据URI的规范,一个+号是允许存在于地址和请求参数中的,但是在处理查询中的符号是不一致的。在一些遗留系统中,+相当于空白字符。feign根据现代系统的做法,不会将+符号表示为空白字符,而是编码为 %2B 当在查询参数中时。
如果确实要将+符号处理为空白字符,可以使用空格或直接将其编码为 %20

自定义扩展

@Param有一个可选属性expander允许对该参数进行扩展,expander属性必须要引用到一个实现了Expander接口的类:

public class DateToMillis implements Param.Expander {
    
    
    @Override
    public String expand(Object o) {
    
    
        return null;
    }
}

方法的返回值规则与上述相同,忽略掉无法解析的表达式,也会默认使用pct-encode编码

Request Headers的扩展

Headers和HeaderMap模板与请求参数的扩展规则相同:

  1. 忽略掉无法解析的表达式,如果header的值是空的,则忽略掉整个返回值
  2. 相同的编码规则

Headers

feign通过api和客户端来设置headers

通过api设置headers

如果特定的接口或调用总是需要特定的header值,那么定义header作为api的一部分是有意义的。
静态headers能够被设置在API的接口上或者方法上通过使用@Headers注解

@Headers("Accept: application/json")
interface BaseApi<V> {
    
    
    @Headers("Content-Type: application/json")
    @RequestLine("PUT /api/{key}")
    void put(@Param("key") String key, V value);
}

@Headers也可以为headers中的变量表达式填充指定的内容

public interface Api<V>{
    
    
    @RequestLine("POST /")
    @Headers("X-Ping: {token}")
    void post(@Param("token") String token);
}

如果header字段的key和value都是动态的并且key的可能的范围无法提前知道,可能不同的方法调用相同的API(如自定义元数据header字段"x-amz-meta-" 或者"x-good-meta-" ,这种情况下,用@HeaderMap注解在一个map参数上,并且用map中的内容来作为header的参数)

public interface Api<V>{
    
    
    @RequestLine("POST /")
    void post(@HeaderMap Map<String, Object> headerMap);
}

动态设置setting

在一个客户端中对发起的请求方法都要设置自定义的headers,可以用RequestInterceptor。RequestInterceptor能够被多个target实例共享并且是线程安全的。RequestInterceptor可以应用于一个target发出的所有请求方法。
如果是每一个请求方法中都需要不同的headers,那么需要自定义target,因为RequestInterceptor 无法访问当前方法的元数据。
下面的例子是用RequestInterceptor来设置headers
headers能够被设置作为自定义target的一部分。

static class DynamicAuthTokenTarget<T> implements Target<T> {
    
    
    public DynamicAuthTokenTarget(Class<T> clazz,
                                  UrlAndTokenProvider provider,
                                  ThreadLocal<String> requestIdPrivider){
    
    

    }

    @Override
    public Request apply(RequestTemplate input){
    
    
        TokenIdAndPublicURL urlAndToken = provider.get();
        if (input.url().indexOf("http") != 0) {
    
    
            input.insert(0, urlAndToken.publicURL);
        }
        input.header("X-Auth-Token", urlAndToken.tokenId);
        input.header("X-Request-ID", requestIdProvider.get());

        return input.request();
    }
}

public class Example {
    
    
    public static void main(String[] args) {
    
    
        Bank bank = Feign.builder()
                .target(new DynamicAuthTokenTarget(Bank.class, provider, requestIdProvider))
    }
}

这些处理依赖于自定义的RequestInterceptor或者target,把RequestInterceptor或者target设置在Feign的客户端中当feign在构建的时候,用这种方式可以设置headers在所有的api上。这是一个很实用的方法,比如为client的每一个请求header添加授权token. The methods are run when the api call is made on the thread that invokes the api call, which allows the headers to be set dynamically at call time and in a context-specific manner – for example, thread-local storage can be used to set different header values depending on the invoking thread, which can be useful for things such as setting thread-specific trace identifiers for requests.

Advanced Usage

基础api
很多情况下,有些服务的api需要使用相同的约束。Feign通过单接口继承的方式来支持这种模式。
考虑下面这个例子:

interface BaseAPI {
    
    
	@RequestLine("GET /health")
	String health();
	
	@RequestLine("Get /all")
	List<Entity> all();
}

你可以通过继承来扩展基础API的能力

interface CustomAPI extends BaseAPI {
    
    
	@RequestLine("GET /custom")
	String custom();
}

在很多情况下,通过泛型来约束API的接口类型。

@Headers("Accept: application/json")
interface BaseApi<V> {
    
    
	@RequestLine("GET /api/{key}")
	V get(@Param("key") String key);
		
	@RequestLine("GET /api")	
	List<V> list();

	@Headers("Content-type: application/json")
	@RequestLine("PUT /api/{key}")
	void put(@Param("key") String key, V value)
}

interface FooApi extends BaseApi<Foo> {
    
    }

interface BarApi extends BaseApi<Bar> {
    
    }

日志

可以通过设置一个Logger来记录去目标主机和从目标主机返回的信息。接下来给一个简单的方式:

public class Example {
    
    
	public static void main(String[] args){
    
    
		GitHub github = Feign.builder()
							 .decoder(new GsonDecoder())
							 .logger(new Logger.JavaLogger("GitHub.Logger").appendToFile("logs/http.log"))
							 .logLevel(Logger.level.FULL)
							 .target(GitHub.class, "https://api.github.com");
	}
}

避免使用被标记为过期的JavaLogger()
可以考虑SLF4JLogger

请求拦截器

可以通过配置RequestInterceptor来配置对所有请求的拦截,比如,作为中间代理需要设置X-Forwarded-For header.

static class ForwardedForInterceptor implements RequestInterceptor {
    
    
    @Override
    public void apply(RequestTemplate template){
    
    
        template.header("X-Forwarded-For", "origin.host.com");
    }
}

    
public class Example{
    
    
    public static void main(String[] args) {
    
    
        Bank bank = Feign.builder()
                .decoder(accountDecoder)
                .requestInterceptor(new ForwardedForInterceptor())
                .target(Bank.class, "http://api.examplebank.com");
    }
}

或者使用BasicAuthRequestInterceptor来实现一个身份验证的拦截器。

public class Example{
    
    
    public static void main(String[] args) {
    
    
        Bank bank = Feign.builder()
                .decoder(accountDecoder)
                .requestInterceptor(new BasicAuthRequestInterceptor(username, password))
                .target(Bank.class, "http://api.examplebank.com");
    }
}

自定义@Param扩展

用@Param注解的参数扩展基于字符串,通过指定一个自定义的Param.Expander, 用户可以控制这个行为,比如格式化日期的例子。

public interface Api {
    
    
	@RequestLine("GET /?since={date}")
	Result list(@Param(value = "date", expander = DateToMillis.class) Date date);
}

动态查询参数

通过用@QueryMap注解一个Map参数能够用map的内容来作为查询参数

public interface Api<V>{
    
    
    @RequestLine("GET /find")
    V find(@QueryMap Map<String, Object> queryMap);
}

也可以从一个POJO对象中通过QueryMapEncoder生成查询参数

public interface Api<V>{
    
    
    @RequestLine("GET /find")
    V find(@QueryMap CustomPojo customPojo);
}

当时用上面的方式调用的时候,如果没有指定一个自定义的QueryMapEncoder,将用成员变量的名称来生成查询参数。下面的POJO将生成查询参数"/find?name={name}&number={number}"(不保证查询参数的顺序,并且如果某个值是null,参数将被忽略).

public class CustomPojo {
    
    
    private final String name;
    private final int number;

    public CustomPojo(String name, int number){
    
    
        this.name = name;
        this.number = number;
    }
}

设置自定义的QueryMapEncoder:

public class Example {
    
    
    public static void main(String[] args) {
    
    
        Feign.builder()
                .queryMapEncoder(new MyCustomQueryMapEncoder())
                .target(MyApi.class, "https://api.hostname.com");
    }
}

当时用@QueryMap注解对象时,默认编码器通过反射方式查找对象属性,并将属性转化为String类型。如果你希望用getter和setter来构建查询字符串,你需要在JavaBean内定义getter和setter方法,并使用BeanQueryMapEncoder

public class Example {
    
    
    public static void main(String[] args) {
    
    
        Feign.builder()
                .queryMapEncoder(new BeanQueryMapEncoder())
                .target(MyApi.class, "https://api.hostname.com");
    }
}

错误处理

如果需要处理错误返回,feign实例可以通过builder注册一个自定义的ErrorDecoder.

public class Example {
    
    
    public static void main(String[] args) {
    
    
        Bank bank = Feign.builder()
                        .errorDecoder(new MyErrorDecoder())
                        .target();
    }
}

所有的返回结果如果不是200的话会触发ErrorDecoder’s的decode方法,允许你处理返回,把失败包装成一个自定义异常或者用其他方式处理。如果你想重试请求,抛出一个RetryableException。这样的话会使用已注册的Retryer进行重试。

重试

默认情况下,Feign将会重试IOExcetions的方法,不管是什么HTTP method,将他们视为短暂的网络相关异常,还有ErrorDecoder抛出的RetryableExection。可以通过builder()注册一个自定义的Retryer来实现自定义的重试策略。

public class Example {
    
    
    public static void main(String[] args) {
    
    
        Bank bank = Feign.builder()
                        .retryer(new MyRetryer())
                        .target();
    }
}

重试是根据continueOrPropagater(RetryableException e)返回true或者false来决定重试是否发生;retyrer实例是在client执行的时候被创建的,允许维护想要的状态在每次请求的时候。
如果重试失败会抛出RetryException,通过在构建feign客户端的时候设置exceptionPropagationPolicy()选项可以输出导致重试失败的原因。

指标

默认情况下,Feign不收集任何指标。
用指标可以了解更多,在request/reponse的请求生命周期中。

异步调用

通过AsyncFeign允许方法返回CompletableFuture 实例,从Feign10.8.0版本开始

interface GitHub {
    
    
    @RequestLine("GET /repos/{owner}/{repo}/contributors")
    CompletableFuture<List<Contributor>> contributors(@Param("owner") String owner, @Param("repo") String repo);
}

public class MyApp {
    
    
    public static void main(String[] args) {
    
    
        GitHub gitHub = AsyncFeign.asyncBuilder()
                            .decoder(new GsonDecoder())
                            .target(GitHub.class, "Http://api.github.com");

        CompletableFuture<List<Contributor>> contributors = gitHub.contributors("OpenFeign", "feign");
        for (Contributor contributor : contributors) {
    
    
            System.out.println(contributor.login);
        }
    }
}

猜你喜欢

转载自blog.csdn.net/gou553323/article/details/112651700