原标题:USING REACTIVE ROUTES
来源:https://quarkus.io/guides/reactive-routes
版权:本作品采用「署名 3.0 未本地化版本 (CC BY 3.0)」许可协议进行许可。
这是原作者的中文翻译版本。
当前版本:1.13
使用响应式路由
响应式路由提出了一种与众不同的方法来实现HTTP。这种方法在JavaScript中非常流行,在Javascript里常常用Express.Js或Hapi之类的框架。在Quarkus里,可以使用路由来实现REST API,也可以结合JAX-RS和Servlet使用。
该指南中提供的代码可在 这个Github仓库的reactive-routes-quickstart
目录中找到
Quarkus HTTP
先了解一下Quarkus的HTTP层。Quarkus HTTP是基于非阻塞和响应式引擎(底层使用Eclipse Vert.x和Netty)。应用程序收到的所有HTTP请求都由事件循环(event loops)处理,事件循环也称为IO线程(IO Thread),然后被路由到具体的代码。使用Servlet,Jax-RS,则处理请求的代码在工作线程(working thread),使用响应式路由,则在IO线程上。注意,响应式路由必须是非阻塞的或显式声明其是否阻塞。
声明响应式路由
使用响应式路由的第一种方法是使用@Route
注解。你需要添加quarkus-vertx-web
扩展:
在pom.xml
文件中,添加:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-web</artifactId>
</dependency>
在bean中,这样使用@Route
注解:
package org.acme.reactive.routes;
import io.quarkus.vertx.web.Route;
import io.quarkus.vertx.web.RoutingExchange;
import io.vertx.core.http.HttpMethod;
import io.vertx.ext.web.RoutingContext;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped //1
public class MyDeclarativeRoutes {
// neither path nor regex is set - match a path derived from the method name
@Route(methods = 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 = HttpMethod.GET)
void greetings(RoutingExchange ex) { //5
ex.ok("hello " + ex.getParam("name").orElse("world"));
}
}
1:如果在响应式路由所在的类上没有作用域的注解,则会自动添加
@javax.inject.Singleton
。2:
@Route
注解表面该方法是响应性路由。默认情况下,该方法中的代码不得阻塞。3:该方法将一个
RoutingContext
作为参数。使用RoutingContext
来与HTTP交互,例如使用request()
获取HTTP请求,使用response().end(…)
来返回响应。4:如果被注解的方法未返回
void
,则方法的method参数是可选的。5:
RoutingExchange
是经过包装了的RoutingContext
,提供了一些有用的方法。
Vert.x Web文档中 提供了RoutingContext
的更多内容。
@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) {
// ...
}
-
另外,可以使用
@io.smallrye.common.annotation.Blocking
注解并忽略type = Route.HandlerType.BLOCKING
这个属性:@Route(methods = HttpMethod.POST, path = "/post") @Blocking public void blocking(RoutingContext rc) { // ... }
使用
@Blocking
时,会忽略@Route
的type
属性。
@Route
注解是可重复的,可以为一个方法声明几个路由:
@Route(path = "/first")
@Route(path = "/second")
public void route(RoutingContext rc) {
// ...
}
如果未设置content-type
头,则会使用最适合的content-type
,content-type
定义在了io.vertx.ext.web.RoutingContext.getAcceptableContentType()
里面。
@Route(path = "/person", produces = "text/html") //1
String person() {
// ...
}
- 1:如果客户端的
accept
头是text/html
类型,会自动设置content-type
头为text/html
。
处理冲突的路由
在以下示例中,两个路由均匹配/accounts/me
:
@Route(path = "/accounts/:id", methods = HttpMethod.GET)
void getAccount(RoutingContext ctx) {
...
}
@Route(path = "/accounts/me", methods = HttpMethod.GET)
void getCurrentUserAccount(RoutingContext ctx) {
...
}
id
设置为me
的情况下,调用了第一个路由,而不是第二个路由。为避免冲突,使用order
属性:
@Route(path = "/accounts/:id", methods = HttpMethod.GET, order = 2)
void getAccount(RoutingContext ctx) {
...
}
@Route(path = "/accounts/me", methods = HttpMethod.GET, order = 1)
void getCurrentUserAccount(RoutingContext ctx) {
...
}
通过给第二个路由一个较低的order
值,它会首先被检查。如果请求路径匹配,则将调用它,否则将检查能否走其他路由。
@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
属性为下面的所有路由的path()
里都加上路径前缀。2:
produces()
的值为text/plain
,则下面的所有路由的produces()
的值都为text/plain
响应式路由的方法
路由方法必须是bean的非私有非静态方法。如果带注解的方法返回void
,则它必须有至少一个参数-请参阅下面的受支持类型。如果带注解的方法未返回void
,则可以没有参数。
返回void
的方法必须手动结束请求,否则对该路由的HTTP请求将永不结束。RoutingExchange
中的有些方法自己本身就可以结束请求,有些方法不能,此时必须自己调用end
方法,有关更多信息,请参考JavaDoc。
路由方法可以接受以下类型的参数:
io.vertx.ext.web.RoutingContext
io.vertx.mutiny.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
此外,当一个方法参数用@io.quarkus.ertx.web.Param
注解,则可以获得http请求的参数
参数类型 | 通过此方法来获取 |
---|---|
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");
}
当一个方法参数用@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。
失败处理示例
@Route(type = HandlerType.FAILURE)
void unsupported(UnsupportedOperationException e, HttpServerResponse response) {
response.setStatusCode(501).end(e.getMessage());
}
返回 Uni
在响应式路由中,可以直接返回一个Uni
:
@Route(path = "/hello")
Uni<String> hello(RoutingContext context) {
return Uni.createFrom().item("Hello world!");
}
@Route(path = "/person")
Uni<Person> getPerson(RoutingContext context) {
return Uni.createFrom().item(() -> new Person("neo", 12345));
}
使用响应式客户端时,返回Unis
很方便:
@Route(path = "/mail")
Uni<Void> sendEmail(RoutingContext context) {
return mailer.send(...);
}
Uni
产生的东西是:
- 字符串-直接写入HTTP响应
- 缓冲区-直接写入HTTP响应
- 一个对象-编码为JSON后写入HTTP响应。
content-type
头被设置为application/json
。
如果返回Uni
失败(或Uni
为null
),则会返回HTTP 500。
返回Uni<Void>
会返回HTTP 204。
返回结果
可以直接返回结果:
@Route(path = "/hello")
String helloSync(RoutingContext context) {
return "Hello world";
}
注意,代码处理过程必须是非阻塞的,因为响应式路由是在IO线程上调用的。如果此处的代码是阻塞的,则要将@Route
注解的type
属性设置为Route.HandlerType.BLOCKING
,或使用@io.smallrye.common.annotation.Blocking
注解。
方法可以返回:
- 字符串-直接写入HTTP响应
- 缓冲区(buffer)-直接写入HTTP响应
-
对象-编码为JSON后写入HTTP响应。响应中的
content-type
头会被自动设置为application/json
返回Multi
响应式路由可以返回一个Multi
。在响应中,这些项目将被一一写入到一个块里(chunk
)。响应中的Transfer-Encoding
头设置为chunked
。(对于Transfer-Encoding: chunked
的知识可以参考 此博客)
@Route(path = "/hello")
Multi<String> hellos(RoutingContext context) {
return Multi.createFrom().items("hello", "world", "!"); //1
}
- 1:此句最终生成
helloworld!
该方法可以返回:
- 一个
Multi<String>
-每一项写在一个chunk
里。 - 一个
Multi<Buffer>
-每一个buffer
写在一个chunk
里。 - 一个
Multi<Object>
-每一项json化,写在一个chunk
里。
@Route(path = "/people")
Multi<Person> people(RoutingContext context) {
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数组。content-type
会被设置为application/json
。
需要使用io.quarkus.vertx.web.ReactiveRoutes.asJsonArray
方法来来包裹Multi
:
@Route(path = "/people")
Multi<Person> people(RoutingContext context) {
return ReactiveRoutes.asJsonArray(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
,要先将buffer中的内容转换为JSON或String类型。
事件流(Event Stream)和服务器发送的事件(Server-Sent Event)
可以通过返回Multi
来生成事件源(event source)即服务器发送的事件流。要启用此功能,你需要使用io.quarkus.vertx.web.ReactiveRoutes.asEventStream
方法来包裹Multi
:
@Route(path = "/people")
Multi<Person> people(RoutingContext context) {
return ReactiveRoutes.asEventStream(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
接口来自定义服务器发送事件(server sent event)的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>
(注意要用io.quarkus.vertx.web.ReactiveRoutes.asEventStream
方法包裹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
使用Bean验证
可以将响应式路由和Bean
验证结合在一起。首先,将quarkus-hibernate-validator
扩展添加到项目中。然后,将约束条件添加到路由的参数上(路由参数首先要用@Param
或@Body
注解):
@Route(produces = "application/json")
Person createPerson(@Body @Valid Person person, @NonNull @Param("id") String primaryKey) {
// ...
}
如果请求的参数未通过验证,则返回HTTP 400
响应。如果未通过验证的请求是JSON格式,则响应会返回这样的格式。
返回是一个对象或一个Uni
,也可以使用@Valid
注解:
@Route(...)
@Valid Uni<Person> createPerson(@Body @Valid Person person, @NonNull @Param("id") String primaryKey) {
// ...
}
如果请求的参数未通过验证,则返回HTTP 500响应。如果未通过验证的请求是JSON格式,则响应会返回这样的格式。
使用Vert.x Web路由
你也可以在HTTP路由层(HTTP routing layer)上注册路由,这需要使用使用Router
对象。需要在启动时获取Router
实例。
public void init(@Observes Router router) {
router.get("/my-route").handler(rc -> rc.response().end("Hello from my route"));
}
要了解路由注册,选项和handler的更多信息。查看Vert.x Web文档
- 要使用
Router
对象,需要quarkus-vertx-http
扩展。如果使用quarkus-resteasy
或quarkus-vertx-web
,该扩展将被自动添加。
拦截HTTP请求
可以注册拦截器,用来拦截HTTP请求。这些过滤器也适用于servlet
,JAX-RS resources
和响应式路由。
以下代码注册了一个拦截器,来添加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()
方法以调用链下的下一个拦截器。
添加OpenAPI和Swagger UI
可以使用quarkus-smallrye-openapi
扩展来添加OpenAPI和Swagger UI。
运行命令:
./mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-smallrye-openapi"
这会将以下内容添加到pom.xml
里:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
这会从您的 Vert.x Routes 生成一个 OpenAPI schema文档(OpenAPI schema document)。
curl http://localhost:8080/q/openapi
你将看到生成的OpenAPI schema文档(OpenAPI schema document):
---
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更好地记录您的schema,例如,添加头信息,或指定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 = 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 schema:
---
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
使用Swagger UI
在dev
或test
模式下运行时,会包含Swagger UI ,你可以选择是否将Swagger UI添加到prod
模式。有关更多详细信息,请参见《 Swagger UI 指南 》。
访问localhost:8080/q/swagger-ui/: