Using Reactive Routes

Reactive 路由提出了一种实现 HTTP 端点的替代方法,你可以在其中声明和链接 routes。此方法在 JavaScript 世界中非常流行,具有 Express.Js 或 Hapi 之类的框架。Quarkus 也提供了使用 Reactive 路由的可能性。你可以仅使用路由实现 REST API,或将其与 Jakarta REST 资源和 servlet 结合使用。 本指南中提供的代码可在 reactive-routes-quickstart directory 下的此 ${quickstarts-base-url}[GitHub 存储库] 中获取

Reactive 路由最初引入是为了在 Quarkus Reactive Architecture 之上为 HTTP API 提供一个 Reactive 执行模型。通过引入 Quarkus REST (formerly RESTEasy Reactive),你现在可以实现 Reactive HTTP API 并仍然使用 Jakarta REST 注释。Reactive 路由仍然受到支持,尤其是如果你想要一种更 route-based 的方法,以及更接近于底层 Reactive 引擎的东西。

Quarkus HTTP

在继续之前,让我们来看看 Quarkus 的 HTTP 层。Quarkus HTTP 支持基于一个非阻塞和 Reactive 引擎(Eclipse Vert.x 和 Netty)。你的应用程序接收的所有 HTTP 请求都由 event loops(I/O 线程)处理,然后被路由到管理请求的代码。根据目标,它可以在工作线程(Servlet,Jax-RS)上调用管理请求的代码或使用 IO 线程(Reactive 路由)。请注意,由于这个原因,一个 Reactive 路由必须是非阻塞的或明确声明其阻塞性质(这将导致在工作线程上调用)。

http architecture

请参阅 Quarkus Reactive Architecture documentation以了解有关此主题的更多详细信息。

Declaring reactive routes

使用 Reactive 路由的第一种方法是使用 @Route 注释。要访问此注释,你需要通过运行此命令添加 quarkus-reactive-routes 扩展:

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

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

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 如果在没有范围注释的类上找到了 Reactive 路由,则 @jakarta.inject.Singleton 会自动添加。
2 @Route 注释表示该方法是一个 Reactive 路由。同样,在默认情况下,该方法中包含的代码不得阻塞。
3 该方法将 RoutingContext 作为参数获取。从 RoutingContext 中,你可以检索 HTTP 请求(使用 request())并使用 response().end(&#8230;&#8203;) 编写响应。
4 如果带注释的方法不返回 void,则这些参数是可选的。
5 RoutingExchangeRoutingContext 的一个便捷包装器,它提供了一些有用的方法。
6 RoutingExchange 用于检索请求查询参数 name
7 该路径定义了一个可以使用 @Param 注释在方法参数中注入的参数 name

有关使用 RoutingContext 的更多详细信息,请查阅 Vert.x Web documentation

@Route 注释允许您配置:

  • path - 使用 Vert.x Web format 以路径进行路由

  • regex - 使用正则表达式进行路由,请参见 for more details

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

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

  • order - 处理传入请求时涉及多条路由时的路由顺序。必须为常规用户路由设置正值。

  • 使用 producesconsumes 生成的 MIME 类型和处理的 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 时,将忽略 type 中的 @Route 属性。

@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

Executing route on a virtual thread

您可以使用 @io.smallrye.common.annotation.RunOnVirtualThread 注释路由方法,以便在虚拟线程上执行该路由。但是,请记住并非所有内容都可以在虚拟线程上安全运行。您应该仔细阅读 Virtual thread support reference 并了解所有详细信息。

Handling conflicting routes

您最终可能会得到多个与给定路径匹配的路由。在以下示例中,两个路由都与 /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() 为空路由的内容路由。

Reactive Route Methods

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

返回 void 的方法必须 end 响应,否则对该路由的 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 注解的方法参数中:

Parameter Type Obtained via

java.lang.String

routingContext.request().getParam()

java.util.Optional<String>

routingContext.request().getParam()

java.util.List<String>

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

Request Parameter Example
@Route
String hello(@Param Optional<String> name) {
   return "Hello " + name.orElse("world");
}

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

Parameter Type Obtained via

java.lang.String

routingContext.request().getHeader()

java.util.Optional<String>

routingContext.request().getHeader()

java.util.List<String>

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

Request Header Example
@Route
String helloFromHeader(@Header("My-Header") String header) {
   return header;
}

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

Parameter Type Obtained via

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

any other type

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

Request Body Example
@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() 的结果匹配。

Failure Handler Example
@Route(type = HandlerType.FAILURE)
void unsupported(UnsupportedOperationException e, HttpServerResponse response) {
  response.setStatusCode(501).end(e.getMessage());
}

Returning Unis

在响应式路由中,您可以直接返回一个 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));
}

当使用响应式客户端时,返回 Unis 很方便:

@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 响应(无内容)。

Returning results

您也可以直接返回结果:

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

注意,处理过程必须是 non-blocking ,因为反应式路由调用于 IO 线程。否则,将 type 注解的 @Route 属性设置为 Route.HandlerType.BLOCKING ,或使用 @io.smallrye.common.annotation.Blocking 注解。

该方法可以返回:

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

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

  • 一个对象 - 在编码成 JSON 之后写进 HTTP 响应中。如果尚未设置,则 content-type 头信息被设置为 application/json

Returning Multis

反应式路由可以返回 Multi 。元素一项项写入响应中。响应 Transfer-Encoding 头部设置为 chunked

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

该方法可以返回:

  • Multi&lt;String&gt; - 元素一项项(每个一项 chunk )写入响应中。

  • Multi&lt;Buffer&gt; - 缓冲区一项项(每个一项 chunk )写入,无任何处理。

  • Multi&lt;Object&gt; - 元素编码为 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

Streaming JSON Array items

您可以返回 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 或字符串表示。

Deprecation of asJsonArray

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

Event Stream and Server-Sent Event support

您可以返回 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
Deprecation of asEventStream

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

Json Stream in NDJSON format

您可以返回 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"
Deprecation of asJsonStream

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

Using Bean Validation

您可以组合响应式路由和 Bean 验证。首先,不要忘记将 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) {
  // ...
}

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

请注意,只支持在返回类型上使用 @Valid。返回的类可以使用任何约束。如果是 Uni,则会检查异步生成的对`Uni`象。

Using the Vert.x Web Router

您还可以在 HTTP routing layer 上直接注册路由,方法是直接在 Router 对象上注册路由。要在启动时检索 Router 实例:

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

查看 Vert.x Web documentation 以了解有关路由注册、选项和可用处理程序的更多信息。

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

您还可以接收 Mutiny variant of the Router (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"));
}

Intercepting HTTP requests

您还可以注册将拦截传入 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 Compression

默认情况下,不压缩 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/javascript`和`application/javascript

如果客户端不支持 HTTP 压缩,那么不会压缩响应体。

Adding OpenAPI and Swagger UI

你可以通过使用 quarkus-smallrye-openapi 扩展添加对 OpenAPISwagger UI 的支持。

通过运行此命令添加扩展:

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

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

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 http://localhost: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

另请参阅 the OpenAPI Guide

Adding MicroProfile OpenAPI Annotations

你可以使用 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://www.apache.org/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 Defining the response.

这将生成此 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://www.apache.org/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

Using Swagger UI

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

导航到 localhost:8080/q/swagger-ui/并观察 Swagger UI 屏幕:

reactive routes guide screenshot01

Conclusion

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