還在用HttpUtil?試試這款優(yōu)雅的HTTP客戶端工具吧,跟SpringBoot絕配!
我們平時(shí)開發(fā)項(xiàng)目時(shí),就算是單體應(yīng)用,也免不了要調(diào)用一下其他服務(wù)提供的接口。此時(shí)就會(huì)用到HTTP客戶端工具,之前一直使用的是Hutool中的HttpUtil,雖然容易上手,但用起來頗為麻煩!最近發(fā)現(xiàn)一款更好用的HTTP客戶端工具
Retrofit,你只需聲明接口就可發(fā)起HTTP請(qǐng)求,無需進(jìn)行連接、結(jié)果解析之類的重復(fù)操作,用起來夠優(yōu)雅,推薦給大家!
簡(jiǎn)介
Retrofit是適用于Android和Java且類型安全的HTTP客戶端工具,在Github上已經(jīng)有39k+Star。其最大的特性的是支持通過接口的方式發(fā)起HTTP請(qǐng)求,類似于我們用Feign調(diào)用微服務(wù)接口的那種方式。

SpringBoot是使用最廣泛的Java開發(fā)框架,但是Retrofit官方并沒有提供專門的Starter。于是有位老哥就開發(fā)了retrofit-spring-boot-starter,它實(shí)現(xiàn)了Retrofit與SpringBoot框架的快速整合,并且支持了諸多功能增強(qiáng),極大簡(jiǎn)化開發(fā)。今天我們將使用這個(gè)第三方Starter來操作Retrofit。

使用
在SpringBoot中使用Retrofit是非常簡(jiǎn)單的,下面我們就來體驗(yàn)下。
依賴集成
有了第三方Starter的支持,集成Retrofit僅需一步,添加如下依賴即可。
<dependency>
????<groupId>com.github.lianjiatechgroupId>
????<artifactId>retrofit-spring-boot-starterartifactId>
????<version>2.2.18version>
dependency>
基本使用
下面以調(diào)用
mall-tiny-swagger中的接口為例,我們來體驗(yàn)下Retrofit的基本使用。
首先我們準(zhǔn)備一個(gè)服務(wù)來方便遠(yuǎn)程調(diào)用,使用的是之前的 mall-tiny-swagger這個(gè)Demo,打開Swagger看下,里面有一個(gè)登錄接口和需要登錄認(rèn)證的商品品牌CRUD接口,項(xiàng)目地址:https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-swagger

我們先來調(diào)用下登錄接口試試,在 application.yml中配置好mall-tiny-swagger的服務(wù)地址;
remote:
??baseUrl:?http://localhost:8088/
再通過 @RetrofitClient聲明一個(gè)Retrofit客戶端,由于登錄接口是通過POST表單形式調(diào)用的,這里使用到了@POST和@FormUrlEncoded注解;
/**
?*?定義Http接口,用于調(diào)用遠(yuǎn)程的UmsAdmin服務(wù)
?*?Created?by?macro?on?2022/1/19.
?*/
@RetrofitClient(baseUrl?=?"${remote.baseUrl}")
public?interface?UmsAdminApi?{
????@FormUrlEncoded
????@POST("admin/login")
????CommonResult?login(@Field("username")?String?username,?@Field("password")?String?password) ;
}
如果你不太明白這些注解是干嘛的,看下下面的表基本就懂了,更具體的話可以參考Retrofit官方文檔;

接下來在Controller中注入 UmsAdminApi,然后進(jìn)行調(diào)用即可;
/**
?*?Retrofit測(cè)試接口
?*?Created?by?macro?on?2022/1/19.
?*/
@Api(tags?=?"RetrofitController",?description?=?"Retrofit測(cè)試接口")
@RestController
@RequestMapping("/retrofit")
public?class?RetrofitController?{
????@Autowired
????private?UmsAdminApi?umsAdminApi;
????@Autowired
????private?TokenHolder?tokenHolder;
????@ApiOperation(value?=?"調(diào)用遠(yuǎn)程登錄接口獲取token")
????@PostMapping(value?=?"/admin/login")
????public?CommonResult?login(@RequestParam?String?username,?@RequestParam?String?password)? {
????????CommonResult?result?=?umsAdminApi.login(username,?password);
????????LoginInfo?loginInfo?=?result.getData();
????????if?(result.getData()?!=?null)?{
????????????tokenHolder.putToken(loginInfo.getTokenHead()?+?"?"?+?loginInfo.getToken());
????????}
????????return?result;
????}
}
為方便后續(xù)調(diào)用需要登錄認(rèn)證的接口,我創(chuàng)建了 TokenHolder這個(gè)類,把token存儲(chǔ)到了Session中;
/**
?*?登錄token存儲(chǔ)(在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;
????}
}
接下來通過Swagger進(jìn)行測(cè)試,調(diào)用接口就可以獲取到遠(yuǎn)程服務(wù)返回的token了,訪問地址:http://localhost:8086/swagger-ui/

注解式攔截器
商品品牌管理接口,需要添加登錄認(rèn)證頭才可以正常訪問,我們可以使用Retrofit中的注解式攔截器來實(shí)現(xiàn)。
首先創(chuàng)建一個(gè)注解式攔截器 TokenInterceptor繼承BasePathMatchInterceptor,然后在doIntercept方法中給請(qǐng)求添加Authorization頭;
/**
?*?給請(qǐng)求添加登錄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);
????}
}
創(chuàng)建調(diào)用品牌管理接口的客戶端 PmsBrandApi,使用@Intercept注解配置攔截器和攔截路徑;
/**
?*?定義Http接口,用于調(diào)用遠(yuǎn)程的PmsBrand服務(wù)
?*?Created?by?macro?on?2022/1/19.
?*/
@RetrofitClient(baseUrl?=?"${remote.baseUrl}")
@Intercept(handler?=?TokenInterceptor.class,?include?=?"/brand/**")
public?interface?PmsBrandApi?{
????@GET("brand/list")
????CommonResult>?list(@Query("pageNum")?Integer?pageNum,?@Query("pageSize")?Integer?pageSize);
????@GET("brand/{id}")
????CommonResult?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實(shí)例,并添加方法調(diào)用遠(yuǎn)程服務(wù)即可;
/**
?*?Retrofit測(cè)試接口
?*?Created?by?macro?on?2022/1/19.
?*/
@Api(tags?=?"RetrofitController",?description?=?"Retrofit測(cè)試接口")
@RestController
@RequestMapping("/retrofit")
public?class?RetrofitController?{
????
????@Autowired
????private?PmsBrandApi?pmsBrandApi;
????@ApiOperation("調(diào)用遠(yuǎn)程接口分頁(yè)查詢品牌列表")
????@GetMapping(value?=?"/brand/list")
????public?CommonResult>?listBrand(@RequestParam(value?=?"pageNum",?defaultValue?=?"1")
????????????????????????????????????????????????????????@ApiParam("頁(yè)碼")?Integer?pageNum,
????????????????????????????????????????????????????????@RequestParam(value?=?"pageSize",?defaultValue?=?"3")
????????????????????????????????????????????????????????@ApiParam("每頁(yè)數(shù)量")?Integer?pageSize)?{
????????return?pmsBrandApi.list(pageNum,?pageSize);
????}
????@ApiOperation("調(diào)用遠(yuǎn)程接口獲取指定id的品牌詳情")
????@GetMapping(value?=?"/brand/{id}")
????public?CommonResult?brand(@PathVariable("id")?Long?id)? {
????????return?pmsBrandApi.detail(id);
????}
????@ApiOperation("調(diào)用遠(yuǎn)程接口添加品牌")
????@PostMapping(value?=?"/brand/create")
????public?CommonResult?createBrand(@RequestBody?PmsBrand?pmsBrand)?{
????????return?pmsBrandApi.create(pmsBrand);
????}
????@ApiOperation("調(diào)用遠(yuǎn)程接口更新指定id品牌信息")
????@PostMapping(value?=?"/brand/update/{id}")
????public?CommonResult?updateBrand(@PathVariable("id")?Long?id,?@RequestBody?PmsBrand?pmsBrand)?{
????????return?pmsBrandApi.update(id,pmsBrand);
????}
????@ApiOperation("調(diào)用遠(yuǎn)程接口刪除指定id的品牌")
????@GetMapping(value?=?"/delete/{id}")
????public?CommonResult?deleteBrand(@PathVariable("id")?Long?id)?{
????????return??pmsBrandApi.delete(id);
????}
}
在Swagger中調(diào)用接口進(jìn)行測(cè)試,發(fā)現(xiàn)已經(jīng)可以成功調(diào)用。

全局?jǐn)r截器
如果你想給所有請(qǐng)求都加個(gè)請(qǐng)求頭的話,可以使用全局?jǐn)r截器。
創(chuàng)建SourceInterceptor類繼承BaseGlobalInterceptor接口,然后在Header中添加source請(qǐng)求頭。
/**
?*?全局?jǐn)r截器,給請(qǐng)求添加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的配置很多,下面我們講講日志打印、全局超時(shí)時(shí)間和全局請(qǐng)求重試這三種最常用的配置。
日志打印
默認(rèn)配置下Retrofit使用 basic日志策略,打印的日志非常簡(jiǎn)單;

我們可以將 application.yml中的retrofit.global-log-strategy屬性修改為body來打印最全日志;
retrofit:
??#?日志打印配置
??log:
????#?啟用日志打印
????enable:?true
????#?日志打印攔截器
????logging-interceptor:?com.github.lianjiatech.retrofit.spring.boot.interceptor.DefaultLoggingInterceptor
????#?全局日志打印級(jí)別
????global-log-level:?info
????#?全局日志打印策略
????global-log-strategy:?body
修改日志打印策略后,日志信息更全面了;

Retrofit支持四種日志打印策略; NONE:不打印日志; BASIC:只打印日志請(qǐng)求記錄; HEADERS:打印日志請(qǐng)求記錄、請(qǐng)求和響應(yīng)頭信息; BODY:打印日志請(qǐng)求記錄、請(qǐng)求和響應(yīng)頭信息、請(qǐng)求和響應(yīng)體信息。
全局超時(shí)時(shí)間
有時(shí)候我們需要修改一下Retrofit的請(qǐng)求超時(shí)時(shí)間,可以通過如下配置實(shí)現(xiàn)。
retrofit:
??#?全局連接超時(shí)時(shí)間
??global-connect-timeout-ms:?3000
??#?全局讀取超時(shí)時(shí)間
??global-read-timeout-ms:?3000
??#?全局寫入超時(shí)時(shí)間
??global-write-timeout-ms:?35000
??#?全局完整調(diào)用超時(shí)時(shí)間
??global-call-timeout-ms:?0
全局請(qǐng)求重試
retrofit-spring-boot-starter支持請(qǐng)求重試,可以通過如下配置實(shí)現(xiàn)。
retrofit:
??#?重試配置
??retry:
????#?是否啟用全局重試
????enable-global-retry:?true
????#?全局重試間隔時(shí)間
????global-interval-ms:?100
????#?全局最大重試次數(shù)
????global-max-retries:?2
????#?全局重試規(guī)則
????global-retry-rules:
??????-?response_status_not_2xx
??????-?occur_exception
????#?重試攔截器
????retry-interceptor:?com.github.lianjiatech.retrofit.spring.boot.retry.DefaultRetryInterceptor
重試規(guī)則 global-retry-rules支持如下三種配置。RESPONSE_STATUS_NOT_2XX:響應(yīng)狀態(tài)碼不是2xx時(shí)執(zhí)行重試; OCCUR_IO_EXCEPTION:發(fā)生IO異常時(shí)執(zhí)行重試; OCCUR_EXCEPTION:發(fā)生任意異常時(shí)執(zhí)行重試。
總結(jié)
今天體驗(yàn)了一把Retrofit,對(duì)比使用HttpUtil,確實(shí)優(yōu)雅不少!通過接口發(fā)起HTTP請(qǐng)求已不再是Feign的專屬,通過Retrofit我們?cè)趩误w應(yīng)用中照樣可以使用這種方式。當(dāng)然retrofit-spring-boot-starter提供的功能遠(yuǎn)不止于此,它還能支持微服務(wù)間的調(diào)用和熔斷降級(jí),感興趣的朋友可以研究下!
參考資料
官方文檔:https://github.com/LianjiaTech/retrofit-spring-boot-starter
項(xiàng)目源碼地址
https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-retrofit

