使用 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 响应式架构文档。
声明响应式路由
使用响应式路由的第一种方法是使用 @Route
注解。要访问此注解,您需要通过运行以下命令添加 quarkus-reactive-routes
扩展:
quarkus extension add quarkus-reactive-routes
./mvnw quarkus:add-extension -Dextensions='quarkus-reactive-routes'
./gradlew addExtension --extensions='quarkus-reactive-routes'
这会将以下内容添加到您的构建文件中
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-reactive-routes</artifactId>
</dependency>
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 | RoutingExchange 是 RoutingContext 的便捷包装器,它提供了一些有用的方法。 |
6 | RoutingExchange 用于检索请求查询参数 name 。 |
7 | 路径定义了一个参数 name ,可以使用 @Param 注解将其注入到方法参数中。 |
有关使用 RoutingContext
的更多详细信息,请参阅 Vert.x Web 文档。
@Route
注解允许您配置:
-
path
- 用于按路径路由,使用 Vert.x Web 格式。 -
regex
- 用于使用正则表达式路由,请参阅 以获取更多详细信息。 -
methods
- 触发路由的 HTTP 动词,例如GET
、POST
…… -
type
- 它可以是* normal*(非阻塞)、* blocking*(方法分派到工作线程)或 *failure*,表示此路由在发生故障时被调用。 -
order
- 当多个路由参与处理传入请求时,路由的顺序。对于常规用户路由,必须为正数。 -
使用
produces
和consumes
指定生成的和消耗的 MIME 类型。
例如,您可以声明一个阻塞路由,如下所示:
@Route(methods = HttpMethod.POST, path = "/post", type = Route.HandlerType.BLOCKING)
public void blocking(RoutingContext rc) {
// ...
}
或者,您可以使用
当使用 |
@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
注解的方法参数中:
参数类型 | 通过...获得 |
---|---|
|
|
|
|
|
|
@Route
String hello(@Param Optional<String> name) {
return "Hello " + name.orElse("world");
}
可以使用以下类型将 HttpServerRequest
标头注入到使用 @io.quarkus.vertx.web.Header
注解的方法参数中:
参数类型 | 通过...获得 |
---|---|
|
|
|
|
|
|
@Route
String helloFromHeader(@Header("My-Header") String header) {
return header;
}
可以使用以下类型将请求体注入到使用 @io.quarkus.vertx.web.Body
注解的方法参数中:
参数类型 | 通过...获得 |
---|---|
|
|
|
|
|
|
|
|
任何其他类型 |
|
@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 的弃用
|
事件流和服务器发送事件支持
您可以返回一个 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
接口来定制服务器发送事件的 event
和 id
部分。
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 的弃用
|
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 的弃用
|
使用 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 的 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/html 、text/plain 、text/xml 、text/css 、text/javascript 、application/javascript 、application/json 、application/graphql+json 和 application/xhtml+xml 。 |
如果客户端不支持 HTTP 压缩,则响应正文不会被压缩。 |
添加 OpenAPI 和 Swagger UI
您可以使用 quarkus-smallrye-openapi
扩展来添加对 OpenAPI 和 Swagger UI 的支持。
通过运行以下命令添加扩展:
quarkus extension add quarkus-smallrye-openapi
./mvnw quarkus:add-extension -Dextensions='quarkus-smallrye-openapi'
./gradlew addExtension --extensions='quarkus-smallrye-openapi'
这会将以下内容添加到您的构建文件中
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
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 在 dev
或 test
模式下运行时默认包含,并且可以选择添加到 prod
模式。有关更多信息,请参阅 Swagger UI 指南。
导航到 localhost:8080/q/swagger-ui/ 并查看 Swagger UI 屏幕。