因為 API first, RESTful Service 的取用是常見的場景。Java 在開發上,有一個難題的是同一個功能性的,會有很多家不同的實作。例如 JSON 有 gson, jsonb, jackson..., 而 HTTP Client 也是百家爭鳴,例如有以下
Quarkus 生態系中帶的是 Eclipse MicroProfile HTTP Client, 可以用少少設定就把外部的 RESTful Service 當作自家的 service。有點像是 proxy的感覺。Quarkus 有分 Rest Client 與 Reactive Rest Client,不過今天是要利用 Kotlin 的 croutines 把 Rest Client 轉成 Reactive
今天要打的目標是這邊 https://www.fruityvice.com/api/fruit/banana
GET 回來是
{"name":"Orange","genus":"Citrus","id":2,"family":"Rutaceae","order":"Sapindales","nutritions":{"carbohydrates":8,"protein":1,"fat":0.2,"calories":43,"sugar":8.2}}
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-mutiny</artifactId>
</dependency>
根據 Json 長相建立 class , 也可以利用 JSON To Kotlin Class plugin 作建立
data class FruityVice(
val name: String,
val genus: String,
val id: Long,
val family: String,
val order: String,
val nutritions: Nutrition
)
data class Nutrition(val carbohydrates: Long, val protein: Long, val fat: Double, val calories: Long, val sugar: Double)
這個雖然長的很像 RESTful endpoint , 但是要注意他是 interface, PATH 只會有相對的要打出去的 uri, Host 要在properties 設定。用 Uni 表示 async call
@Path("/api/fruit")
@RegisterRestClient(configKey = "fruits-api")
interface FruityViceService {
@GET
@Path("/{name}")
@Produces(MediaType.APPLICATION_JSON)
fun getFruitByName(@PathParam("name") name: String): Uni<FruityVice>
}
根據剛剛的 config-key,在 properties 設定 host
quarkus.rest-client.fruits-api.url=https://www.fruityvice.com
quarkus.rest-client.fruits-api.scope=javax.inject.Singleton
@ApplicationScoped
class FruitService(@RestClient val fruityViceService: FruityViceService) { // (1)
suspend fun findByName(name: String) = Either.catch {
fruityViceService.getFruitByName(name)
.onFailure().retry().atMost(3).awaitSuspending()
}.mapLeft {
when {
it.message.orEmpty().contains("status code 404") -> AppError.NoThisFruit(name)
else -> AppError.FruitServiceCallError(it)
}
}
}
@Path("/fruits")
class FruitResource(val fruitService: FruitService) { //(1)
@GET
@Path("/{name}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
suspend fun fruit(@PathParam("name") name: String) = fruitService.findByName(name).fold(
ifRight = ::identity, //(2)
ifLeft = { AppError.toResponse(it) }
)
}
有相關的 method 與進階 header 要如何傳入的設定。
在封閉的網路環境,需要作一些憑證的全信任,Quarkus 可以直接設定。
quarkus.rest-client.extensions-api.hostname-verifier=io.quarkus.restclient.NoopHostnameVerifier
quarkus.tls.trust-all=true
https://github.com/hmchangm/getting-start-QK/tree/ironman2022-d26-jdb/src/main
Ktor Client 可以參考我們鐵人賽團隊的文章,也是個不錯的選擇
Kotlin 全面啟動 Ktor Client III