還在用HttpUtil?試試這款優雅的HTTP客戶端工具吧,跟SpringBoot絕配!

macrozheng發表於2022-02-08
我們平時開發專案時,就算是單體應用,也免不了要呼叫一下其他服務提供的介面。此時就會用到HTTP客戶端工具,之前一直使用的是Hutool中的HttpUtil,雖然容易上手,但用起來頗為麻煩!最近發現一款更好用的HTTP客戶端工具Retrofit,你只需宣告介面就可發起HTTP請求,無需進行連線、結果解析之類的重複操作,用起來夠優雅,推薦給大家!

SpringBoot實戰電商專案mall(50k+star)地址:https://github.com/macrozheng/mall

簡介

Retrofit是適用於AndroidJava且型別安全的HTTP客戶端工具,在Github上已經有39k+Star。其最大的特性的是支援通過介面的方式發起HTTP請求,類似於我們用Feign呼叫微服務介面的那種方式。

SpringBoot是使用最廣泛的Java開發框架,但是Retrofit官方並沒有提供專門的Starter。於是有位老哥就開發了retrofit-spring-boot-starter,它實現了Retrofit與SpringBoot框架的快速整合,並且支援了諸多功能增強,極大簡化開發。今天我們將使用這個第三方Starter來操作Retrofit。

使用

在SpringBoot中使用Retrofit是非常簡單的,下面我們就來體驗下。

依賴整合

有了第三方Starter的支援,整合Retrofit僅需一步,新增如下依賴即可。

<!--Retrofit依賴-->
<dependency>
    <groupId>com.github.lianjiatech</groupId>
    <artifactId>retrofit-spring-boot-starter</artifactId>
    <version>2.2.18</version>
</dependency>

基本使用

下面以呼叫mall-tiny-swagger中的介面為例,我們來體驗下Retrofit的基本使用。
  • 首先我們準備一個服務來方便遠端呼叫,使用的是之前的mall-tiny-swagger這個Demo,開啟Swagger看下,裡面有一個登入介面和需要登入認證的商品品牌CRUD介面,專案地址:https://github.com/macrozheng...

  • 我們先來呼叫下登入介面試試,在application.yml中配置好mall-tiny-swagger的服務地址;
remote:
  baseUrl: http://localhost:8088/
  • 再通過@RetrofitClient宣告一個Retrofit客戶端,由於登入介面是通過POST表單形式呼叫的,這裡使用到了@POST@FormUrlEncoded註解;
/**
 * 定義Http介面,用於呼叫遠端的UmsAdmin服務
 * Created by macro on 2022/1/19.
 */
@RetrofitClient(baseUrl = "${remote.baseUrl}")
public interface UmsAdminApi {

    @FormUrlEncoded
    @POST("admin/login")
    CommonResult<LoginInfo> login(@Field("username") String username, @Field("password") String password);
}
  • 如果你不太明白這些註解是幹嘛的,看下下面的表基本就懂了,更具體的話可以參考Retrofit官方文件;

  • 接下來在Controller中注入UmsAdminApi,然後進行呼叫即可;
/**
 * Retrofit測試介面
 * Created by macro on 2022/1/19.
 */
@Api(tags = "RetrofitController", description = "Retrofit測試介面")
@RestController
@RequestMapping("/retrofit")
public class RetrofitController {

    @Autowired
    private UmsAdminApi umsAdminApi;
    @Autowired
    private TokenHolder tokenHolder;

    @ApiOperation(value = "呼叫遠端登入介面獲取token")
    @PostMapping(value = "/admin/login")
    public CommonResult<LoginInfo> login(@RequestParam String username, @RequestParam String password) {
        CommonResult<LoginInfo> result = umsAdminApi.login(username, password);
        LoginInfo loginInfo = result.getData();
        if (result.getData() != null) {
            tokenHolder.putToken(loginInfo.getTokenHead() + " " + loginInfo.getToken());
        }
        return result;
    }
}
  • 為方便後續呼叫需要登入認證的介面,我建立了TokenHolder這個類,把token儲存到了Session中;
/**
 * 登入token儲存(在Session中)
 * Created by macro on 2022/1/19.
 */
@Component
public class TokenHolder {
    /**
     * 新增token
     */
    public void putToken(String token) {
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = ((ServletRequestAttributes) ra).getRequest();
        request.getSession().setAttribute("token", token);
    }

    /**
     * 獲取token
     */
    public String getToken() {
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = ((ServletRequestAttributes) ra).getRequest();
        Object token = request.getSession().getAttribute("token");
        if(token!=null){
            return (String) token;
        }
        return null;
    }

}

註解式攔截器

商品品牌管理介面,需要新增登入認證頭才可以正常訪問,我們可以使用Retrofit中的註解式攔截器來實現。
  • 首先建立一個註解式攔截器TokenInterceptor繼承BasePathMatchInterceptor,然後在doIntercept方法中給請求新增Authorization頭;
/**
 * 給請求新增登入Token頭的攔截器
 * Created by macro on 2022/1/19.
 */
@Component
public class TokenInterceptor extends BasePathMatchInterceptor {
    @Autowired
    private TokenHolder tokenHolder;

    @Override
    protected Response doIntercept(Chain chain) throws IOException {
        Request request = chain.request();
        if (tokenHolder.getToken() != null) {
            request = request.newBuilder()
                    .header("Authorization", tokenHolder.getToken())
                    .build();
        }
        return chain.proceed(request);
    }
}
  • 建立呼叫品牌管理介面的客戶端PmsBrandApi,使用@Intercept註解配置攔截器和攔截路徑;
/**
 * 定義Http介面,用於呼叫遠端的PmsBrand服務
 * Created by macro on 2022/1/19.
 */
@RetrofitClient(baseUrl = "${remote.baseUrl}")
@Intercept(handler = TokenInterceptor.class, include = "/brand/**")
public interface PmsBrandApi {
    @GET("brand/list")
    CommonResult<CommonPage<PmsBrand>> list(@Query("pageNum") Integer pageNum, @Query("pageSize") Integer pageSize);

    @GET("brand/{id}")
    CommonResult<PmsBrand> detail(@Path("id") Long id);

    @POST("brand/create")
    CommonResult create(@Body PmsBrand pmsBrand);

    @POST("brand/update/{id}")
    CommonResult update(@Path("id") Long id, @Body PmsBrand pmsBrand);

    @GET("brand/delete/{id}")
    CommonResult delete(@Path("id") Long id);
}
  • 再在Controller中注入PmsBrandApi例項,並新增方法呼叫遠端服務即可;
/**
 * Retrofit測試介面
 * Created by macro on 2022/1/19.
 */
@Api(tags = "RetrofitController", description = "Retrofit測試介面")
@RestController
@RequestMapping("/retrofit")
public class RetrofitController {
    
    @Autowired
    private PmsBrandApi pmsBrandApi;

    @ApiOperation("呼叫遠端介面分頁查詢品牌列表")
    @GetMapping(value = "/brand/list")
    public CommonResult<CommonPage<PmsBrand>> listBrand(@RequestParam(value = "pageNum", defaultValue = "1")
                                                        @ApiParam("頁碼") Integer pageNum,
                                                        @RequestParam(value = "pageSize", defaultValue = "3")
                                                        @ApiParam("每頁數量") Integer pageSize) {
        return pmsBrandApi.list(pageNum, pageSize);
    }

    @ApiOperation("呼叫遠端介面獲取指定id的品牌詳情")
    @GetMapping(value = "/brand/{id}")
    public CommonResult<PmsBrand> brand(@PathVariable("id") Long id) {
        return pmsBrandApi.detail(id);
    }

    @ApiOperation("呼叫遠端介面新增品牌")
    @PostMapping(value = "/brand/create")
    public CommonResult createBrand(@RequestBody PmsBrand pmsBrand) {
        return pmsBrandApi.create(pmsBrand);
    }
    @ApiOperation("呼叫遠端介面更新指定id品牌資訊")
    @PostMapping(value = "/brand/update/{id}")
    public CommonResult updateBrand(@PathVariable("id") Long id, @RequestBody PmsBrand pmsBrand) {
        return pmsBrandApi.update(id,pmsBrand);
    }

    @ApiOperation("呼叫遠端介面刪除指定id的品牌")
    @GetMapping(value = "/delete/{id}")
    public CommonResult deleteBrand(@PathVariable("id") Long id) {
        return  pmsBrandApi.delete(id);
    }
}
  • 在Swagger中呼叫介面進行測試,發現已經可以成功呼叫。

全域性攔截器

如果你想給所有請求都加個請求頭的話,可以使用全域性攔截器。

建立SourceInterceptor類繼承BaseGlobalInterceptor介面,然後在Header中新增source請求頭。

/**
 * 全域性攔截器,給請求新增source頭
 * Created by macro on 2022/1/19.
 */
@Component
public class SourceInterceptor extends BaseGlobalInterceptor {
    @Override
    protected Response doIntercept(Chain chain) throws IOException {
        Request request = chain.request();
        Request newReq = request.newBuilder()
                .addHeader("source", "retrofit")
                .build();
        return chain.proceed(newReq);
    }
}

配置

Retrofit的配置很多,下面我們講講日誌列印、全域性超時時間和全域性請求重試這三種最常用的配置。

日誌列印

  • 預設配置下Retrofit使用basic日誌策略,列印的日誌非常簡單;

  • 我們可以將application.yml中的retrofit.global-log-strategy屬性修改為body來列印最全日誌;
retrofit:
  # 日誌列印配置
  log:
    # 啟用日誌列印
    enable: true
    # 日誌列印攔截器
    logging-interceptor: com.github.lianjiatech.retrofit.spring.boot.interceptor.DefaultLoggingInterceptor
    # 全域性日誌列印級別
    global-log-level: info
    # 全域性日誌列印策略
    global-log-strategy: body
  • 修改日誌列印策略後,日誌資訊更全面了;

  • Retrofit支援四種日誌列印策略;

    • NONE:不列印日誌;
    • BASIC:只列印日誌請求記錄;
    • HEADERS:列印日誌請求記錄、請求和響應頭資訊;
    • BODY:列印日誌請求記錄、請求和響應頭資訊、請求和響應體資訊。

全域性超時時間

有時候我們需要修改一下Retrofit的請求超時時間,可以通過如下配置實現。

retrofit:
  # 全域性連線超時時間
  global-connect-timeout-ms: 3000
  # 全域性讀取超時時間
  global-read-timeout-ms: 3000
  # 全域性寫入超時時間
  global-write-timeout-ms: 35000
  # 全域性完整呼叫超時時間
  global-call-timeout-ms: 0

全域性請求重試

  • retrofit-spring-boot-starter支援請求重試,可以通過如下配置實現。
retrofit:
  # 重試配置
  retry:
    # 是否啟用全域性重試
    enable-global-retry: true
    # 全域性重試間隔時間
    global-interval-ms: 100
    # 全域性最大重試次數
    global-max-retries: 2
    # 全域性重試規則
    global-retry-rules:
      - response_status_not_2xx
      - occur_exception
    # 重試攔截器
    retry-interceptor: com.github.lianjiatech.retrofit.spring.boot.retry.DefaultRetryInterceptor
  • 重試規則global-retry-rules支援如下三種配置。

    • RESPONSE_STATUS_NOT_2XX:響應狀態碼不是2xx時執行重試;
    • OCCUR_IO_EXCEPTION:發生IO異常時執行重試;
    • OCCUR_EXCEPTION:發生任意異常時執行重試。

總結

今天體驗了一把Retrofit,對比使用HttpUtil,確實優雅不少!通過介面發起HTTP請求已不再是Feign的專屬,通過Retrofit我們在單體應用中照樣可以使用這種方式。當然retrofit-spring-boot-starter提供的功能遠不止於此,它還能支援微服務間的呼叫和熔斷降級,感興趣的朋友可以研究下!

參考資料

官方文件:https://github.com/LianjiaTec...

專案原始碼地址

https://github.com/macrozheng...

本文 GitHub https://github.com/macrozheng/mall-learning 已經收錄,歡迎大家Star!

相關文章