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 路由必须是非阻塞的或明确声明其阻塞性质(这将导致在工作线程上调用)。
请参阅 Quarkus Reactive Architecture documentation以了解有关此主题的更多详细信息。
Declaring reactive routes
使用 Reactive 路由的第一种方法是使用 @Route
注释。要访问此注释,你需要通过运行此命令添加 quarkus-reactive-routes
扩展:
quarkus extension add {add-extension-extensions}
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
./gradlew addExtension --extensions='{add-extension-extensions}'
这会将以下内容添加到构建文件中:
<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 | 如果在没有范围注释的类上找到了 Reactive 路由,则 @jakarta.inject.Singleton 会自动添加。 |
2 | @Route 注释表示该方法是一个 Reactive 路由。同样,在默认情况下,该方法中包含的代码不得阻塞。 |
3 | 该方法将 RoutingContext 作为参数获取。从 RoutingContext 中,你可以检索 HTTP 请求(使用 request() )并使用 response().end(…​) 编写响应。 |
4 | 如果带注释的方法不返回 void ,则这些参数是可选的。 |
5 | RoutingExchange 是 RoutingContext 的一个便捷包装器,它提供了一些有用的方法。 |
6 | RoutingExchange 用于检索请求查询参数 name 。 |
7 | 该路径定义了一个可以使用 @Param 注释在方法参数中注入的参数 name 。 |
有关使用 RoutingContext
的更多详细信息,请查阅 Vert.x Web documentation。
@Route
注释允许您配置:
-
path
- 使用 Vert.x Web format 以路径进行路由 -
regex
- 使用正则表达式进行路由,请参见 for more details -
methods
- 触发路由的 HTTP 动词,例如GET
、POST
… -
type
- 可以是 normal(非阻塞)、blocking(在工作线程上分派方法)或 failure,以指示在发生故障时调用此路由 -
order
- 处理传入请求时涉及多条路由时的路由顺序。必须为常规用户路由设置正值。 -
使用
produces
和consumes
生成的 MIME 类型和处理的 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 。 |
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
,则参数是可选项。
返回 |
路由方法可以接受以下类型的参数:
-
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 |
---|---|
|
|
|
|
|
|
@Route
String hello(@Param Optional<String> name) {
return "Hello " + name.orElse("world");
}
可以使用以下类型将 HttpServerRequest
头信息注入带 @io.quarkus.vertx.web.Header
注解的方法参数中:
Parameter Type | Obtained via |
---|---|
|
|
|
|
|
|
@Route
String helloFromHeader(@Header("My-Header") String header) {
return header;
}
可以使用以下类型将请求主体注入带 @io.quarkus.vertx.web.Body
注解的方法参数中:
Parameter Type | Obtained via |
---|---|
|
|
|
|
|
|
|
|
any other type |
|
@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());
}
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<String>
- 元素一项项(每个一项 chunk )写入响应中。 -
Multi<Buffer>
- 缓冲区一项项(每个一项 chunk )写入,无任何处理。 -
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
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
]
|
只有 Multi<String>
、 Multi<Object>
和 Multi<Void>
可以写入 JSON 数组。使用 Multi<Void>
会生成一个空数组。您无法使用 Multi<Buffer>
。如果你需要使用 Buffer
,请首先将内容转化为 JSON 或字符串表示。
Deprecation of
asJsonArray
|
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
|
您还可以实现 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
Deprecation of
asEventStream
|
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}
|
您还可以提供字符串而不是对象,在这种情况下,字符串将用引号括起来以成为有效的 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
|
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 以了解有关路由注册、选项和可用处理程序的更多信息。
|
您还可以接收 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
标头,则响应体永远不会被压缩。
默认情况下,压缩以下媒体类型列表: |
如果客户端不支持 HTTP 压缩,那么不会压缩响应体。 |
Adding OpenAPI and Swagger UI
你可以通过使用 quarkus-smallrye-openapi
扩展添加对 OpenAPI和 Swagger UI 的支持。
通过运行此命令添加扩展:
quarkus extension add {add-extension-extensions}
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
./gradlew addExtension --extensions='{add-extension-extensions}'
这会将以下内容添加到构建文件中:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
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 屏幕: