Micronaut 使用 @Client 的聲明式 HTTP 客戶端

2023-03-08 15:34 更新

現(xiàn)在您已經(jīng)了解了底層 HTTP 客戶端的工作原理,讓我們來看看 Micronaut 通過 Client 注解對聲明式客戶端的支持。

從本質(zhì)上講,@Client注解可以聲明在任何接口或抽象類上,通過使用Introduction Advice在編譯時為你實現(xiàn)抽象方法,大大簡化了HTTP客戶端的創(chuàng)建。

讓我們從一個簡單的例子開始。給定以下課程:

Pet.java

 Java Groovy  Kotlin 
public class Pet {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
class Pet {
    String name
    int age
}
class Pet {
    var name: String? = null
    var age: Int = 0
}

您可以定義一個通用接口來保存新的 Pet 實例:

PetOperations.java

 Java Groovy  Kotlin 
import io.micronaut.http.annotation.Post;
import io.micronaut.validation.Validated;
import org.reactivestreams.Publisher;
import io.micronaut.core.async.annotation.SingleResult;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;

@Validated
public interface PetOperations {
    @Post
    @SingleResult
    Publisher<Pet> save(@NotBlank String name, @Min(1L) int age);
}
import io.micronaut.http.annotation.Post
import io.micronaut.validation.Validated
import org.reactivestreams.Publisher
import io.micronaut.core.async.annotation.SingleResult
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank

@Validated
interface PetOperations {
    @Post
    @SingleResult
    Publisher<Pet> save(@NotBlank String name, @Min(1L) int age)
}
import io.micronaut.http.annotation.Post
import io.micronaut.validation.Validated
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
import io.micronaut.core.async.annotation.SingleResult
import org.reactivestreams.Publisher

@Validated
interface PetOperations {
    @Post
    @SingleResult
    fun save(@NotBlank name: String, @Min(1L) age: Int): Publisher<Pet>
}

請注意該接口如何使用可在服務器端和客戶端使用的 Micronaut HTTP 注釋。您還可以使用 javax.validation 約束來驗證參數(shù)。

請注意,某些注釋(例如 Produces 和 Consumes)在服務器端和客戶端用法之間具有不同的語義。例如,控制器方法(服務器端)上的@Produces 指示方法的返回值如何格式化,而客戶端上的@Produces 指示方法的參數(shù)在發(fā)送到服務器時如何格式化。雖然這看起來有點令人困惑,但考慮到服務器生產(chǎn)/消費與客戶端之間的不同語義是合乎邏輯的:服務器使用參數(shù)并將響應返回給客戶端,而客戶端使用參數(shù)并將輸出發(fā)送到服務器.

此外,要使用 javax.validation 功能,請將驗證模塊添加到您的構(gòu)建中:

 Gradle Maven 
implementation("io.micronaut:micronaut-validation")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-validation</artifactId>
</dependency>

在 Micronaut 的服務器端,您可以實現(xiàn) PetOperations 接口:

PetController.java

 Java Groovy  Kotlin 
import io.micronaut.http.annotation.Controller;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import io.micronaut.core.async.annotation.SingleResult;

@Controller("/pets")
public class PetController implements PetOperations {

    @Override
    @SingleResult
    public Publisher<Pet> save(String name, int age) {
        Pet pet = new Pet();
        pet.setName(name);
        pet.setAge(age);
        // save to database or something
        return Mono.just(pet);
    }
}
import io.micronaut.http.annotation.Controller
import org.reactivestreams.Publisher
import io.micronaut.core.async.annotation.SingleResult
import reactor.core.publisher.Mono

@Controller("/pets")
class PetController implements PetOperations {

    @Override
    @SingleResult
    Publisher<Pet> save(String name, int age) {
        Pet pet = new Pet(name: name, age: age)
        // save to database or something
        return Mono.just(pet)
    }
}
import io.micronaut.http.annotation.Controller
import reactor.core.publisher.Mono
import io.micronaut.core.async.annotation.SingleResult
import org.reactivestreams.Publisher

@Controller("/pets")
open class PetController : PetOperations {

    @SingleResult
    override fun save(name: String, age: Int): Publisher<Pet> {
        val pet = Pet()
        pet.name = name
        pet.age = age
        // save to database or something
        return Mono.just(pet)
    }
}

然后,您可以在 src/test/java 中定義一個聲明式客戶端,它使用 @Client 在編譯時自動實現(xiàn)客戶端:

PetClient.java

 Java Groovy  Kotlin 
import io.micronaut.http.client.annotation.Client;
import org.reactivestreams.Publisher;
import io.micronaut.core.async.annotation.SingleResult;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;

@Client("/pets") // (1)
public interface PetClient extends PetOperations { // (2)

    @Override
    @SingleResult
    Publisher<Pet> save(@NotBlank String name, @Min(1L) int age); // (3)
}
import io.micronaut.http.client.annotation.Client
import org.reactivestreams.Publisher
import io.micronaut.core.async.annotation.SingleResult

@Client("/pets") // (1)
interface PetClient extends PetOperations { // (2)

    @Override
    @SingleResult
    Publisher<Pet> save(String name, int age) // (3)
}
import io.micronaut.http.client.annotation.Client
import io.micronaut.core.async.annotation.SingleResult
import org.reactivestreams.Publisher

@Client("/pets") // (1)
interface PetClient : PetOperations { // (2)

    @SingleResult
    override fun save(name: String, age: Int): Publisher<Pet> // (3)
}
  1. Client 注釋與相對于當前服務器的值一起使用,在本例中為 /pets

  2. 該接口擴展自 PetOperations

  3. 保存方法被覆蓋。請參閱下面的警告。

請注意,在上面的示例中,我們重寫了 save 方法。如果您在沒有 -parameters 選項的情況下進行編譯,這是必需的,因為 Java 不會在字節(jié)碼中保留參數(shù)名稱,否則。如果您使用 -parameters 進行編譯,則不需要覆蓋。此外,當覆蓋方法時,您應該確保再次聲明任何驗證注釋,因為這些不是繼承注釋。

一旦你定義了一個客戶端,你可以在任何你需要的地方@Inject它。

回想一下@Client 的值可以是:

  • 絕對 URI,例如https://api.twitter.com/1.1

  • 相對 URI,在這種情況下目標服務器是當前服務器(對測試有用)

  • 服務標識符。

在生產(chǎn)中,您通常使用服務 ID 和服務發(fā)現(xiàn)來自動發(fā)現(xiàn)服務。

關(guān)于上面示例中的 save 方法,另一個需要注意的重要事項是它返回 Single 類型。

這是一種非阻塞反應類型——通常您希望您的 HTTP 客戶端不阻塞。在某些情況下,您可能需要一個確實阻塞的 HTTP 客戶端(例如在單元測試中),但這種情況很少見。

下表說明了可用于@Client 的常見返回類型:

表 1. Micronaut 響應類型
類型 描述 示例簽名

Publisher

任何實現(xiàn) Publisher 接口的類型

Flux<String> hello()

HttpResponse

HttpResponse 和可選的響應主體類型

Mono<HttpResponse<String>> hello()

Publisher

發(fā)出 POJO 的 Publisher 實現(xiàn)

Mono<Book> hello()

CompletableFuture

Java CompletableFuture 實例

CompletableFuture<String> hello()

CharSequence

阻塞本機類型。比如String

String hello()

T

任何簡單的 POJO 類型。

Book show()

通常,任何可以轉(zhuǎn)換為 Publisher 接口的反應類型都支持作為返回類型,包括(但不限于)RxJava 1.x、RxJava 2.x 和 Reactor 3.x 定義的反應類型。

還支持返回 CompletableFuture 實例。請注意,返回任何其他類型都會導致阻塞請求,除非用于測試,否則不推薦使用。

自定義參數(shù)綁定

前面的示例展示了一個使用方法參數(shù)表示 POST 請求正文的簡單示例:

PetOperations.java

@Post
@SingleResult
Publisher<Pet> save(@NotBlank String name, @Min(1L) int age);

默認情況下,save 方法使用以下 JSON 執(zhí)行 HTTP POST:

Example Produced JSON

{"name":"Dino", "age":10}

但是,您可能想要自定義作為正文、參數(shù)、URI 變量等發(fā)送的內(nèi)容。@Client 注釋在這方面非常靈活,并且支持與 Micronaut 的 HTTP 服務器相同的 io.micronaut.http.annotation。

例如,下面定義了一個 URI 模板,name 參數(shù)用作 URI 模板的一部分,而@Body 聲明要發(fā)送到服務器的內(nèi)容由 Pet POJO 表示:

PetOperations.java

@Post("/{name}")
Mono<Pet> save(
    @NotBlank String name, (1)
    @Body @Valid Pet pet) (2)
  1. name 參數(shù),包含在 URI 中,并聲明為 @NotBlank

  2. pet參數(shù),用于對body進行編碼,聲明為@Valid

下表總結(jié)了參數(shù)注釋及其用途,并提供了示例:

表 1. 參數(shù)綁定注解
注解 描述 示例

@Body

指定請求正文的參數(shù)

@Body String body

@CookieValue

指定要作為 cookie 發(fā)送的參數(shù)

@CookieValue String myCookie

@Header

指定要作為 HTTP 標頭發(fā)送的參數(shù)

@Header String requestId

@QueryValue

自定義要綁定的 URI 參數(shù)的名稱

@QueryValue("userAge") Integer age

@PathVariable

專門從路徑變量綁定參數(shù)。

@PathVariable Long id

@RequestAttribute

指定要設置為請求屬性的參數(shù)

@RequestAttribute Integer locationId

始終使用 @Produces 或 @Consumes 而不是為 Content-Type 或 Accept 提供標頭。

查詢值格式化

Format 注釋可以與@QueryValue 注釋一起使用來格式化查詢值。

支持的值為:“csv”、“ssv”、“pipes”、“multi”和“deep-object”,其含義類似于 Open API v3 查詢參數(shù)的 style 屬性。

該格式只能應用于 java.lang.Iterable、java.util.Map 或帶有 Introspected 注釋的 POJO。下表給出了如何格式化不同值的示例:

格式 可迭代的例子 Map 或 POJO 示例

Original value

["Mike", "Adam", "Kate"]

{"name": "Mike", "age": 30"}

"CSV"

"param=Mike,Adam,Kate"

"param=name,Mike,age,30"

"SSV"

"param=Mike Adam Kate"

"param=name Mike age 30"

"PIPES"

"param=Mike|Adam|Kate"

"param=name|Mike|age|30"

"MULTI"

"param=Mike?m=Adam&param=Kate"

"name=Mike&age=30"

"DEEP_OBJECT"

"param[0]=Mike&param[1]=Adam&param[2]=Kate"

"param[name]=Mike&param[age]=30"

基于類型的綁定參數(shù)

一些參數(shù)通過它們的類型而不是它們的注釋來識別。下表總結(jié)了這些參數(shù)類型及其用途,并提供了一個示例:

類型 描述 示例

BasicAuth

設置授權(quán)標頭

BasicAuth basicAuth

HttpHeaders

向請求添加多個標頭

HttpHeaders headers

Cookies

向請求添加多個 cookie

Cookies cookies

Cookie

向請求添加 cookie

Cookie cookie

Locale

設置接受語言標頭。使用 @QueryValue 或 @PathVariable 注釋以填充 URI 變量。

Locale locale

自定義綁定

ClientArgumentRequestBinder API 將客戶端參數(shù)綁定到請求。在綁定過程中自動使用注冊為 beans 的自定義綁定器類。首先搜索基于注釋的綁定器,如果未找到綁定器,則搜索基于類型的綁定器。

通過注解綁定

要根據(jù)參數(shù)上的注釋控制參數(shù)如何綁定到請求,請創(chuàng)建類型為 AnnotatedClientArgumentRequestBinder 的 bean。任何自定義注解都必須使用@Bindable 進行注解。

在此示例中,請參閱以下客戶端:

帶有@Metadata 參數(shù)的客戶端

 Java Groovy  Kotlin
@Client("/")
public interface MetadataClient {

    @Get("/client/bind")
    String get(@Metadata Map<String, Object> metadata);
}
@Client("/")
interface MetadataClient {

    @Get("/client/bind")
    String get(@Metadata Map metadata)
}
@Client("/")
interface MetadataClient {

    @Get("/client/bind")
    operator fun get(@Metadata metadata: Map<String, Any>): String
}

該參數(shù)使用以下注釋進行注釋:

@Metadata Annotation

 Java Groovy  Kotlin 
import io.micronaut.core.bind.annotation.Bindable;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Retention(RUNTIME)
@Target(PARAMETER)
@Bindable
public @interface Metadata {
}
import io.micronaut.core.bind.annotation.Bindable

import java.lang.annotation.Documented
import java.lang.annotation.Retention
import java.lang.annotation.Target

import static java.lang.annotation.ElementType.PARAMETER
import static java.lang.annotation.RetentionPolicy.RUNTIME

@Documented
@Retention(RUNTIME)
@Target(PARAMETER)
@Bindable
@interface Metadata {
}
import io.micronaut.core.bind.annotation.Bindable
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER

@MustBeDocumented
@Retention(RUNTIME)
@Target(VALUE_PARAMETER)
@Bindable
annotation class Metadata

在沒有任何額外代碼的情況下,客戶端嘗試將元數(shù)據(jù)轉(zhuǎn)換為字符串并將其附加為查詢參數(shù)。在這種情況下,這不是預期的效果,因此需要自定義活頁夾。

以下活頁夾處理傳遞給帶有 @Metadata 注釋的客戶端的參數(shù),并改變請求以包含所需的標頭??梢孕薷膶崿F(xiàn)以接受除 Map 之外的更多類型的數(shù)據(jù)。

注釋參數(shù)綁定器

 Java Groovy  Kotlin 
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.client.bind.AnnotatedClientArgumentRequestBinder;
import io.micronaut.http.client.bind.ClientRequestUriContext;

import jakarta.inject.Singleton;
import java.util.Map;

@Singleton
public class MetadataClientArgumentBinder implements AnnotatedClientArgumentRequestBinder<Metadata> {

    @NonNull
    @Override
    public Class<Metadata> getAnnotationType() {
        return Metadata.class;
    }

    @Override
    public void bind(@NonNull ArgumentConversionContext<Object> context,
                     @NonNull ClientRequestUriContext uriContext,
                     @NonNull Object value,
                     @NonNull MutableHttpRequest<?> request) {
        if (value instanceof Map) {
            for (Map.Entry<?, ?> entry: ((Map<?, ?>) value).entrySet()) {
                String key = NameUtils.hyphenate(StringUtils.capitalize(entry.getKey().toString()), false);
                request.header("X-Metadata-" + key, entry.getValue().toString());
            }
        }
    }
}
import io.micronaut.core.annotation.NonNull
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.naming.NameUtils
import io.micronaut.core.util.StringUtils
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.client.bind.AnnotatedClientArgumentRequestBinder
import io.micronaut.http.client.bind.ClientRequestUriContext

import jakarta.inject.Singleton

@Singleton
class MetadataClientArgumentBinder implements AnnotatedClientArgumentRequestBinder<Metadata> {

    final Class<Metadata> annotationType = Metadata

    @Override
    void bind(@NonNull ArgumentConversionContext<Object> context,
              @NonNull ClientRequestUriContext uriContext,
              @NonNull Object value,
              @NonNull MutableHttpRequest<?> request) {
        if (value instanceof Map) {
            for (entry in value.entrySet()) {
                String key = NameUtils.hyphenate(StringUtils.capitalize(entry.key as String), false)
                request.header("X-Metadata-$key", entry.value as String)
            }
        }
    }
}
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.naming.NameUtils
import io.micronaut.core.util.StringUtils
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.client.bind.AnnotatedClientArgumentRequestBinder
import io.micronaut.http.client.bind.ClientRequestUriContext
import jakarta.inject.Singleton

@Singleton
class MetadataClientArgumentBinder : AnnotatedClientArgumentRequestBinder<Metadata> {

    override fun getAnnotationType(): Class<Metadata> {
        return Metadata::class.java
    }

    override fun bind(context: ArgumentConversionContext<Any>,
                      uriContext: ClientRequestUriContext,
                      value: Any,
                      request: MutableHttpRequest<*>) {
        if (value is Map<*, *>) {
            for ((key1, value1) in value) {
                val key = NameUtils.hyphenate(StringUtils.capitalize(key1.toString()), false)
                request.header("X-Metadata-$key", value1.toString())
            }
        }
    }
}

按類型綁定

要根據(jù)參數(shù)類型綁定到請求,請創(chuàng)建類型為 TypedClientArgumentRequestBinder 的 bean。

在此示例中,請參閱以下客戶端:

具有元數(shù)據(jù)參數(shù)的客戶端

 Java Groovy  Kotlin 
@Client("/")
public interface MetadataClient {

    @Get("/client/bind")
    String get(Metadata metadata);
}
@Client("/")
interface MetadataClient {

    @Get("/client/bind")
    String get(Metadata metadata)
}
@Client("/")
interface MetadataClient {

    @Get("/client/bind")
    operator fun get(metadata: Metadata?): String?
}

在沒有任何額外代碼的情況下,客戶端嘗試將元數(shù)據(jù)轉(zhuǎn)換為字符串并將其附加為查詢參數(shù)。在這種情況下,這不是預期的效果,因此需要自定義活頁夾。

以下活頁夾處理傳遞給元數(shù)據(jù)類型客戶端的參數(shù),并改變請求以包含所需的標頭。

類型化參數(shù)綁定器

 Java Groovy  Kotlin 
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.type.Argument;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.client.bind.ClientRequestUriContext;
import io.micronaut.http.client.bind.TypedClientArgumentRequestBinder;

import jakarta.inject.Singleton;

@Singleton
public class MetadataClientArgumentBinder implements TypedClientArgumentRequestBinder<Metadata> {

    @Override
    @NonNull
    public Argument<Metadata> argumentType() {
        return Argument.of(Metadata.class);
    }

    @Override
    public void bind(@NonNull ArgumentConversionContext<Metadata> context,
                     @NonNull ClientRequestUriContext uriContext,
                     @NonNull Metadata value,
                     @NonNull MutableHttpRequest<?> request) {
        request.header("X-Metadata-Version", value.getVersion().toString());
        request.header("X-Metadata-Deployment-Id", value.getDeploymentId().toString());
    }
}
import io.micronaut.core.annotation.NonNull
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.type.Argument
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.client.bind.ClientRequestUriContext
import io.micronaut.http.client.bind.TypedClientArgumentRequestBinder

import jakarta.inject.Singleton

@Singleton
class MetadataClientArgumentBinder implements TypedClientArgumentRequestBinder<Metadata> {

    @Override
    @NonNull
    Argument<Metadata> argumentType() {
        Argument.of(Metadata)
    }

    @Override
    void bind(@NonNull ArgumentConversionContext<Metadata> context,
              @NonNull ClientRequestUriContext uriContext,
              @NonNull Metadata value,
              @NonNull MutableHttpRequest<?> request) {
        request.header("X-Metadata-Version", value.version.toString())
        request.header("X-Metadata-Deployment-Id", value.deploymentId.toString())
    }
}
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.type.Argument
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.client.bind.ClientRequestUriContext
import io.micronaut.http.client.bind.TypedClientArgumentRequestBinder
import jakarta.inject.Singleton

@Singleton
class MetadataClientArgumentBinder : TypedClientArgumentRequestBinder<Metadata> {

    override fun argumentType(): Argument<Metadata> {
        return Argument.of(Metadata::class.java)
    }

    override fun bind(
        context: ArgumentConversionContext<Metadata>,
        uriContext: ClientRequestUriContext,
        value: Metadata,
        request: MutableHttpRequest<*>
    ) {
        request.header("X-Metadata-Version", value.version.toString())
        request.header("X-Metadata-Deployment-Id", value.deploymentId.toString())
    }
}

綁定方法

也可以創(chuàng)建一個活頁夾,它將通過方法上的注釋更改請求。例如:

帶有注釋方法的客戶端

 Java Groovy  Kotlin 
@Client("/")
public interface NameAuthorizedClient {

    @Get("/client/authorized-resource")
    @NameAuthorization(name="Bob") // (1)
    String get();
}
@Client("/")
public interface NameAuthorizedClient {

    @Get("/client/authorized-resource")
    @NameAuthorization(name="Bob") // (1)
    String get()
}
@Client("/")
public interface NameAuthorizedClient {

    @Get("/client/authorized-resource")
    @NameAuthorization(name="Bob") // (1)
    fun get(): String
}
  1. @NameAuthorization 是注解一個方法

注釋定義為:

Annotation Definition

 Java Groovy  Kotlin 
@Documented
@Retention(RUNTIME)
@Target(METHOD) // (1)
@Bindable
public @interface NameAuthorization {
    @AliasFor(member = "name")
    String value() default "";

    @AliasFor(member = "value")
    String name() default "";
}
@Documented
@Retention(RUNTIME)
@Target(METHOD) // (1)
@Bindable
@interface NameAuthorization {
    @AliasFor(member = "name")
    String value() default ""

    @AliasFor(member = "value")
    String name() default ""
}
@MustBeDocumented
@Retention(RUNTIME)
@Target(FUNCTION) // (1)
@Bindable
annotation class NameAuthorization(val name: String = "")
  1.  它被定義為在方法上使用

以下活頁夾指定行為:

Annotation Definition

 Java Groovy  Kotlin 
@Singleton // (1)
public class NameAuthorizationBinder implements AnnotatedClientRequestBinder<NameAuthorization> { // (2)
    @NonNull
    @Override
    public Class<NameAuthorization> getAnnotationType() {
        return NameAuthorization.class;
    }

    @Override
    public void bind( // (3)
            @NonNull MethodInvocationContext<Object, Object> context,
            @NonNull ClientRequestUriContext uriContext,
            @NonNull MutableHttpRequest<?> request
    ) {
        context.getValue(NameAuthorization.class)
                .ifPresent(name -> uriContext.addQueryParameter("name", String.valueOf(name)));

    }
}
@Singleton // (1)
public class NameAuthorizationBinder implements AnnotatedClientRequestBinder<NameAuthorization> { // (2)
    @NonNull
    @Override
    Class<NameAuthorization> getAnnotationType() {
        return NameAuthorization.class
    }

    @Override
    void bind( // (3)
            @NonNull MethodInvocationContext<Object, Object> context,
            @NonNull ClientRequestUriContext uriContext,
            @NonNull MutableHttpRequest<?> request
    ) {
        context.getValue(NameAuthorization.class)
                .ifPresent(name -> uriContext.addQueryParameter("name", String.valueOf(name)))

    }
}
import io.micronaut.http.client.bind.AnnotatedClientRequestBinder

@Singleton // (1)
class NameAuthorizationBinder: AnnotatedClientRequestBinder<NameAuthorization> { // (2)
    @NonNull
    override fun getAnnotationType(): Class<NameAuthorization> {
        return NameAuthorization::class.java
    }

    override fun bind( // (3)
            @NonNull context: MethodInvocationContext<Any, Any>,
            @NonNull uriContext: ClientRequestUriContext,
            @NonNull request: MutableHttpRequest<*>
    ) {
        context.getValue(NameAuthorization::class.java, "name")
                .ifPresent { name -> uriContext.addQueryParameter("name", name.toString()) }

    }
}
  1. @Singleton 注解將其注冊到 Micronaut 上下文中

  2. 它實現(xiàn)了 AnnotatedClientRequestBinder<NameAuthorization>

  3. 自定義bind方法用于實現(xiàn)binder的行為

使用@Client 進行流式傳輸

@Client 注釋還可以處理流式 HTTP 響應。

使用@Client 流式傳輸 JSON

例如,要編寫一個從文檔的 JSON Streaming 部分中定義的控制器流式傳輸數(shù)據(jù)的客戶端,您可以定義一個返回未綁定發(fā)布者的客戶端,例如 Reactor 的 Flux 或 RxJava 的 Flowable:

HeadlineClient.java

 Java Groovy  Kotlin 
import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.annotation.Client;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import static io.micronaut.http.MediaType.APPLICATION_JSON_STREAM;

@Client("/streaming")
public interface HeadlineClient {

    @Get(value = "/headlines", processes = APPLICATION_JSON_STREAM) // (1)
    Publisher<Headline> streamHeadlines(); // (2)

}
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import org.reactivestreams.Publisher

import static io.micronaut.http.MediaType.APPLICATION_JSON_STREAM

@Client("/streaming")
interface HeadlineClient {

    @Get(value = "/headlines", processes = APPLICATION_JSON_STREAM) // (1)
    Publisher<Headline> streamHeadlines() // (2)

}
import io.micronaut.http.MediaType.APPLICATION_JSON_STREAM
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import reactor.core.publisher.Flux


@Client("/streaming")
interface HeadlineClient {

    @Get(value = "/headlines", processes = [APPLICATION_JSON_STREAM]) // (1)
    fun streamHeadlines(): Flux<Headline>  // (2)

}
  1. @Get 方法處理 APPLICATION_JSON_STREAM 類型的響應

  2. 返回類型是 Publisher

以下示例顯示了如何從測試中調(diào)用先前定義的 HeadlineClient:

Streaming HeadlineClient

 Java Groovy  Kotlin 
@Test
public void testClientAnnotationStreaming() {
    try(EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class)) {
        HeadlineClient headlineClient = embeddedServer
                                            .getApplicationContext()
                                            .getBean(HeadlineClient.class); // (1)

        Mono<Headline> firstHeadline = Mono.from(headlineClient.streamHeadlines()); // (2)

        Headline headline = firstHeadline.block(); // (3)

        assertNotNull(headline);
        assertTrue(headline.getText().startsWith("Latest Headline"));
    }
}
void "test client annotation streaming"() throws Exception {
    when:
    def headlineClient = embeddedServer.applicationContext
                                       .getBean(HeadlineClient) // (1)

    Mono<Headline> firstHeadline = Mono.from(headlineClient.streamHeadlines()) // (2)

    Headline headline = firstHeadline.block() // (3)

    then:
    headline
    headline.text.startsWith("Latest Headline")
}
"test client annotation streaming" {
    val headlineClient = embeddedServer
            .applicationContext
            .getBean(HeadlineClient::class.java) // (1)

    val firstHeadline = headlineClient.streamHeadlines().next() // (2)

    val headline = firstHeadline.block() // (3)

    headline shouldNotBe null
    headline.text shouldStartWith "Latest Headline"
}
  1. 從 ApplicationContext 中檢索客戶端

  2. next 方法將 Flux 發(fā)出的第一個元素發(fā)射到 Mono 中。

  3. block() 方法檢索測試中的結(jié)果。

流媒體客戶端和響應類型

上一節(jié)中定義的示例期望服務器響應 JSON 對象流,內(nèi)容類型為 application/x-json-stream。例如:

A JSON Stream

{"title":"The Stand"}
{"title":"The Shining"}

這樣做的原因是簡單的;事實上,一系列 JSON 對象不是有效的 JSON,因此響應內(nèi)容類型不能是 application/json。為了使 JSON 有效,它必須返回一個數(shù)組:

A JSON Array

[
    {"title":"The Stand"},
    {"title":"The Shining"}
]

然而,Micronaut 的客戶端確實支持通過 application/x-json-stream 流式傳輸單個 JSON 對象以及使用 application/json 定義的 JSON 數(shù)組。

如果服務器返回 application/json 并返回一個非單個 Publisher(例如 Reactor 的 Flux 或 RxJava 的 Flowable),則客戶端會在數(shù)組元素可用時對其進行流式傳輸。

流式傳輸客戶端和讀取超時

當流式傳輸來自服務器的響應時,底層 HTTP 客戶端不會應用 HttpClientConfiguration 的默認 readTimeout 設置(默認為 10 秒),因為流式響應的讀取之間的延遲可能與正常讀取不同。

相反,read-idle-timeout 設置(默認為 5 分鐘)指示連接變?yōu)榭臻e后何時關(guān)閉連接。

如果您從定義項目之間延遲超過 5 分鐘的服務器流式傳輸數(shù)據(jù),則應調(diào)整 readIdleTimeout。配置文件(例如 application.yml)中的以下配置演示了如何:

Adjusting the readIdleTimeout

 Properties Yaml  Toml  Groovy  Hocon  JSON 
micronaut.http.client.read-idle-timeout=10m
micronaut:
  http:
    client:
      read-idle-timeout: 10m
[micronaut]
  [micronaut.http]
    [micronaut.http.client]
      read-idle-timeout="10m"
micronaut {
  http {
    client {
      readIdleTimeout = "10m"
    }
  }
}
{
  micronaut {
    http {
      client {
        read-idle-timeout = "10m"
      }
    }
  }
}
{
  "micronaut": {
    "http": {
      "client": {
        "read-idle-timeout": "10m"
      }
    }
  }
}

上面的示例將 readIdleTimeout 設置為十分鐘。

流媒體服務器發(fā)送的事件

Micronaut 具有由 SseClient 接口定義的服務器發(fā)送事件 (SSE) 的本機客戶端。

您可以使用此客戶端從任何發(fā)出它們的服務器流式傳輸 SSE 事件。

雖然 SSE 流通常由瀏覽器 EventSource 使用,但在某些情況下您可能希望通過 SseClient 使用 SSE 流,例如在單元測試中或當 Micronaut 服務充當另一項服務的網(wǎng)關(guān)時。

@Client 注釋還支持使用 SSE 流。例如,考慮以下生成 SSE 事件流的控制器方法:

SSE Controller

 Java Groovy  Kotlin 
@Get(value = "/headlines", processes = MediaType.TEXT_EVENT_STREAM) // (1)
Publisher<Event<Headline>> streamHeadlines() {
    return Flux.<Event<Headline>>create((emitter) -> {  // (2)
        Headline headline = new Headline();
        headline.setText("Latest Headline at " + ZonedDateTime.now());
        emitter.next(Event.of(headline));
        emitter.complete();
    }, FluxSink.OverflowStrategy.BUFFER)
            .repeat(100) // (3)
            .delayElements(Duration.of(1, ChronoUnit.SECONDS)); // (4)
}
@Get(value = "/headlines", processes = MediaType.TEXT_EVENT_STREAM) // (1)
Flux<Event<Headline>> streamHeadlines() {
    Flux.<Event<Headline>>create( { emitter -> // (2)
        Headline headline = new Headline(text: "Latest Headline at ${ZonedDateTime.now()}")
        emitter.next(Event.of(headline))
        emitter.complete()
    }, FluxSink.OverflowStrategy.BUFFER)
            .repeat(100) // (3)
            .delayElements(Duration.of(1, ChronoUnit.SECONDS)) // (4)
}
@Get(value = "/headlines", processes = [TEXT_EVENT_STREAM]) // (1)
internal fun streamHeadlines(): Flux<Event<Headline>> {
    return Flux.create<Event<Headline>>( { emitter -> // (2)
        val headline = Headline()
        headline.text = "Latest Headline at ${ZonedDateTime.now()}"
        emitter.next(Event.of(headline))
        emitter.complete()
    }, FluxSink.OverflowStrategy.BUFFER)
        .repeat(100) // (3)
        .delayElements(Duration.of(1, ChronoUnit.SECONDS)) // (4)
}
  1. 控制器定義了一個 @Get 注釋,它產(chǎn)生一個 MediaType.TEXT_EVENT_STREAM

  2. 該方法使用 Reactor 發(fā)出標題對象

  3. repeat 方法重復發(fā)射 100 次

  4. 每個之間延遲一秒

請注意,控制器的返回類型也是 Event 并且 Event.of 方法創(chuàng)建事件以流式傳輸?shù)娇蛻舳恕?

要定義使用事件的客戶端,請定義處理 MediaType.TEXT_EVENT_STREAM 的方法:

SSE Client

 Java Groovy  Kotlin 
@Client("/streaming/sse")
public interface HeadlineClient {

    @Get(value = "/headlines", processes = TEXT_EVENT_STREAM)
    Publisher<Event<Headline>> streamHeadlines();
}
@Client("/streaming/sse")
interface HeadlineClient {

    @Get(value = "/headlines", processes = TEXT_EVENT_STREAM)
    Publisher<Event<Headline>> streamHeadlines()
}
@Client("/streaming/sse")
interface HeadlineClient {

    @Get(value = "/headlines", processes = [TEXT_EVENT_STREAM])
    fun streamHeadlines(): Flux<Event<Headline>>
}

Flux 的通用類型可以是事件,在這種情況下您將收到完整的事件對象,也可以是 POJO,在這種情況下您將僅收到從 JSON 轉(zhuǎn)換而來的事件中包含的數(shù)據(jù)。

錯誤響應

如果返回代碼為 400 或更高的 HTTP 響應,則會創(chuàng)建 HttpClientResponseException。異常包含原始響應。如何拋出異常取決于方法返回類型。

  • 對于反應式響應類型,異常作為錯誤通過發(fā)布者傳遞。

  • 對于阻塞響應類型,拋出異常并應由調(diào)用者捕獲和處理。

此規(guī)則的一個例外是 HTTP Not Found (404) 響應。此異常僅適用于聲明式客戶端。

阻止返回類型的 HTTP Not Found (404) 響應不被視為錯誤條件,并且不會拋出客戶端異常。該行為包括返回 void 的方法。

如果方法返回 HttpResponse,則返回原始響應。如果返回類型是 Optional,則返回一個空的可選。對于所有其他類型,返回 null。

自定義請求標頭

自定義請求標頭值得特別提及,因為有多種方法可以實現(xiàn)。

使用配置填充標頭

@Header 注釋可以在類型級別聲明并且是可重復的,這樣就可以使用注釋元數(shù)據(jù)來驅(qū)動通過配置發(fā)送的請求標頭。

以下示例用于說明這一點:

通過配置定義標頭

 Java Groovy  Kotlin 
@Client("/pets")
@Header(name="X-Pet-Client", value="${pet.client.id}")
public interface PetClient extends PetOperations {

    @Override
    @SingleResult
    Publisher<Pet> save(String name, int age);

    @Get("/{name}")
    @SingleResult
    Publisher<Pet> get(String name);
}
@Client("/pets")
@Header(name="X-Pet-Client", value='${pet.client.id}')
interface PetClient extends PetOperations {

    @Override
    @SingleResult
    Publisher<Pet> save(@NotBlank String name, @Min(1L) int age)

    @Get("/{name}")
    @SingleResult
    Publisher<Pet> get(String name)
}
@Client("/pets")
@Header(name = "X-Pet-Client", value = "\${pet.client.id}")
interface PetClient : PetOperations {

    @SingleResult
    override fun save(name: String, age: Int): Publisher<Pet>

    @Get("/{name}")
    @SingleResult
    operator fun get(name: String): Publisher<Pet>
}

上面的示例在 PetClient 接口上定義了一個 @Header 注釋,它使用屬性占位符配置讀取 pet.client.id 屬性。

然后在配置文件(例如 application.yml)中設置以下內(nèi)容以填充值:

Configuring Headers

 Properties Yaml  Toml  Groovy  Hocon  JSON 
pet.client.id=foo
pet:
  client:
    id: foo
[pet]
  [pet.client]
    id="foo"
pet {
  client {
    id = "foo"
  }
}
{
  pet {
    client {
      id = "foo"
    }
  }
}
{
  "pet": {
    "client": {
      "id": "foo"
    }
  }
}

或者,您可以提供 PET_CLIENT_ID 環(huán)境變量,該值將被填充。

使用客戶端過濾器填充標頭

或者,要動態(tài)填充標頭,另一種選擇是使用客戶端過濾器。

自定義 Jackson 設置

如前所述,Jackson 用于將消息編碼為 JSON。默認的 Jackson ObjectMapper 由 Micronaut HTTP 客戶端配置和使用。

您可以使用配置文件(例如 application.yml)中的 JacksonConfiguration 類定義的屬性覆蓋用于構(gòu)造 ObjectMapper 的設置。

例如,以下配置為 Jackson 啟用縮進輸出:

Example Jackson Configuration

 Properties Yaml  Toml  Groovy  Hocon  JSON 
jackson.serialization.indentOutput=true
jackson:
  serialization:
    indentOutput: true
[jackson]
  [jackson.serialization]
    indentOutput=true
jackson {
  serialization {
    indentOutput = true
  }
}
{
  jackson {
    serialization {
      indentOutput = true
    }
  }
}
{
  "jackson": {
    "serialization": {
      "indentOutput": true
    }
  }
}

但是,這些設置適用于全局并影響 HTTP 服務器呈現(xiàn) JSON 的方式以及從 HTTP 客戶端發(fā)送 JSON 的方式。鑒于此,有時提供特定于客戶端的 Jackson 設置很有用。您可以使用客戶端上的 @JacksonFeatures 注釋來執(zhí)行此操作:

例如,以下代碼片段取自 Micronaut 的原生 Eureka 客戶端(當然使用的是 Micronaut 的 HTTP 客戶端):

Example of JacksonFeatures

@Client(id = EurekaClient.SERVICE_ID,
        path = "/eureka",
        configuration = EurekaConfiguration.class)
@JacksonFeatures(
    enabledSerializationFeatures = WRAP_ROOT_VALUE,
    disabledSerializationFeatures = WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED,
    enabledDeserializationFeatures = {UNWRAP_ROOT_VALUE, ACCEPT_SINGLE_VALUE_AS_ARRAY}
)
public interface EurekaClient {
    ...
}

JSON 的 Eureka 序列化格式使用 Jackson 的 WRAP_ROOT_VALUE 序列化特性,因此它只為該客戶端啟用。

如果 JacksonFeatures 提供的定制化還不夠,您還可以為 ObjectMapper 編寫一個 BeanCreatedEventListener 并添加您需要的任何定制化。

重試和斷路器

從故障中恢復對于 HTTP 客戶端至關(guān)重要,這就是 Micronaut 的集成重試建議派上用場的地方。

您可以在任何 @Client 接口上聲明 @Retryable 或 @CircuitBreaker 注釋,并且將應用重試策略,例如:

Declaring @Retryable

 Java Groovy  Kotlin 
@Client("/pets")
@Retryable
public interface PetClient extends PetOperations {

    @Override
    @SingleResult
    Publisher<Pet> save(String name, int age);
}
@Client("/pets")
@Retryable
interface PetClient extends PetOperations {

    @Override
    Mono<Pet> save(String name, int age)
}
@Client("/pets")
@Retryable
interface PetClient : PetOperations {

    override fun save(name: String, age: Int): Mono<Pet>
}

客戶端回退

在分布式系統(tǒng)中,失敗時有發(fā)生,最好做好準備并優(yōu)雅地處理它。

此外,在開發(fā)微服務時,在沒有項目要求可用的其他微服務的情況下處理單個微服務是很常見的。

考慮到這一點,Micronaut 具有一個集成到 Retry Advice 中的本地回退機制,允許在失敗的情況下回退到另一個實現(xiàn)。

使用 @Fallback 注釋,您可以聲明客戶端的回退實現(xiàn),以便在所有可能的重試都用盡時使用。

事實上,該機制并沒有嚴格鏈接到重試;您可以將任何類聲明為@Recoverable,如果方法調(diào)用失敗(或者,在響應類型的情況下,會發(fā)出錯誤),將搜索帶有@Fallback 注釋的類。

為了說明這一點,再次考慮之前聲明的 PetOperations 接口。您可以定義一個 PetFallback 類,在失敗的情況下將被調(diào)用:

Defining a Fallback

 Java Groovy  Kotlin 
@Fallback
public class PetFallback implements PetOperations {
    @Override
    @SingleResult
    public Publisher<Pet> save(String name, int age) {
        Pet pet = new Pet();
        pet.setAge(age);
        pet.setName(name);
        return Mono.just(pet);
    }
}
@Fallback
class PetFallback implements PetOperations {
    @Override
    Mono<Pet> save(String name, int age) {
        Pet pet = new Pet(age: age, name: name)
        return Mono.just(pet)
    }
}
@Fallback
open class PetFallback : PetOperations {
    override fun save(name: String, age: Int): Mono<Pet> {
        val pet = Pet()
        pet.age = age
        pet.name = name
        return Mono.just(pet)
    }
}

如果您只需要回退來幫助測試外部微服務,您可以在 src/test/java 目錄中定義回退,這樣它們就不會包含在生產(chǎn)代碼中。如果您使用不帶 hystrix 的回退,則必須在聲明式客戶端上指定 @Recoverable(api = PetOperations.class)。

如您所見,回退不執(zhí)行任何網(wǎng)絡操作并且非常簡單,因此在外部系統(tǒng)關(guān)閉的情況下將提供成功的結(jié)果。

當然,回退的實際行為取決于您。例如,您可以實施回退,當實際數(shù)據(jù)不可用時從本地緩存中提取數(shù)據(jù),并向操作發(fā)送有關(guān)停機的警報電子郵件或其他通知。

Netflix Hystrix 支持

使用 CLI

如果您使用 Micronaut CLI 創(chuàng)建項目,請?zhí)峁?nbsp;netflix-hystrix 功能以在您的項目中配置 Hystrix:

$ mn create-app my-app --features netflix-hystrix

Netflix Hystrix 是 Netflix 團隊開發(fā)的容錯庫,旨在提高進程間通信的彈性。

Micronaut 通過 netflix-hystrix 模塊與 Hystrix 集成,您可以將其添加到您的構(gòu)建中:

 Gradle Maven 
implementation("io.micronaut.netflix:micronaut-netflix-hystrix")
<dependency>
    <groupId>io.micronaut.netflix</groupId>
    <artifactId>micronaut-netflix-hystrix</artifactId>
</dependency>

使用@HystrixCommand 注解

通過聲明上述依賴關(guān)系,您可以使用 HystrixCommand 注釋來注釋任何方法(包括在 @Client 接口上定義的方法),并且方法的執(zhí)行將被包裝在 Hystrix 命令中。例如:

Using @HystrixCommand

@HystrixCommand
String hello(String name) {
    return "Hello $name"
}

這適用于響應式返回類型,例如 Flux,并且響應式類型將包裝在 HystrixObservableCommand 中。

HystrixCommand 注釋還集成了 Micronaut 對重試建議和回退的支持

有關(guān)如何自定義 Hystrix 線程池、組和屬性的信息,請參閱 HystrixCommand 的 Javadoc。

啟用 Hystrix 流和儀表板

您可以通過在配置文件(例如 application.yml)中將 hystrix.stream.enabled 設置為 true 來啟用服務器發(fā)送事件流以饋送到 Hystrix 儀表板:

Enabling Hystrix Stream

 Properties Yaml  Toml  Groovy  Hocon  JSON 
hystrix.stream.enabled=true
hystrix:
  stream:
    enabled: true
[hystrix]
  [hystrix.stream]
    enabled=true
hystrix {
  stream {
    enabled = true
  }
}
{
  hystrix {
    stream {
      enabled = true
    }
  }
}
{
  "hystrix": {
    "stream": {
      "enabled": true
    }
  }
}

這會暴露一個 /hystrix.stream 端點,其格式為 Hystrix 儀表板所期望的格式。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號