阅读(45) (0)

Micronaut 使用 @Client 的声明式 HTTP 客户端

2023-02-23 13:37:08 更新

现在您已经了解了底层 HTTP 客户端的工作原理,让我们来看看 Micronaut 通过 Client 注解对声明式客户端的支持。

从本质上讲,@Client注解可以声明在任何接口或抽象类上,通过使用Introduction Advice在编译时为你实现抽象方法,大大简化了HTTP客户端的创建。

让我们从一个简单的例子开始。给定以下课程:

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 约束来验证参数。

请注意,某些注释(例如 Produces 和 Consumes)在服务器端和客户端用法之间具有不同的语义。例如,控制器方法(服务器端)上的@Produces 指示方法的返回值如何格式化,而客户端上的@Produces 指示方法的参数在发送到服务器时如何格式化。虽然这看起来有点令人困惑,但考虑到服务器生产/消费与客户端之间的不同语义是合乎逻辑的:服务器使用参数并将响应返回给客户端,而客户端使用参数并将输出发送到服务器.

此外,要使用 javax.validation 功能,请将验证模块添加到您的构建中:

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

在 Micronaut 的服务器端,您可以实现 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 在编译时自动实现客户端:

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 不会在字节码中保留参数名称,否则。如果您使用 -parameters 进行编译,则不需要覆盖。此外,当覆盖方法时,您应该确保再次声明任何验证注释,因为这些不是继承注释。

一旦你定义了一个客户端,你可以在任何你需要的地方@Inject它。

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

  • 绝对 URI,例如https://api.twitter.com/1.1

  • 相对 URI,在这种情况下目标服务器是当前服务器(对测试有用)

  • 服务标识符。

在生产中,您通常使用服务 ID 和服务发现来自动发现服务。

关于上面示例中的 save 方法,另一个需要注意的重要事项是它返回 Single 类型。

这是一种非阻塞反应类型——通常您希望您的 HTTP 客户端不阻塞。在某些情况下,您可能需要一个确实阻塞的 HTTP 客户端(例如在单元测试中),但这种情况很少见。

下表说明了可用于@Client 的常见返回类型:

表 1. Micronaut 响应类型
类型 描述 示例签名

Publisher

任何实现 Publisher 接口的类型

Flux<String> hello()

HttpResponse

HttpResponse 和可选的响应主体类型

Mono<HttpResponse<String>> hello()

Publisher

发出 POJO 的 Publisher 实现

Mono<Book> hello()

CompletableFuture

Java CompletableFuture 实例

CompletableFuture<String> hello()

CharSequence

阻塞本机类型。比如String

String hello()

T

任何简单的 POJO 类型。

Book show()

通常,任何可以转换为 Publisher 接口的反应类型都支持作为返回类型,包括(但不限于)RxJava 1.x、RxJava 2.x 和 Reactor 3.x 定义的反应类型。

还支持返回 CompletableFuture 实例。请注意,返回任何其他类型都会导致阻塞请求,除非用于测试,否则不推荐使用。

自定义参数绑定

前面的示例展示了一个使用方法参数表示 POST 请求正文的简单示例:

PetOperations.java

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

默认情况下,save 方法使用以下 JSON 执行 HTTP POST:

Example Produced JSON

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

但是,您可能想要自定义作为正文、参数、URI 变量等发送的内容。@Client 注释在这方面非常灵活,并且支持与 Micronaut 的 HTTP 服务器相同的 io.micronaut.http.annotation。

例如,下面定义了一个 URI 模板,name 参数用作 URI 模板的一部分,而@Body 声明要发送到服务器的内容由 Pet POJO 表示:

PetOperations.java

@Post("/{name}")
Mono<Pet> save(
    @NotBlank String name, (1)
    @Body @Valid Pet pet) (2)
  1. name 参数,包含在 URI 中,并声明为 @NotBlank

  2. pet参数,用于对body进行编码,声明为@Valid

下表总结了参数注释及其用途,并提供了示例:

表 1. 参数绑定注解
注解 描述 示例

@Body

指定请求正文的参数

@Body String body

@CookieValue

指定要作为 cookie 发送的参数

@CookieValue String myCookie

@Header

指定要作为 HTTP 标头发送的参数

@Header String requestId

@QueryValue

自定义要绑定的 URI 参数的名称

@QueryValue("userAge") Integer age

@PathVariable

专门从路径变量绑定参数。

@PathVariable Long id

@RequestAttribute

指定要设置为请求属性的参数

@RequestAttribute Integer locationId

始终使用 @Produces 或 @Consumes 而不是为 Content-Type 或 Accept 提供标头。

查询值格式化

Format 注释可以与@QueryValue 注释一起使用来格式化查询值。

支持的值为:“csv”、“ssv”、“pipes”、“multi”和“deep-object”,其含义类似于 Open API v3 查询参数的 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"

基于类型的绑定参数

一些参数通过它们的类型而不是它们的注释来识别。下表总结了这些参数类型及其用途,并提供了一个示例:

类型 描述 示例

BasicAuth

设置授权标头

BasicAuth basicAuth

HttpHeaders

向请求添加多个标头

HttpHeaders headers

Cookies

向请求添加多个 cookie

Cookies cookies

Cookie

向请求添加 cookie

Cookie cookie

Locale

设置接受语言标头。使用 @QueryValue 或 @PathVariable 注释以填充 URI 变量。

Locale locale

自定义绑定

ClientArgumentRequestBinder API 将客户端参数绑定到请求。在绑定过程中自动使用注册为 beans 的自定义绑定器类。首先搜索基于注释的绑定器,如果未找到绑定器,则搜索基于类型的绑定器。

通过注解绑定

要根据参数上的注释控制参数如何绑定到请求,请创建类型为 AnnotatedClientArgumentRequestBinder 的 bean。任何自定义注解都必须使用@Bindable 进行注解。

在此示例中,请参阅以下客户端:

带有@Metadata 参数的客户端

 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
}

该参数使用以下注释进行注释:

@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

在没有任何额外代码的情况下,客户端尝试将元数据转换为字符串并将其附加为查询参数。在这种情况下,这不是预期的效果,因此需要自定义活页夹。

以下活页夹处理传递给带有 @Metadata 注释的客户端的参数,并改变请求以包含所需的标头。可以修改实现以接受除 Map 之外的更多类型的数据。

注释参数绑定器

 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())
            }
        }
    }
}

按类型绑定

要根据参数类型绑定到请求,请创建类型为 TypedClientArgumentRequestBinder 的 bean。

在此示例中,请参阅以下客户端:

具有元数据参数的客户端

 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?
}

在没有任何额外代码的情况下,客户端尝试将元数据转换为字符串并将其附加为查询参数。在这种情况下,这不是预期的效果,因此需要自定义活页夹。

以下活页夹处理传递给元数据类型客户端的参数,并改变请求以包含所需的标头。

类型化参数绑定器

 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())
    }
}

绑定方法

也可以创建一个活页夹,它将通过方法上的注释更改请求。例如:

带有注释方法的客户端

 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. 它实现了 AnnotatedClientRequestBinder<NameAuthorization>

  3. 自定义bind方法用于实现binder的行为

使用@Client 进行流式传输

@Client 注释还可以处理流式 HTTP 响应。

使用@Client 流式传输 JSON

例如,要编写一个从文档的 JSON Streaming 部分中定义的控制器流式传输数据的客户端,您可以定义一个返回未绑定发布者的客户端,例如 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

以下示例显示了如何从测试中调用先前定义的 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 发出的第一个元素发射到 Mono 中。

  3. block() 方法检索测试中的结果。

流媒体客户端和响应类型

上一节中定义的示例期望服务器响应 JSON 对象流,内容类型为 application/x-json-stream。例如:

A JSON Stream

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

这样做的原因是简单的;事实上,一系列 JSON 对象不是有效的 JSON,因此响应内容类型不能是 application/json。为了使 JSON 有效,它必须返回一个数组:

A JSON Array

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

然而,Micronaut 的客户端确实支持通过 application/x-json-stream 流式传输单个 JSON 对象以及使用 application/json 定义的 JSON 数组。

如果服务器返回 application/json 并返回一个非单个 Publisher(例如 Reactor 的 Flux 或 RxJava 的 Flowable),则客户端会在数组元素可用时对其进行流式传输。

流式传输客户端和读取超时

当流式传输来自服务器的响应时,底层 HTTP 客户端不会应用 HttpClientConfiguration 的默认 readTimeout 设置(默认为 10 秒),因为流式响应的读取之间的延迟可能与正常读取不同。

相反,read-idle-timeout 设置(默认为 5 分钟)指示连接变为空闲后何时关闭连接。

如果您从定义项目之间延迟超过 5 分钟的服务器流式传输数据,则应调整 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 设置为十分钟。

流媒体服务器发送的事件

Micronaut 具有由 SseClient 接口定义的服务器发送事件 (SSE) 的本机客户端。

您可以使用此客户端从任何发出它们的服务器流式传输 SSE 事件。

虽然 SSE 流通常由浏览器 EventSource 使用,但在某些情况下您可能希望通过 SseClient 使用 SSE 流,例如在单元测试中或当 Micronaut 服务充当另一项服务的网关时。

@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 注释,它产生一个 MediaType.TEXT_EVENT_STREAM

  2. 该方法使用 Reactor 发出标题对象

  3. repeat 方法重复发射 100 次

  4. 每个之间延迟一秒

请注意,控制器的返回类型也是 Event 并且 Event.of 方法创建事件以流式传输到客户端。

要定义使用事件的客户端,请定义处理 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 转换而来的事件中包含的数据。

错误响应

如果返回代码为 400 或更高的 HTTP 响应,则会创建 HttpClientResponseException。异常包含原始响应。如何抛出异常取决于方法返回类型。

  • 对于反应式响应类型,异常作为错误通过发布者传递。

  • 对于阻塞响应类型,抛出异常并应由调用者捕获和处理。

此规则的一个例外是 HTTP Not Found (404) 响应。此异常仅适用于声明式客户端。

阻止返回类型的 HTTP Not Found (404) 响应不被视为错误条件,并且不会抛出客户端异常。该行为包括返回 void 的方法。

如果方法返回 HttpResponse,则返回原始响应。如果返回类型是 Optional,则返回一个空的可选。对于所有其他类型,返回 null。

自定义请求标头

自定义请求标头值得特别提及,因为有多种方法可以实现。

使用配置填充标头

@Header 注释可以在类型级别声明并且是可重复的,这样就可以使用注释元数据来驱动通过配置发送的请求标头。

以下示例用于说明这一点:

通过配置定义标头

 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)中设置以下内容以填充值:

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 环境变量,该值将被填充。

使用客户端过滤器填充标头

或者,要动态填充标头,另一种选择是使用客户端过滤器。

自定义 Jackson 设置

如前所述,Jackson 用于将消息编码为 JSON。默认的 Jackson ObjectMapper 由 Micronaut HTTP 客户端配置和使用。

您可以使用配置文件(例如 application.yml)中的 JacksonConfiguration 类定义的属性覆盖用于构造 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 服务器呈现 JSON 的方式以及从 HTTP 客户端发送 JSON 的方式。鉴于此,有时提供特定于客户端的 Jackson 设置很有用。您可以使用客户端上的 @JacksonFeatures 注释来执行此操作:

例如,以下代码片段取自 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 客户端至关重要,这就是 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>
}

客户端回退

在分布式系统中,失败时有发生,最好做好准备并优雅地处理它。

此外,在开发微服务时,在没有项目要求可用的其他微服务的情况下处理单个微服务是很常见的。

考虑到这一点,Micronaut 具有一个集成到 Retry Advice 中的本地回退机制,允许在失败的情况下回退到另一个实现。

使用 @Fallback 注释,您可以声明客户端的回退实现,以便在所有可能的重试都用尽时使用。

事实上,该机制并没有严格链接到重试;您可以将任何类声明为@Recoverable,如果方法调用失败(或者,在响应类型的情况下,会发出错误),将搜索带有@Fallback 注释的类。

为了说明这一点,再次考虑之前声明的 PetOperations 接口。您可以定义一个 PetFallback 类,在失败的情况下将被调用:

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 目录中定义回退,这样它们就不会包含在生产代码中。如果您使用不带 hystrix 的回退,则必须在声明式客户端上指定 @Recoverable(api = PetOperations.class)。

如您所见,回退不执行任何网络操作并且非常简单,因此在外部系统关闭的情况下将提供成功的结果。

当然,回退的实际行为取决于您。例如,您可以实施回退,当实际数据不可用时从本地缓存中提取数据,并向操作发送有关停机的警报电子邮件或其他通知。

Netflix Hystrix 支持

使用 CLI

如果您使用 Micronaut CLI 创建项目,请提供 netflix-hystrix 功能以在您的项目中配置 Hystrix:

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

Netflix Hystrix 是 Netflix 团队开发的容错库,旨在提高进程间通信的弹性。

Micronaut 通过 netflix-hystrix 模块与 Hystrix 集成,您可以将其添加到您的构建中:

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

使用@HystrixCommand 注解

通过声明上述依赖关系,您可以使用 HystrixCommand 注释来注释任何方法(包括在 @Client 接口上定义的方法),并且方法的执行将被包装在 Hystrix 命令中。例如:

Using @HystrixCommand

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

这适用于响应式返回类型,例如 Flux,并且响应式类型将包装在 HystrixObservableCommand 中。

HystrixCommand 注释还集成了 Micronaut 对重试建议和回退的支持

有关如何自定义 Hystrix 线程池、组和属性的信息,请参阅 HystrixCommand 的 Javadoc。

启用 Hystrix 流和仪表板

您可以通过在配置文件(例如 application.yml)中将 hystrix.stream.enabled 设置为 true 来启用服务器发送事件流以馈送到 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 仪表板所期望的格式。