编辑此页面

使用 Reactive Routes

响应式路由提供了一种替代方法来实现 HTTP 端点,您可以在其中声明和链接 *路由*。这种方法在 JavaScript 世界中非常受欢迎,例如 Express.Js 或 Hapi 等框架。Quarkus 也提供了使用响应式路由的可能性。您可以使用路由来实现 REST API,或者将它们与 Jakarta REST 资源和 Servlet 结合使用。

本指南中提供的代码可在此 GitHub 存储库reactive-routes-quickstart 目录下找到。

响应式路由最初是为了在 Quarkus 响应式架构之上为 HTTP API 提供响应式执行模型而引入的。随着 Quarkus REST (以前称为 RESTEasy Reactive) 的推出,您现在可以实现响应式 HTTP API 并继续使用 Jakarta REST 注解。响应式路由仍然受支持,特别是如果您想要一种更*基于路由*的方法,并且更接近底层响应式引擎。

Quarkus HTTP

在深入探讨之前,让我们先了解一下 Quarkus 的 HTTP 层。Quarkus HTTP 支持基于无阻塞和响应式引擎(Eclipse Vert.x 和 Netty)。您的应用程序接收的所有 HTTP 请求都由*事件循环*(I/O 线程)处理,然后路由到管理请求的代码。根据目标,它可以调用工作线程上的代码(Servlet、Jax-RS)来管理请求,或使用 I/O 线程(响应式路由)。请注意,因此,响应式路由必须是非阻塞的,或者显式声明其阻塞性质(这将导致它在工作线程上被调用)。

Quarkus HTTP Architecture

有关此主题的更多详细信息,请参阅 Quarkus 响应式架构文档

声明响应式路由

使用响应式路由的第一种方法是使用 @Route 注解。要访问此注解,您需要通过运行以下命令添加 quarkus-reactive-routes 扩展:

CLI
quarkus extension add quarkus-reactive-routes
Maven
./mvnw quarkus:add-extension -Dextensions='quarkus-reactive-routes'
Gradle
./gradlew addExtension --extensions='quarkus-reactive-routes'

这会将以下内容添加到您的构建文件中

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-reactive-routes</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-reactive-routes")

然后,在* bean *中,您可以使用 @Route 注解,如下所示:

package org.acme.reactive.routes;

import io.quarkus.vertx.web.Route;
import io.quarkus.vertx.web.Route.HttpMethod;
import io.quarkus.vertx.web.RoutingExchange;
import io.vertx.ext.web.RoutingContext;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped (1)
public class MyDeclarativeRoutes {

    // neither path nor regex is set - match a path derived from the method name
    @Route(methods = Route.HttpMethod.GET) (2)
    void hello(RoutingContext rc) { (3)
        rc.response().end("hello");
    }

    @Route(path = "/world")
    String helloWorld() { (4)
        return "Hello world!";
    }

    @Route(path = "/greetings", methods = Route.HttpMethod.GET)
    void greetingsQueryParam(RoutingExchange ex) { (5)
        ex.ok("hello " + ex.getParam("name").orElse("world")); (6)
    }

    @Route(path = "/greetings/:name", methods = Route.HttpMethod.GET) (7)
    void greetingsPathParam(@Param String name, RoutingExchange ex) {
        ex.ok("hello " + name);
    }
}
1 如果一个类上找到了响应式路由,而该类没有作用域注解,则会自动添加 @jakarta.inject.Singleton
2 @Route 注解表示该方法是一个响应式路由。同样,默认情况下,方法中包含的代码不得阻塞。
3 该方法将 RoutingContext 作为参数。您可以从 RoutingContext 中检索 HTTP 请求(使用 request())并使用 response().end(…​) 写入响应。
4 如果注解的方法不返回 void,则参数是可选的。
5 RoutingExchangeRoutingContext 的便捷包装器,它提供了一些有用的方法。
6 RoutingExchange 用于检索请求查询参数 name
7 路径定义了一个参数 name,可以使用 @Param 注解将其注入到方法参数中。

有关使用 RoutingContext 的更多详细信息,请参阅 Vert.x Web 文档

@Route 注解允许您配置:

  • path - 用于按路径路由,使用 Vert.x Web 格式

  • regex - 用于使用正则表达式路由,请参阅 以获取更多详细信息

  • methods - 触发路由的 HTTP 动词,例如 GETPOST……

  • type - 它可以是* normal*(非阻塞)、* blocking*(方法分派到工作线程)或 *failure*,表示此路由在发生故障时被调用。

  • order - 当多个路由参与处理传入请求时,路由的顺序。对于常规用户路由,必须为正数。

  • 使用 producesconsumes 指定生成的和消耗的 MIME 类型。

例如,您可以声明一个阻塞路由,如下所示:

@Route(methods = HttpMethod.POST, path = "/post", type = Route.HandlerType.BLOCKING)
public void blocking(RoutingContext rc) {
    // ...
}

或者,您可以使用 @io.smallrye.common.annotation.Blocking 并省略 type = Route.HandlerType.BLOCKING

@Route(methods = HttpMethod.POST, path = "/post")
@Blocking
public void blocking(RoutingContext rc) {
    // ...
}

当使用 @Blocking 时,@Routetype 属性将被忽略。

@Route 注解是可重复的,因此您可以为单个方法声明多个路由。

@Route(path = "/first") (1)
@Route(path = "/second")
public void route(RoutingContext rc) {
    // ...
}
1 每个路由可以使用不同的路径、方法……

如果未设置 content-type 标头,我们将尝试使用最可接受的内容类型,将 accept 标头与 Route 的 produces 属性值进行匹配,如 io.vertx.ext.web.RoutingContext.getAcceptableContentType() 所定义的。

@Route(path = "/person", produces = "text/html") (1)
String person() {
    // ...
}
1 如果 accept 标头匹配 text/html,我们将自动将内容类型设置为 text/html

在虚拟线程上执行路由

您可以使用 @io.smallrye.common.annotation.RunOnVirtualThread 注解一个路由方法,以便在虚拟线程上执行它。但是,请记住,并非所有内容都可以安全地在虚拟线程上运行。您应该仔细阅读 虚拟线程支持参考,并熟悉所有详细信息。

处理冲突路由

您可能会遇到多个路由匹配给定路径的情况。在以下示例中,两个路由都匹配 /accounts/me

@Route(path = "/accounts/:id", methods = HttpMethod.GET)
void getAccount(RoutingContext rc) {
  ...
}

@Route(path = "/accounts/me", methods = HttpMethod.GET)
void getCurrentUserAccount(RoutingContext rc) {
  ...
}

因此,结果不是预期的,因为第一个路由被调用时,路径参数 id 设置为 me。为了避免冲突,请使用 order 属性。

@Route(path = "/accounts/:id", methods = HttpMethod.GET, order = 2)
void getAccount(RoutingContext rc) {
  ...
}

@Route(path = "/accounts/me", methods = HttpMethod.GET, order = 1)
void getCurrentUserAccount(RoutingContext rc) {
  ...
}

通过为第二个路由设置较低的顺序,它会先被评估。如果请求路径匹配,则调用它,否则会评估其他路由。

@RouteBase

此注解可用于为类上声明的响应式路由配置一些默认值。

@RouteBase(path = "simple", produces = "text/plain") (1) (2)
public class SimpleRoutes {

    @Route(path = "ping") // the final path is /simple/ping
    void ping(RoutingContext rc) {
        rc.response().end("pong");
    }
}
1 path 值用作类上声明的任何路由方法的缀,其中使用了 Route#path()
2 produces() 的值用于所有 Route#produces() 为空的所有路由的内容路由。

响应式路由方法

路由方法必须是 CDI bean 的非私有非静态方法。如果注解的方法返回 void,则它必须至少接受一个参数 - 请参阅下面的支持的类型。如果注解的方法不返回 void,则参数是可选的。

返回 void 的方法必须*结束*响应,否则到此路由的 HTTP 请求将永远不会结束。RoutingExchange 的某些方法会为您完成此操作,而另一些则不会,您必须自己调用响应的 end() 方法,请参阅其 JavaDoc 以获取更多信息。

路由方法可以接受以下类型的参数:

  • io.vertx.ext.web.RoutingContext

  • io.quarkus.vertx.web.RoutingExchange

  • io.vertx.core.http.HttpServerRequest

  • io.vertx.core.http.HttpServerResponse

  • io.vertx.mutiny.core.http.HttpServerRequest

  • io.vertx.mutiny.core.http.HttpServerResponse

此外,还可以使用以下类型将 HttpServerRequest 参数注入到使用 @io.quarkus.vertx.web.Param 注解的方法参数中:

参数类型 通过...获得

java.lang.String

routingContext.request().getParam()

java.util.Optional<String>

routingContext.request().getParam()

java.util.List<String>

routingContext.request().params().getAll()

请求参数示例
@Route
String hello(@Param Optional<String> name) {
   return "Hello " + name.orElse("world");
}

可以使用以下类型将 HttpServerRequest 标头注入到使用 @io.quarkus.vertx.web.Header 注解的方法参数中:

参数类型 通过...获得

java.lang.String

routingContext.request().getHeader()

java.util.Optional<String>

routingContext.request().getHeader()

java.util.List<String>

routingContext.request().headers().getAll()

请求头示例
@Route
String helloFromHeader(@Header("My-Header") String header) {
   return header;
}

可以使用以下类型将请求体注入到使用 @io.quarkus.vertx.web.Body 注解的方法参数中:

参数类型 通过...获得

java.lang.String

routingContext.getBodyAsString()

io.vertx.core.buffer.Buffer

routingContext.getBody()

io.vertx.core.json.JsonObject

routingContext.getBodyAsJson()

io.vertx.core.json.JsonArray

routingContext.getBodyAsJsonArray()

任何其他类型

routingContext.getBodyAsJson().mapTo(MyPojo.class)

请求体示例
@Route(produces = "application/json")
Person createPerson(@Body Person person, @Param("id") Optional<String> primaryKey) {
  person.setId(primaryKey.map(Integer::valueOf).orElse(42));
  return person;
}

故障处理程序可以声明一个参数类型扩展 Throwable 的单个方法参数。参数类型用于匹配 RoutingContext#failure() 的结果。

故障处理程序示例
@Route(type = HandlerType.FAILURE)
void unsupported(UnsupportedOperationException e, HttpServerResponse response) {
  response.setStatusCode(501).end(e.getMessage());
}

返回 Uni

在响应式路由中,您可以直接返回 Uni

@Route(path = "/hello")
Uni<String> hello() {
    return Uni.createFrom().item("Hello world!");
}

@Route(path = "/person")
Uni<Person> getPerson() {
    return Uni.createFrom().item(() -> new Person("neo", 12345));
}

使用响应式客户端时,返回 Uni 很方便。

@Route(path = "/mail")
Uni<Void> sendEmail() {
    return mailer.send(...);
}

返回的 Uni 生成的项可以是:

  • 一个字符串 - 直接写入 HTTP 响应。

  • 一个 io.vertx.core.buffer.Buffer - 直接写入 HTTP 响应。

  • 一个对象 - 在被编码为 JSON 后写入 HTTP 响应。如果尚未设置,则 content-type 标头将设置为 application/json

如果返回的 Uni 生成了故障(或为 null),则会写入 HTTP 500 响应。

返回 Uni<Void> 会生成 204 响应(无内容)。

返回结果

您也可以直接返回结果。

@Route(path = "/hello")
String helloSync() {
    return "Hello world";
}

请注意,处理必须是*非阻塞*的,因为响应式路由是在 IO 线程上调用的。否则,请将 @Route 注解的 type 属性设置为 Route.HandlerType.BLOCKING,或使用 @io.smallrye.common.annotation.Blocking 注解。

该方法可以返回:

  • 一个字符串 - 直接写入 HTTP 响应。

  • 一个 io.vertx.core.buffer.Buffer - 直接写入 HTTP 响应。

  • 一个对象 - 在被编码为 JSON 后写入 HTTP 响应。如果尚未设置,则 content-type 标头将设置为 application/json

返回 Multi

响应式路由可以返回 Multi。各项逐一写入响应。响应的 Transfer-Encoding 标头设置为 chunked

@Route(path = "/hello")
Multi<String> hellos() {
    return Multi.createFrom().items("hello", "world", "!");  (1)
}
1 生成 helloworld!

该方法可以返回:

  • 一个 Multi<String> - 各项逐一(每个*块*一个)写入响应。

  • 一个 Multi<Buffer> - 缓冲区逐一(每个*块*一个)写入,不进行任何处理。

  • 一个 Multi<Object> - 各项被编码为 JSON,逐一写入响应。

@Route(path = "/people")
Multi<Person> people() {
    return Multi.createFrom().items(
            new Person("superman", 1),
            new Person("batman", 2),
            new Person("spiderman", 3));
}

前面的代码片段生成:

{"name":"superman", "id": 1} // chunk 1
{"name":"batman", "id": 2} // chunk 2
{"name":"spiderman", "id": 3} // chunk 3

流式 JSON 数组项

您可以返回一个 Multi 来生成一个 JSON 数组,其中每个项都来自该数组。响应项逐项写入客户端。为此,请将 produces 属性设置为 "application/json"(或 ReactiveRoutes.APPLICATION_JSON)。

@Route(path = "/people", produces = ReactiveRoutes.APPLICATION_JSON)
Multi<Person> people() {
    return Multi.createFrom().items(
            new Person("superman", 1),
            new Person("batman", 2),
            new Person("spiderman", 3));
}

前面的代码片段生成:

[
  {"name":"superman", "id": 1} // chunk 1
  ,{"name":"batman", "id": 2} // chunk 2
  ,{"name":"spiderman", "id": 3} // chunk 3
]
produces 属性是一个数组。当您传递单个值时,可以省略“{”和“}”。请注意,"application/json" 必须是数组中的第一个值。

只有 Multi<String>Multi<Object>Multi<Void> 可以写入 JSON 数组。使用 Multi<Void> 会生成一个空数组。您不能使用 Multi<Buffer>。如果需要使用 Buffer,请先将其内容转换为 JSON 或字符串表示形式。

asJsonArray 的弃用

ReactiveRoutes.asJsonArray 已被弃用,因为它与 Quarkus 的安全层不兼容。

事件流和服务器发送事件支持

您可以返回一个 Multi 来生成一个事件源(服务器发送事件流)。要启用此功能,请将 produces 属性设置为 "text/event-stream"(或 ReactiveRoutes.EVENT_STREAM),如下所示:

@Route(path = "/people", produces = ReactiveRoutes.EVENT_STREAM)
Multi<Person> people() {
    return Multi.createFrom().items(
            new Person("superman", 1),
            new Person("batman", 2),
            new Person("spiderman", 3));
}

此方法将生成:

data: {"name":"superman", "id": 1}
id: 0

data: {"name":"batman", "id": 2}
id: 1

data: {"name":"spiderman", "id": 3}
id: 2
produces 属性是一个数组。当您传递单个值时,可以省略“{”和“}”。请注意,"text/event-stream" 必须是数组中的第一个值。

您还可以实现 io.quarkus.vertx.web.ReactiveRoutes.ServerSentEvent 接口来定制服务器发送事件的 eventid 部分。

class PersonEvent implements ReactiveRoutes.ServerSentEvent<Person> {
    public String name;
    public int id;

    public PersonEvent(String name, int id) {
        this.name = name;
        this.id = id;
    }

    @Override
    public Person data() {
        return new Person(name, id); // Will be JSON encoded
    }

    @Override
    public long id() {
        return id;
    }

    @Override
    public String event() {
        return "person";
    }
}

使用 Multi<PersonEvent> 将生成:

event: person
data: {"name":"superman", "id": 1}
id: 1

event: person
data: {"name":"batman", "id": 2}
id: 2

event: person
data: {"name":"spiderman", "id": 3}
id: 3
asEventStream 的弃用

ReactiveRoutes.asEventStream 已被弃用,因为它与 Quarkus 的安全层不兼容。

NDJSON 格式的 JSON 流

您可以返回一个 Multi 来生成一个换行符分隔的 JSON 值流。要启用此功能,请将 @Route 注解的 produces 属性设置为 "application/x-ndjson"(或 ReactiveRoutes.ND_JSON)。

@Route(path = "/people", produces = ReactiveRoutes.ND_JSON)
Multi<Person> people() {
    return ReactiveRoutes.asJsonStream(Multi.createFrom().items(
            new Person("superman", 1),
            new Person("batman", 2),
            new Person("spiderman", 3)
            ));
}

此方法将生成:

{"name":"superman", "id": 1}
{"name":"batman", "id": 2}
{"name":"spiderman", "id": 3}
produces 属性是一个数组。当您传递单个值时,可以省略“{”和“}”。请注意,"application/x-ndjson" 必须是数组中的第一个值。

您也可以提供字符串而不是对象,在这种情况下,字符串将被包装在引号中以成为有效的 JSON 值。

@Route(path = "/people", produces = ReactiveRoutes.ND_JSON)
Multi<Person> people() {
    return ReactiveRoutes.asJsonStream(Multi.createFrom().items(
            "superman",
            "batman",
            "spiderman"
            ));
}
"superman"
"batman"
"spiderman"
asJsonStream 的弃用

ReactiveRoutes.asJsonStream 已被弃用,因为它与 Quarkus 的安全层不兼容。

使用 Bean Validation

您可以结合响应式路由和 Bean Validation。首先,请记住将 quarkus-hibernate-validator 扩展添加到您的项目中。然后,您可以向您的路由参数(用 @Param@Body 注解)添加约束。

@Route(produces = "application/json")
Person createPerson(@Body @Valid Person person, @NonNull @Param("id") String primaryKey) {
  // ...
}

如果参数未能通过测试,则返回 HTTP 400 响应。如果请求接受 JSON 有效负载,则响应遵循 Problem 格式。

当返回对象或 Uni 时,您还可以使用 @Valid 注解。

@Route(...)
@Valid Uni<Person> createPerson(@Body @Valid Person person, @NonNull @Param("id") String primaryKey) {
  // ...
}

如果路由生成的项未能通过验证,则返回 HTTP 500 响应。如果请求接受 JSON 有效负载,则响应遵循 Problem 格式。

请注意,只有 @Valid 支持返回值类型。返回的类可以使用任何约束。对于 Uni,它会异步检查生成的项。

使用 Vert.x Web Router

您还可以通过直接在 Router 对象上注册路由来将您的路由注册到* HTTP 路由层*。要在启动时检索 Router 实例:

public void init(@Observes Router router) {
    router.get("/my-route").handler(rc -> rc.response().end("Hello from my route"));
}

请参阅 Vert.x Web 文档,了解路由注册、选项和可用处理程序。

Router 访问由 quarkus-vertx-http 扩展提供。如果您使用 quarkus-restquarkus-reactive-routes,该扩展将自动添加。

您还可以接收 Router 的 Mutiny 变体(io.vertx.mutiny.ext.web.Router)。

public void init(@Observes io.vertx.mutiny.ext.web.Router router) {
    router.get("/my-route").handler(rc -> rc.response().endAndForget("Hello from my route"));
}

拦截 HTTP 请求

您还可以注册拦截传入 HTTP 请求的过滤器。请注意,这些过滤器也适用于 Servlet、Jakarta REST 资源和响应式路由。

例如,以下代码片段注册了一个添加 HTTP 标头的过滤器:

package org.acme.reactive.routes;

import io.vertx.ext.web.RoutingContext;

public class MyFilters {

    @RouteFilter(100) (1)
    void myFilter(RoutingContext rc) {
       rc.response().putHeader("X-Header", "intercepting the request");
       rc.next(); (2)
    }
}
1 RouteFilter#value() 定义了用于对过滤器进行排序的优先级 - 优先级较高的过滤器会先被调用。
2 过滤器很可能需要调用 next() 方法以继续链式调用。

HTTP 压缩

HTTP 响应正文默认不压缩。您可以通过 quarkus.http.enable-compression=true 来启用 HTTP 压缩支持。

如果启用了压缩支持,则当以下情况发生时,响应正文将被压缩:

  • 路由方法被 @io.quarkus.vertx.http.Compressed 注解,或者

  • 设置了 Content-Type 标头,并且该值是根据 quarkus.http.compress-media-types 配置的压缩媒体类型。

当以下情况发生时,响应正文永远不会被压缩:

  • 路由方法被 @io.quarkus.vertx.http.Uncompressed 注解,或者

  • 未设置 Content-Type 标头。

默认情况下,以下媒体类型会被压缩:text/htmltext/plaintext/xmltext/csstext/javascriptapplication/javascriptapplication/jsonapplication/graphql+jsonapplication/xhtml+xml
如果客户端不支持 HTTP 压缩,则响应正文不会被压缩。

添加 OpenAPI 和 Swagger UI

您可以使用 quarkus-smallrye-openapi 扩展来添加对 OpenAPISwagger UI 的支持。

通过运行以下命令添加扩展:

CLI
quarkus extension add quarkus-smallrye-openapi
Maven
./mvnw quarkus:add-extension -Dextensions='quarkus-smallrye-openapi'
Gradle
./gradlew addExtension --extensions='quarkus-smallrye-openapi'

这会将以下内容添加到您的构建文件中

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-smallrye-openapi")

这足以从您的 Vert.x 路由生成基本的 OpenAPI 模式文档。

curl https://:8080/q/openapi

您将看到生成的 OpenAPI 模式文档:

---
openapi: 3.0.3
info:
  title: Generated API
  version: "1.0"
paths:
  /greetings:
    get:
      responses:
        "204":
          description: No Content
  /hello:
    get:
      responses:
        "204":
          description: No Content
  /world:
    get:
      responses:
        "200":
          description: OK
          content:
            '*/*':
              schema:
                type: string

另请参阅 OpenAPI 指南

添加 MicroProfile OpenAPI 注解

您可以使用 MicroProfile OpenAPI 来更好地记录您的模式,例如,添加标头信息或指定 void 方法的返回类型可能会很有用。

@OpenAPIDefinition( (1)
    info = @Info(
        title="Greeting API",
        version = "1.0.1",
        contact = @Contact(
            name = "Greeting API Support",
            url = "http://exampleurl.com/contact",
            email = "techsupport@example.com"),
        license = @License(
            name = "Apache 2.0",
            url = "https://apache.ac.cn/licenses/LICENSE-2.0.html"))
)
@ApplicationScoped
public class MyDeclarativeRoutes {

    // neither path nor regex is set - match a path derived from the method name
    @Route(methods = Route.HttpMethod.GET)
    @APIResponse(responseCode="200",
            description="Say hello",
            content=@Content(mediaType="application/json", schema=@Schema(type=SchemaType.STRING))) (2)
    void hello(RoutingContext rc) {
        rc.response().end("hello");
    }

    @Route(path = "/world")
    String helloWorld() {
        return "Hello world!";
    }

    @Route(path = "/greetings", methods = HttpMethod.GET)
    @APIResponse(responseCode="200",
            description="Greeting",
            content=@Content(mediaType="application/json", schema=@Schema(type=SchemaType.STRING)))
    void greetings(RoutingExchange ex) {
        ex.ok("hello " + ex.getParam("name").orElse("world"));
    }
}
1 关于您 API 的标头信息。
2 定义响应。

这将生成此 OpenAPI 模式:

---
openapi: 3.0.3
info:
  title: Greeting API
  contact:
    name: Greeting API Support
    url: http://exampleurl.com/contact
    email: techsupport@example.com
  license:
    name: Apache 2.0
    url: https://apache.ac.cn/licenses/LICENSE-2.0.html
  version: 1.0.1
paths:
  /greetings:
    get:
      responses:
        "200":
          description: Greeting
          content:
            application/json:
              schema:
                type: string
  /hello:
    get:
      responses:
        "200":
          description: Say hello
          content:
            application/json:
              schema:
                type: string
  /world:
    get:
      responses:
        "200":
          description: OK
          content:
            '*/*':
              schema:
                type: string

使用 Swagger UI

Swagger UI 在 devtest 模式下运行时默认包含,并且可以选择添加到 prod 模式。有关更多信息,请参阅 Swagger UI 指南。

导航到 localhost:8080/q/swagger-ui/ 并查看 Swagger UI 屏幕。

Swagger UI

结论

本指南介绍了如何使用响应式路由定义 HTTP 端点。它还描述了 Quarkus HTTP 层的结构以及如何编写过滤器。

相关内容