原標(biāo)題:USING REACTIVE ROUTES
來源:https://quarkus.io/guides/reactive-routes
版權(quán):本作品采用「署名 3.0 未本地化版本 (CC BY 3.0)」許可協(xié)議進(jìn)行許可。
這是原作者的中文翻譯版本。
當(dāng)前版本:1.13
使用響應(yīng)式路由
響應(yīng)式路由提出了一種與眾不同的方法來實(shí)現(xiàn)HTTP。這種方法在JavaScript中非常流行,在Javascript里常常用Express.Js或Hapi之類的框架。在Quarkus里,可以使用路由來實(shí)現(xiàn)REST API,也可以結(jié)合JAX-RS和Servlet使用。
該指南中提供的代碼可在 這個(gè)Github倉庫的reactive-routes-quickstart目錄中找到
Quarkus HTTP
先了解一下Quarkus的HTTP層。Quarkus HTTP是基于非阻塞和響應(yīng)式引擎(底層使用Eclipse Vert.x和Netty)。應(yīng)用程序收到的所有HTTP請求都由事件循環(huán)(event loops)處理,事件循環(huán)也稱為IO線程(IO Thread),然后被路由到具體的代碼。使用Servlet,Jax-RS,則處理請求的代碼在工作線程(working thread),使用響應(yīng)式路由,則在IO線程上。注意,響應(yīng)式路由必須是非阻塞的或顯式聲明其是否阻塞。

聲明響應(yīng)式路由
使用響應(yīng)式路由的第一種方法是使用@Route注解。你需要添加quarkus-vertx-web擴(kuò)展:
在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:如果在響應(yīng)式路由所在的類上沒有作用域的注解,則會(huì)自動(dòng)添加
@javax.inject.Singleton。2:
@Route注解表面該方法是響應(yīng)性路由。默認(rèn)情況下,該方法中的代碼不得阻塞。3:該方法將一個(gè)
RoutingContext作為參數(shù)。使用RoutingContext來與HTTP交互,例如使用request()獲取HTTP請求,使用response().end(…)來返回響應(yīng)。4:如果被注解的方法未返回
void,則方法的method參數(shù)是可選的。5:
RoutingExchange是經(jīng)過包裝了的RoutingContext,提供了一些有用的方法。
Vert.x Web文檔中 提供了RoutingContext的更多內(nèi)容。
@Route注解允許配置如下參數(shù):
-
path-指明路由路徑,要依照Vert.x Web格式 -
regex-用正則表達(dá)式的路由,查看更多細(xì)節(jié) -
methods-HTTP觸發(fā)的方式,例如GET,POST... -
type-可以是normal(非阻塞),blocking(方法會(huì)被調(diào)度到工作線程上執(zhí)行),或failure,以表示這個(gè)路由在失敗時(shí)被調(diào)用。 -
order-當(dāng)多個(gè)路由都可以處理請求時(shí),路由的順序是怎樣的。對于普通路由,必須為正值。 - 使用
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這個(gè)屬性:@Route(methods = HttpMethod.POST, path = "/post") @Blocking public void blocking(RoutingContext rc) { // ... }使用
@Blocking時(shí),會(huì)忽略@Route的type屬性。
@Route注解是可重復(fù)的,可以為一個(gè)方法聲明幾個(gè)路由:
@Route(path = "/first")
@Route(path = "/second")
public void route(RoutingContext rc) {
// ...
}
如果未設(shè)置content-type頭,則會(huì)使用最適合的content-type ,content-type定義在了io.vertx.ext.web.RoutingContext.getAcceptableContentType()里面。
@Route(path = "/person", produces = "text/html") //1
String person() {
// ...
}
- 1:如果客戶端的
accept頭是text/html類型,會(huì)自動(dòng)設(shè)置content-type頭為text/html。
處理沖突的路由
在以下示例中,兩個(gè)路由均匹配/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設(shè)置為me的情況下,調(diào)用了第一個(gè)路由,而不是第二個(gè)路由。為避免沖突,使用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) {
...
}
通過給第二個(gè)路由一個(gè)較低的order值,它會(huì)首先被檢查。如果請求路徑匹配,則將調(diào)用它,否則將檢查能否走其他路由。
@RouteBase
該注解可為響應(yīng)式路由配置一些默認(rèn)值。
@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
響應(yīng)式路由的方法
路由方法必須是bean的非私有非靜態(tài)方法。如果帶注解的方法返回void,則它必須有至少一個(gè)參數(shù)-請參閱下面的受支持類型。如果帶注解的方法未返回void,則可以沒有參數(shù)。
返回void的方法必須手動(dòng)結(jié)束請求,否則對該路由的HTTP請求將永不結(jié)束。RoutingExchange中的有些方法自己本身就可以結(jié)束請求,有些方法不能,此時(shí)必須自己調(diào)用end方法,有關(guān)更多信息,請參考JavaDoc。
路由方法可以接受以下類型的參數(shù):
io.vertx.ext.web.RoutingContextio.vertx.mutiny.ext.web.RoutingContextio.quarkus.vertx.web.RoutingExchangeio.vertx.core.http.HttpServerRequestio.vertx.core.http.HttpServerResponseio.vertx.mutiny.core.http.HttpServerRequestio.vertx.mutiny.core.http.HttpServerResponse
此外,當(dāng)一個(gè)方法參數(shù)用@io.quarkus.ertx.web.Param注解,則可以獲得http請求的參數(shù)
| 參數(shù)類型 | 通過此方法來獲取 |
|---|---|
java.lang.String |
routingContext.request().getParam() |
java.util.Optional<String> |
routingContext.request().getParam() |
java.util.List<String> |
routingContext.request().params().getAll() |
請求參數(shù)示例
@Route
String hello(@Param Optional<String> name) {
return "Hello " + name.orElse("world");
}
當(dāng)一個(gè)方法參數(shù)用@io.quarkus.vertx.web.Header注解,那么可以獲得請求頭
| 參數(shù)類型 | 通過此方法來獲取 |
|---|---|
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;
}
當(dāng)一個(gè)方法參數(shù)用@io.quarkus.vertx.web.Body注解,那么可以獲得請求體
| 參數(shù)類型 | 通過此方法獲取 |
|---|---|
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;
}
如果要處理失敗,可以聲明一個(gè)方法參數(shù),這個(gè)參數(shù)的類型繼承Throwable。
失敗處理示例
@Route(type = HandlerType.FAILURE)
void unsupported(UnsupportedOperationException e, HttpServerResponse response) {
response.setStatusCode(501).end(e.getMessage());
}
返回 Uni
在響應(yīng)式路由中,可以直接返回一個(gè)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));
}
使用響應(yīng)式客戶端時(shí),返回Unis很方便:
@Route(path = "/mail")
Uni<Void> sendEmail(RoutingContext context) {
return mailer.send(...);
}
Uni產(chǎn)生的東西是:
- 字符串-直接寫入HTTP響應(yīng)
- 緩沖區(qū)-直接寫入HTTP響應(yīng)
- 一個(gè)對象-編碼為JSON后寫入HTTP響應(yīng)。
content-type頭被設(shè)置為application/json。
如果返回Uni失?。ɑ?code>Uni為null),則會(huì)返回HTTP 500。
返回Uni<Void>會(huì)返回HTTP 204。
返回結(jié)果
可以直接返回結(jié)果:
@Route(path = "/hello")
String helloSync(RoutingContext context) {
return "Hello world";
}
注意,代碼處理過程必須是非阻塞的,因?yàn)轫憫?yīng)式路由是在IO線程上調(diào)用的。如果此處的代碼是阻塞的,則要將@Route注解的type屬性設(shè)置為Route.HandlerType.BLOCKING,或使用@io.smallrye.common.annotation.Blocking注解。
方法可以返回:
- 字符串-直接寫入HTTP響應(yīng)
- 緩沖區(qū)(buffer)-直接寫入HTTP響應(yīng)
-
對象-編碼為JSON后寫入HTTP響應(yīng)。響應(yīng)中的
content-type頭會(huì)被自動(dòng)設(shè)置為application/json
返回Multi
響應(yīng)式路由可以返回一個(gè)Multi。在響應(yīng)中,這些項(xiàng)目將被一一寫入到一個(gè)塊里(chunk)。響應(yīng)中的Transfer-Encoding頭設(shè)置為chunked。(對于Transfer-Encoding: chunked的知識可以參考 此博客)
@Route(path = "/hello")
Multi<String> hellos(RoutingContext context) {
return Multi.createFrom().items("hello", "world", "!"); //1
}
- 1:此句最終生成
helloworld!
該方法可以返回:
- 一個(gè)
Multi<String>-每一項(xiàng)寫在一個(gè)chunk里。 - 一個(gè)
Multi<Buffer>-每一個(gè)buffer寫在一個(gè)chunk里。 - 一個(gè)
Multi<Object>-每一項(xiàng)json化,寫在一個(gè)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));
}
產(chǎn)生如下結(jié)果:
{"name":"superman", "id": 1} // chunk 1
{"name":"batman", "id": 2} // chunk 2
{"name":"spiderman", "id": 3} // chunk 3
流式JSON數(shù)組項(xiàng)
可以通過返回Multi來生成JSON數(shù)組。content-type會(huì)被設(shè)置為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)));
}
產(chǎn)生如下結(jié)果:
[
{"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數(shù)組。使用Multi<Void>會(huì)產(chǎn)生一個(gè)空數(shù)組。不能使用Multi<Buffer>。如果需要使用Buffer,要先將buffer中的內(nèi)容轉(zhuǎn)換為JSON或String類型。
事件流(Event Stream)和服務(wù)器發(fā)送的事件(Server-Sent Event)
可以通過返回Multi來生成事件源(event source)即服務(wù)器發(fā)送的事件流。要啟用此功能,你需要使用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)));
}
結(jié)果是:
data: {"name":"superman", "id": 1}
id: 0
data: {"name":"batman", "id": 2}
id: 1
data: {"name":"spiderman", "id": 3}
id: 2
可以通過實(shí)現(xiàn)io.quarkus.vertx.web.ReactiveRoutes.ServerSentEvent接口來自定義服務(wù)器發(fā)送事件(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驗(yàn)證
可以將響應(yīng)式路由和Bean驗(yàn)證結(jié)合在一起。首先,將quarkus-hibernate-validator擴(kuò)展添加到項(xiàng)目中。然后,將約束條件添加到路由的參數(shù)上(路由參數(shù)首先要用@Param或@Body注解):
@Route(produces = "application/json")
Person createPerson(@Body @Valid Person person, @NonNull @Param("id") String primaryKey) {
// ...
}
如果請求的參數(shù)未通過驗(yàn)證,則返回HTTP 400響應(yīng)。如果未通過驗(yàn)證的請求是JSON格式,則響應(yīng)會(huì)返回這樣的格式。
返回是一個(gè)對象或一個(gè)Uni,也可以使用@Valid注解:
@Route(...)
@Valid Uni<Person> createPerson(@Body @Valid Person person, @NonNull @Param("id") String primaryKey) {
// ...
}
如果請求的參數(shù)未通過驗(yàn)證,則返回HTTP 500響應(yīng)。如果未通過驗(yàn)證的請求是JSON格式,則響應(yīng)會(huì)返回這樣的格式。
使用Vert.x Web路由
你也可以在HTTP路由層(HTTP routing layer)上注冊路由,這需要使用使用Router對象。需要在啟動(dòng)時(shí)獲取Router實(shí)例。
public void init(@Observes Router router) {
router.get("/my-route").handler(rc -> rc.response().end("Hello from my route"));
}
要了解路由注冊,選項(xiàng)和handler的更多信息。查看Vert.x Web文檔
- 要使用
Router對象,需要quarkus-vertx-http擴(kuò)展。如果使用quarkus-resteasy或quarkus-vertx-web,該擴(kuò)展將被自動(dòng)添加。
攔截HTTP請求
可以注冊攔截器,用來攔截HTTP請求。這些過濾器也適用于servlet,JAX-RS resources和響應(yīng)式路由。
以下代碼注冊了一個(gè)攔截器,來添加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()定義了攔截器的優(yōu)先級--優(yōu)先級較高的攔截器會(huì)被優(yōu)先調(diào)用。2:攔截器來調(diào)用該
next()方法以調(diào)用鏈下的下一個(gè)攔截器。
添加OpenAPI和Swagger UI
可以使用quarkus-smallrye-openapi擴(kuò)展來添加OpenAPI和Swagger UI。
運(yùn)行命令:
./mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-smallrye-openapi"
這會(huì)將以下內(nèi)容添加到pom.xml里:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
這會(huì)從您的 Vert.x Routes 生成一個(gè) 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:定義響應(yīng)
這將生成以下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模式下運(yùn)行時(shí),會(huì)包含Swagger UI ,你可以選擇是否將Swagger UI添加到prod模式。有關(guān)更多詳細(xì)信息,請參見《 Swagger UI 指南 》。
訪問localhost:8080/q/swagger-ui/:
