OpenFeign學習(二):高級用法自定義配置組件HttpClient / SLF4J / RequestInterceptor等

說明

在項目開發中,避免不了通過HTTP請求進行對第三方服務的調用,在上篇博文OkHttp的高級封裝Feign學習(一): Feign註解的使用中,我對Feign註解基本使用進行了學習總結。本篇博文我將繼續對feign的其他特性及高級用法進行學習總結。

正文

feign具有很強的擴展性,允許用戶根據需要進行定製,如HTTP客戶端OkHttp, HTTP/2 client, SLF4J日誌的使用, 編解碼,錯誤處理等。使用時可以通過Feign.builder()創建api客戶端時配置自定義組件。

Encoders & Decoders

feign支持使用Gson和Jackson作爲編解碼工具,在創建api接口時通過encoder()和decoder()方法分別指定。在使用不同的類型時,需要引入不同的依賴 feign-jackson 或 feign-gson

在feign中,默認的解碼器只能解碼類型爲Response, String, byte[], void的返回值,若返回值包含其他類型,就必須指定解碼器Decoder。並且希望在解碼前對返回值進行前置處理,則需要使用mapAndDecode方法,設置處理邏輯,使用示例詳見文檔。

同時,在發送請求時使用的post方法,方法參數爲String或者byte[],並且參數沒有使用註解,這時就需要添加Content-Type請求頭並通過encoder()方法設置編碼器,來發送類型安全的請求體。

編碼示例:
在請求時,設置Content-Type爲application/json,方法參數爲一個pojo,並指定encoder,這裏使用jackson。

<dependency>
	<groupId>io.github.openfeign</groupId>
	<artifactId>feign-jackson</artifactId>
	<version>10.7.4</version>
</dependency>
@RequestLine("POST /test/json/encoder")
@Headers("Content-Type: application/json")
ResultPojo jsonEncoderTest(JSONObject content);
HelloService service = Feign.builder()
			.client(new OkHttpClient())
			.encoder(new JacksonEncoder())
			.decoder(new JacksonDecoder())
			.target(HelloService.class, "http://localhost:8080/");

服務端接口又如何接收該類型的參數?可以使用@RequestBody將json格式參數轉換爲pojo或Map<String, Object> paramMap形式。

@RequestMapping("/test/json/encoder")
public String jsonEncoder(@RequestBody ParamPojo paramPojo) {
    System.out.println(paramPojo.getName() + "-" + paramPojo.getPassword() + "- " + paramPojo.getRoles().size());
    JSONObject object = new JSONObject();
    object.put("code", 0);
    object.put("msg", "success");
    return object.toJSONString();
}

解碼示例:在一般的http請求中,返回的多爲json格式的字符串,在使用時需要解析爲json對象,我們可以通過設置decoder,直接將結果轉爲對應的對象。

public class ResultPojo {

    private Integer code;
    private String msg;
    ....省略set get
}
@RequestLine("GET /test/json/decoder")
ResultPojo jsonDecoderTest();

仍然是在Feign.builder()方法中通過decoder()設置對應的組件。

同樣的,Feign也支持對xml格式的編解碼,支持Sax 和 JAXB,詳見文檔

日誌 SLF4J

feing支持對http請求響應記錄日誌,在創建api接口時通過logger()來配置logger,通過logLevel()設置日誌等級。並且支持了SLF4J,方便與系統使用日誌進行整合。

這裏我使用log4j2進行示例:
排除springboot自帶依賴,添加log4j2和feign-slf4j依賴

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<exclusions>
		<exclusion>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-logging</artifactId>
		</exclusion>
	</exclusions>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
	<groupId>io.github.openfeign</groupId>
	<artifactId>feign-slf4j</artifactId>
	<version>10.7.4</version>
</dependency>

log4j的配置:
注意,在配置文件需要配置單獨的logger,並且name爲feign.Logger,level爲debug

<?xml version="1.0" encoding="UTF-8"?>
<!--設置log4j2的自身log級別爲warn -->
<configuration status="warn">
    <appenders>
        <console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="[%d][%t][%p][%l] %m%n" />
        </console>
    </appenders>
    <loggers>
        <logger name="feign.Logger" level="debug" >
            <appender-ref ref="Console"/>
        </logger>
        <root level="error">
            <appender-ref ref="Console" />
        </root>
    </loggers>
</configuration>

在創建api接口時feign.builder()進行配置。

HelloService service = Feign.builder()
			.logger(new Slf4jLogger())
			.logLevel(Logger.Level.FULL)
			.client(new OkHttpClient())
			.target(HelloService.class, "http://localhost:8080/");

注意,loglevel一共有四個等級,分別爲NONE, BASIC, HEADERS, FULL

NONE : 沒有日誌輸出
BASIC : 只對請求方法,url和響應狀態進行日誌輸出
HEADERS : 對請求和響應的請求頭、響應頭的基本信息進行日誌輸出
FULL : 對請求的請求頭,請求體,元數據及響應的響應頭,響應體,元數據進行日誌輸出

OkHttp & HTTP2

feign支持通過OkHttpClient或Http2Client發送http請求。

OkHttpClient將feign的http請求定向到okhttp,okhttp支持SPDY協議和更好的網絡控制。

Http2Client將請求定向到java11的NEW HTTP/2 Client, 該客戶端基於HTTP/2協議。注意,要使用該客戶端,需要java sdk 11。

有關SPDY, HTTP/2更多內容詳見 一文讀懂 HTTP/1HTTP/2HTTP/3

繼承公共api接口

feign支持將公共api抽取到一個公共接口,支持公共api接口的單一繼承。
示例:

public interface CommonService {

    @RequestLine("GET /test/hello2?name={name}")
    ResultPojo common(@Param("name") String name);
}
public interface HelloService extends CommonService{

}
HelloService service = Feign.builder()
			.logger(new Slf4jLogger())
			.logLevel(Logger.Level.FULL)
			.client(new OkHttpClient())
			.encoder(new JacksonEncoder())
			.decoder(new JacksonDecoder())
			.target(HelloService.class, "http://localhost:8080/");

@Test
public void commonServiceTest() {
	ResultPojo res = service.common("tom");
}

請求攔截器

在上篇有關注解的使用一文中,我們已經瞭解到了對於請求頭的設置我們可以使用@Headers和@HeaderMap註解,其中@Headers註解既可以使用在方法上也可以使用在接口類(Type)上。

因此,當我們需要對一個請求目標(Target)的每個方法都設置請求頭時,可以使用@Headers註解。但是,如果需要對不同的請求目標的所有方法都設置請求頭,這時就需要使用請求攔截器。請求攔截器可以在不同的請求目標實例(feign.builder創建的客戶端)間共享,並且是線程安全的。

RequestInterceptors can be shared across Target instances and are expected to be thread-safe. RequestInterceptors are applied to all request methods on a Target.

示例:
爲所有請求方法都設置請求頭user-agent值:

public class HeadersInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header("user-agent", "wds");
    }
}

HelloService service = Feign.builder()
			.client(new OkHttpClient())
			.requestInterceptor(new HeadersInterceptor())
			.target(HelloService.class, "http://localhost:8080/");

如果想對每個方法都進行定製,那就需要創建一個定製的target。因爲攔截器沒有權限獲取到方法的元數據。詳情示例請看官方文檔的 Setting headers per target 部分。

錯誤處理

feign支持對意外的請求響應做自定義處理,所有的不成功(HTTP status 不爲2xx)的響應都會觸發ErrorDecoder的decode()方法進行處理,允許自定義異常或進行其他處理。如果希望進行重試再次請求,則需要拋出RetryableException異常,此時就會調用註冊在客戶端的Retryer。

示例:對404的響應拋出自定義異常

public class CustomErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String s, Response response) {
        int code = response.status();
        if (code == 404) {
            return new RuntimeException(s + "請求方法不存在");
        }
        return null;
    }
}

HelloService service = Feign.builder()
			.errorDecoder(new CustomErrorDecoder())
			.target(HelloService.class, "http://localhost:8080/");

重試retry

feign默認會對所有HTTP方法請求拋出的IOException和ErrorDecoder拋出的RetryableException異常的請求進行重試。 自定義重試頻率策略可以創建定製的Retryer,一定要重寫clone()方法,否則會拋出NullException。

重試機制:

Retryer根據continueOrPropagate(RetryableException e)方法返回的true或false決定是否重試。

以上是官方文檔說明的重試機制,但是通過閱讀源碼發現,feign自身有一個實現Retryer接口的內部類Default,並且接口的方法continueOrPropagate(RetryableException var1)無返回值,並且Default實現該方法控制了重試頻率。所以重試並不是根據此方法返回的true或false決定

public void continueOrPropagate(RetryableException e) {
    if (this.attempt++ >= this.maxAttempts) {
        throw e;
    } else {
        long interval;
        if (e.retryAfter() != null) {
            interval = e.retryAfter().getTime() - this.currentTimeMillis();
            if (interval > this.maxPeriod) {
                interval = this.maxPeriod;
            }

            if (interval < 0L) {
                return;
            }
        } else {
            interval = this.nextMaxInterval();
        }

        try {
            Thread.sleep(interval);
        } catch (InterruptedException var5) {
            Thread.currentThread().interrupt();
            throw e;
        }

        this.sleptForMillis += interval;
    }
}

每個客戶端都需要有自己的Retryer實例,並且允許Retryer維護每個請求的狀態。
爲了保證每個客戶端都有自己的Retryer示例,在執行方法時,都會調用註冊retryer對象的clone()方法,在默認實現Default類中,clone()方法創建自身新實例返回

public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = this.buildTemplateFromArgs.create(argv);
    Options options = this.findOptions(argv);
    Retryer retryer = this.retryer.clone();
    .....
}
public static class Default implements Retryer {
    .....
    public Retryer clone() {
        return new Retryer.Default(this.period, this.maxPeriod, this.maxAttempts);
    }

}

當重試失敗時,最後的RetryException異常將會被拋出。如果需要拋出導致重試失敗的原始原因,則需要在創建客戶端是設置exceptionPropagationPolicy()配置。通過源碼看出,當配置爲ExceptionPropagationPolicy.UNWRAP時,會返回原始的cause

try {
    retryer.continueOrPropagate(e);
} catch (RetryableException var8) {
    Throwable cause = var8.getCause();
    if (this.propagationPolicy == ExceptionPropagationPolicy.UNWRAP && cause != null) {
        throw cause;
    }

    throw var8;
}

示例:創建自定義Retryer

注意,默認情況下feign只會對IOException進行重試,如果需要其他情況的重試,則需要創建配置自定義的ErrorDecoder,拋出RetryableException異常。這裏,我仍使用之前的CustomErrorDecoder :

public class CustomErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String s, Response response) {
        int code = response.status();
        if (code == 404) {
            throw new RetryableException(code, "Service Unavailable", response.request().httpMethod(), null, response.request());
        }
        return null;
    }
}

MyRetryer :
注意 最大重試次數的設置,在默認重試器Default中,默認的最大重試次數爲5,包含了第一次請求失敗的動作,所以失敗後的實際重試次數是4次。

public class MyRetryer implements Retryer {

    private final int maxAttempts;
    int attempt;

    public MyRetryer(int maxAttempts) {
        this.maxAttempts = maxAttempts;
        this.attempt = 1;
    }

    @Override
    public void continueOrPropagate(RetryableException e) {
        if (this.attempt++ >= this.maxAttempts) {
            throw e;
        } else {
            System.out.println("重試 " + attempt + " 次");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
                ex.printStackTrace();
            }
        }
    }

    @Override
    public Retryer clone() {
        return new MyRetryer(5);
    }
}

配置Retryer :

HelloService service = Feign.builder()
			.retryer(new MyRetryer(5))
			.exceptionPropagationPolicy(ExceptionPropagationPolicy.UNWRAP)
			.target(HelloService.class, "http://localhost:8080/");

使用建議:除非需要重新定製重試頻率策略,否則建議使用默認的重試器Default

HelloService service = Feign.builder()
			.retryer(new Retryer.Default(100, 1000, 5))
			.exceptionPropagationPolicy(ExceptionPropagationPolicy.UNWRAP)
			.target(HelloService.class, "http://localhost:8080/");

feign還支持Ribbon和Hystrix,他們的使用方式以及api接口中的靜態和默認方法的使用方式,請看官方文檔。

至此,feign的一些高級用法已經學習介紹完畢,其中常用的包括了encoder, decoder, okhttpclient, slf4j等。接下來我將繼續通過源碼學習總結feign運行的原理。


示例源碼地址:https://github.com/Edenwds/javaweb/tree/master/feigndemo
參考資料:
https://github.com/OpenFeign/feign
https://mp.weixin.qq.com/s/fy84edOix5tGgcvdFkJi2w
https://stackoverflow.com/questions/56987701/feign-client-retry-on-exception

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章