spring-boot項目整合Retrofit最佳實踐跨琳,最優(yōu)雅的HTTP客戶端工具!

大家都知道okhttp是一款由square公司開源的java版本http客戶端工具桐罕。實際上脉让,square公司還開源了基于okhttp進一步封裝的retrofit工具桂敛,用來支持通過接口的方式發(fā)起http請求如果你的項目中還在直接使用RestTemplate或者okhttp溅潜,或者基于它們封裝的HttpUtils术唬,那么你可以嘗試使用Retrofit

retrofit-spring-boot-starter實現(xiàn)了Retrofitspring-boot框架快速整合滚澜,并且支持了部分功能增強粗仓,從而極大的簡化spring-boot項目下http接口調(diào)用開發(fā)。接下來我們直接通過retrofit-spring-boot-starter设捐,來看看spring-boot項目發(fā)送http請求有多簡單借浊。

retrofit官方并沒有提供與spring-boot快速整合的starterretrofit-spring-boot-starter是筆者封裝的萝招,已在生產(chǎn)環(huán)境使用蚂斤,非常穩(wěn)定。造輪子不易槐沼,跪求各位大佬star曙蒸。

引入依賴

<dependency>
    <groupId>com.github.lianjiatech</groupId>
    <artifactId>retrofit-spring-boot-starter</artifactId>
    <version>2.0.2</version>
</dependency>

配置@RetrofitScan注解

你可以給帶有 @Configuration 的類配置@RetrofitScan,或者直接配置到spring-boot的啟動類上岗钩,如下:

@SpringBootApplication
@RetrofitScan("com.github.lianjiatech.retrofit.spring.boot.test")
public class RetrofitTestApplication {

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

定義http接口

接口必須使用@RetrofitClient注解標記纽窟!http相關(guān)注解可參考官方文檔:retrofit官方文檔

@RetrofitClient(baseUrl = "${test.baseUrl}")
public interface HttpApi {

    @GET("person")
    Result<Person> getPerson(@Query("id") Long id);
}

注入使用

將接口注入到其它Service中即可使用凹嘲。

@Service
public class TestService {

    @Autowired
    private HttpApi httpApi;

    public void test() {
        // 通過httpApi發(fā)起http請求
    }
}

只要通過上述幾個步驟师倔,就能實現(xiàn)通過接口發(fā)送http請求了,真的很簡單周蹭。如果你在spring-boot項目里面使用過mybatis趋艘,相信你對這種使用方式會更加熟悉。接下來我們繼續(xù)介紹一下retrofit-spring-boot-starter更高級一點的功能凶朗。

注解式攔截器

很多時候瓷胧,我們希望某個接口下的某些http請求執(zhí)行統(tǒng)一的攔截處理邏輯。這個時候可以使用注解式攔截器棚愤。使用的步驟主要分為2步:

  1. 繼承BasePathMatchInterceptor編寫攔截處理器搓萧;
  2. 接口上使用@Intercept進行標注。

下面以給指定請求的url后面拼接timestamp時間戳為例宛畦,介紹下如何使用注解式攔截器瘸洛。

繼承BasePathMatchInterceptor編寫攔截處理器

@Component
public class TimeStampInterceptor extends BasePathMatchInterceptor {

    @Override
    public Response doIntercept(Chain chain) throws IOException {
        Request request = chain.request();
        HttpUrl url = request.url();
        long timestamp = System.currentTimeMillis();
        HttpUrl newUrl = url.newBuilder()
                .addQueryParameter("timestamp", String.valueOf(timestamp))
                .build();
        Request newRequest = request.newBuilder()
                .url(newUrl)
                .build();
        return chain.proceed(newRequest);
    }
}

接口上使用@Intercept進行標注

@RetrofitClient(baseUrl = "${test.baseUrl}")
@Intercept(handler = TimeStampInterceptor.class, include = {"/api/**"}, exclude = "/api/test/savePerson")
public interface HttpApi {

    @GET("person")
    Result<Person> getPerson(@Query("id") Long id);

    @POST("savePerson")
    Result<Person> savePerson(@Body Person person);
}

上面的@Intercept配置表示:攔截HttpApi接口下/api/**路徑下(排除/api/test/savePerson)的請求,攔截處理器使用TimeStampInterceptor次和。

擴展注解式攔截器

有的時候反肋,我們需要在攔截注解動態(tài)傳入一些參數(shù),然后再執(zhí)行攔截的時候需要使用這個參數(shù)踏施。這種時候石蔗,我們可以擴展實現(xiàn)自定義攔截注解罕邀。自定義攔截注解必須使用@InterceptMark標記,并且注解中必須包括include()养距、exclude()诉探、handler()屬性信息。使用的步驟主要分為3步:

  1. 自定義攔截注解
  2. 繼承BasePathMatchInterceptor編寫攔截處理器
  3. 接口上使用自定義攔截注解棍厌;

例如我們需要在請求頭里面動態(tài)加入accessKeyId肾胯、accessKeySecret簽名信息才能正常發(fā)起http請求,這個時候可以自定義一個加簽攔截器注解@Sign來實現(xiàn)耘纱。下面以自定義@Sign攔截注解為例進行說明阳液。

自定義@Sign注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@InterceptMark
public @interface Sign {
    /**
     * 密鑰key
     * 支持占位符形式配置。
     *
     * @return
     */
    String accessKeyId();

    /**
     * 密鑰
     * 支持占位符形式配置揣炕。
     *
     * @return
     */
    String accessKeySecret();

    /**
     * 攔截器匹配路徑
     *
     * @return
     */
    String[] include() default {"/**"};

    /**
     * 攔截器排除匹配帘皿,排除指定路徑攔截
     *
     * @return
     */
    String[] exclude() default {};

    /**
     * 處理該注解的攔截器類
     * 優(yōu)先從spring容器獲取對應(yīng)的Bean,如果獲取不到畸陡,則使用反射創(chuàng)建一個鹰溜!
     *
     * @return
     */
    Class<? extends BasePathMatchInterceptor> handler() default SignInterceptor.class;
}

擴展自定義攔截注解有以下2點需要注意:

  1. 自定義攔截注解必須使用@InterceptMark標記。
  2. 注解中必須包括include()曹动、exclude()、handler()屬性信息墓陈。

實現(xiàn)SignInterceptor

@Component
public class SignInterceptor extends BasePathMatchInterceptor {

    private String accessKeyId;

    private String accessKeySecret;

    public void setAccessKeyId(String accessKeyId) {
        this.accessKeyId = accessKeyId;
    }

    public void setAccessKeySecret(String accessKeySecret) {
        this.accessKeySecret = accessKeySecret;
    }

    @Override
    public Response doIntercept(Chain chain) throws IOException {
        Request request = chain.request();
        Request newReq = request.newBuilder()
                .addHeader("accessKeyId", accessKeyId)
                .addHeader("accessKeySecret", accessKeySecret)
                .build();
        return chain.proceed(newReq);
    }
}

上述accessKeyIdaccessKeySecret字段值會依據(jù)@Sign注解的accessKeyId()accessKeySecret()值自動注入第献,如果@Sign指定的是占位符形式的字符串,則會取配置屬性值進行注入庸毫。另外,accessKeyIdaccessKeySecret字段必須提供setter方法飒赃。

接口上使用@Sign

@RetrofitClient(baseUrl = "${test.baseUrl}")
@Sign(accessKeyId = "${test.accessKeyId}", accessKeySecret = "${test.accessKeySecret}", exclude = {"/api/test/person"})
public interface HttpApi {

    @GET("person")
    Result<Person> getPerson(@Query("id") Long id);

    @POST("savePerson")
    Result<Person> savePerson(@Body Person person);
}

這樣就能在指定url的請求上,自動加上簽名信息了载佳。

連接池管理

默認情況下,所有通過Retrofit發(fā)送的http請求都會使用max-idle-connections=5 keep-alive-second=300的默認連接池蔫慧。當(dāng)然,我們也可以在配置文件中配置多個自定義的連接池,然后通過@RetrofitClientpoolName屬性來指定使用。比如我們要讓某個接口下的請求全部使用poolName=test1的連接池肋联,代碼實現(xiàn)如下:

  1. 配置連接池。

    retrofit:
        # 連接池配置
        pool:
            test1:
            max-idle-connections: 3
            keep-alive-second: 100
            test2:
            max-idle-connections: 5
            keep-alive-second: 50
    
  2. 通過@RetrofitClientpoolName屬性來指定使用的連接池橄仍。

    @RetrofitClient(baseUrl = "${test.baseUrl}", poolName="test1")
    public interface HttpApi {
    
        @GET("person")
        Result<Person> getPerson(@Query("id") Long id);
    }
    

日志打印

很多情況下韧涨,我們希望將http請求日志記錄下來。通過@RetrofitClientlogLevellogStrategy屬性侮繁,您可以指定每個接口的日志打印級別以及日志打印策略虑粥。retrofit-spring-boot-starter支持了5種日志打印級別(ERROR, WARN, INFO, DEBUG, TRACE),默認INFO宪哩;支持了4種日志打印策略(NONE, BASIC, HEADERS, BODY)娩贷,默認BASIC。4種日志打印策略含義如下:

  1. NONE:No logs.
  2. BASIC:Logs request and response lines.
  3. HEADERS:Logs request and response lines and their respective headers.
  4. BODY:Logs request and response lines and their respective headers and bodies (if present).

retrofit-spring-boot-starter默認使用了DefaultLoggingInterceptor執(zhí)行真正的日志打印功能锁孟,其底層就是okhttp原生的HttpLoggingInterceptor彬祖。當(dāng)然,你也可以自定義實現(xiàn)自己的日志打印攔截器品抽,只需要繼承BaseLoggingInterceptor(具體可以參考DefaultLoggingInterceptor的實現(xiàn))储笑,然后在配置文件中進行相關(guān)配置即可。

retrofit:
  # 日志打印攔截器
  logging-interceptor: com.github.lianjiatech.retrofit.spring.boot.interceptor.DefaultLoggingInterceptor

Http異常信息格式化器

當(dāng)出現(xiàn)http請求異常時圆恤,原始的異常信息可能閱讀起來并不友好突倍,因此retrofit-spring-boot-starter提供了Http異常信息格式化器,用來美化輸出http請求參數(shù)盆昙,默認使用DefaultHttpExceptionMessageFormatter進行請求數(shù)據(jù)格式化羽历。你也可以進行自定義,只需要繼承BaseHttpExceptionMessageFormatter淡喜,再進行相關(guān)配置即可窄陡。

retrofit:
  # Http異常信息格式化器
  http-exception-message-formatter: com.github.lianjiatech.retrofit.spring.boot.interceptor.DefaultHttpExceptionMessageFormatter

調(diào)用適配器 CallAdapter

Retrofit可以通過調(diào)用適配器CallAdapterFactoryCall<T>對象適配成接口方法的返回值類型。retrofit-spring-boot-starter擴展2種CallAdapterFactory實現(xiàn):

  1. BodyCallAdapterFactory
    • 默認啟用拆火,可通過配置retrofit.enable-body-call-adapter=false關(guān)閉
    • 同步執(zhí)行http請求跳夭,將響應(yīng)體內(nèi)容適配成接口方法的返回值類型實例。
    • 除了Retrofit.Call<T>们镜、Retrofit.Response<T>币叹、java.util.concurrent.CompletableFuture<T>之外,其它返回類型都可以使用該適配器模狭。
  2. ResponseCallAdapterFactory
    • 默認啟用颈抚,可通過配置retrofit.enable-response-call-adapter=false關(guān)閉
    • 同步執(zhí)行http請求,將響應(yīng)體內(nèi)容適配成Retrofit.Response<T>返回。
    • 如果方法的返回值類型為Retrofit.Response<T>贩汉,則可以使用該適配器驱富。

Retrofit自動根據(jù)方法返回值類型選用對應(yīng)的CallAdapterFactory執(zhí)行適配處理!加上Retrofit默認的CallAdapterFactory匹舞,可支持多種形式的方法返回值類型:

  • Call<T>: 不執(zhí)行適配處理褐鸥,直接返回Call<T>對象
  • CompletableFuture<T>: 將響應(yīng)體內(nèi)容適配成CompletableFuture<T>對象返回
  • Void: 不關(guān)注返回類型可以使用Void。如果http狀態(tài)碼不是2xx叫榕,直接拋錯姊舵!
  • Response<T>: 將響應(yīng)內(nèi)容適配成Response<T>對象返回
  • 其他任意Java類型: 將響應(yīng)體內(nèi)容適配成一個對應(yīng)的Java類型對象返回括丁,如果http狀態(tài)碼不是2xx史飞,直接拋錯!
    /**
     * Call<T>
     * 不執(zhí)行適配處理会宪,直接返回Call<T>對象
     * @param id
     * @return
     */
    @GET("person")
    Call<Result<Person>> getPersonCall(@Query("id") Long id);

    /**
     *  CompletableFuture<T>
     *  將響應(yīng)體內(nèi)容適配成CompletableFuture<T>對象返回
     * @param id
     * @return
     */
    @GET("person")
    CompletableFuture<Result<Person>> getPersonCompletableFuture(@Query("id") Long id);

    /**
     * Void
     * 不關(guān)注返回類型可以使用Void掸鹅。如果http狀態(tài)碼不是2xx巍沙,直接拋錯句携!
     * @param id
     * @return
     */
    @GET("person")
    Void getPersonVoid(@Query("id") Long id);

    /**
     *  Response<T>
     *  將響應(yīng)內(nèi)容適配成Response<T>對象返回
     * @param id
     * @return
     */
    @GET("person")
    Response<Result<Person>> getPersonResponse(@Query("id") Long id);

    /**
     * 其他任意Java類型
     * 將響應(yīng)體內(nèi)容適配成一個對應(yīng)的Java類型對象返回矮嫉,如果http狀態(tài)碼不是2xx牍疏,直接拋錯鳞陨!
     * @param id
     * @return
     */
    @GET("person")
    Result<Person> getPerson(@Query("id") Long id);

我們也可以通過繼承CallAdapter.Factory擴展實現(xiàn)自己的CallAdapter;然后將自定義的CallAdapterFactory配置成springbean歼狼!

自定義配置的CallAdapter.Factory優(yōu)先級更高羽峰!

數(shù)據(jù)轉(zhuǎn)碼器 Converter

Retrofi使用Converter@Body注解標注的對象轉(zhuǎn)換成請求體凹蜈,將響應(yīng)體數(shù)據(jù)轉(zhuǎn)換成一個Java對象仰坦,可以選用以下幾種Converter

  • Gson: com.squareup.Retrofit:converter-gson
  • Jackson: com.squareup.Retrofit:converter-jackson
  • Moshi: com.squareup.Retrofit:converter-moshi
  • Protobuf: com.squareup.Retrofit:converter-protobuf
  • Wire: com.squareup.Retrofit:converter-wire
  • Simple XML: com.squareup.Retrofit:converter-simplexml

retrofit-spring-boot-starter默認使用的是jackson進行序列化轉(zhuǎn)換悄晃!如果需要使用其它序列化方式妈橄,在項目中引入對應(yīng)的依賴眷蚓,再把對應(yīng)的ConverterFactory配置成spring的bean即可反番。

我們也可以通過繼承Converter.Factory擴展實現(xiàn)自己的Converter罢缸;然后將自定義的Converter.Factory配置成springbean

自定義配置的Converter.Factory優(yōu)先級更高爵川!

全局攔截器 BaseGlobalInterceptor

如果我們需要對整個系統(tǒng)的的http請求執(zhí)行統(tǒng)一的攔截處理寝贡,可以自定義實現(xiàn)全局攔截器BaseGlobalInterceptor, 并配置成spring中的bean兔甘!例如我們需要在整個系統(tǒng)發(fā)起的http請求鳞滨,都帶上來源信息。

@Component
public class SourceInterceptor extends BaseGlobalInterceptor {
    @Override
    public Response doIntercept(Chain chain) throws IOException {
        Request request = chain.request();
        Request newReq = request.newBuilder()
                .addHeader("source", "test")
                .build();
        return chain.proceed(newReq);
    }
}

結(jié)語

至此熔任,spring-boot項目下最優(yōu)雅的http客戶端工具介紹就結(jié)束了疑苔,更多詳細信息可以參考官方文檔:retrofit以及retrofit-spring-boot-starter惦费。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末薪贫,一起剝皮案震驚了整個濱河市刻恭,隨后出現(xiàn)的幾起案子鳍贾,更是在濱河造成了極大的恐慌,老刑警劉巖橡淑,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件梳码,死亡現(xiàn)場離奇詭異掰茶,居然都是意外死亡濒蒋,警方通過查閱死者的電腦和手機把兔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門县好,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缕贡,“玉大人拣播,你說我怎么就攤上這事贮配∪福” “怎么了宴猾?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵仇哆,是天一觀的道長。 經(jīng)常有香客問我怕轿,道長辟拷,這世上最難降的妖魔是什么衫冻? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任谒出,我火速辦了婚禮,結(jié)果婚禮上为居,老公的妹妹穿的比我還像新娘蒙畴。我一直安慰自己呜象,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布蹬音。 她就那樣靜靜地躺著著淆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪牧抽。 梳的紋絲不亂的頭發(fā)上扬舒,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天孕惜,我揣著相機與錄音晨炕,去河邊找鬼。 笑死削罩,一個胖子當(dāng)著我的面吹牛费奸,可吹牛的內(nèi)容都是我干的愿阐。 我是一名探鬼主播缨历,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼魄缚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了伴鳖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體闸天,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年皿桑,在試婚紗的時候發(fā)現(xiàn)自己被綠了镀虐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡搜贤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出哟沫,到底是詐尸還是另有隱情孔祸,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布捣作,位于F島的核電站掉盅,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏卵贱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽朵诫。三九已至,卻和暖如春日缨,著一層夾襖步出監(jiān)牢的瞬間匣距,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工尸红, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人怎爵。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓题山,卻偏偏與公主長得像戴甩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子缴川,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,691評論 2 361