原標(biāo)題:USING REACTIVE ROUTES
來源:https://quarkus.io/guides/reactive-routes
版權(quán):本作品采用「署名 3.0 未本地化版本 (CC BY 3.0)」許可協(xié)議進行許可。
這是原作者的中文翻譯版本臀防。
當(dāng)前版本:1.13
使用響應(yīng)式路由
響應(yīng)式路由提出了一種與眾不同的方法來實現(xiàn)HTTP寄悯。這種方法在JavaScript中非常流行,在Javascript里常常用Express.Js或Hapi之類的框架幼东。在Quarkus里颊糜,可以使用路由來實現(xiàn)REST API凑阶,也可以結(jié)合JAX-RS和Servlet使用。
該指南中提供的代碼可在 這個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
擴展:
在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)式路由所在的類上沒有作用域的注解奶镶,則會自動添加
@javax.inject.Singleton
迟赃。2:
@Route
注解表面該方法是響應(yīng)性路由陪拘。默認情況下,該方法中的代碼不得阻塞纤壁。3:該方法將一個
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
-用正則表達式的路由,查看更多細節(jié) -
methods
-HTTP觸發(fā)的方式舅世,例如GET
旦委,POST
... -
type
-可以是normal
(非阻塞)奇徒,blocking
(方法會被調(diào)度到工作線程上執(zhí)行)雏亚,或failure
,以表示這個路由在失敗時被調(diào)用摩钙。 -
order
-當(dāng)多個路由都可以處理請求時罢低,路由的順序是怎樣的。對于普通路由胖笛,必須為正值网持。 - 使用
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
注解是可重復(fù)的身弊,可以為一個方法聲明幾個路由:
@Route(path = "/first")
@Route(path = "/second")
public void route(RoutingContext rc) {
// ...
}
如果未設(shè)置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
類型帖汞,會自動設(shè)置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
設(shè)置為me
的情況下翩蘸,調(diào)用了第一個路由,而不是第二個路由淮逊。為避免沖突催首,使用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
值扶踊,它會首先被檢查。如果請求路徑匹配郎任,則將調(diào)用它姻檀,否則將檢查能否走其他路由。
@RouteBase
該注解可為響應(yīng)式路由配置一些默認值涝滴。
@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
歼疮,則它必須有至少一個參數(shù)-請參閱下面的受支持類型杂抽。如果帶注解的方法未返回void
,則可以沒有參數(shù)韩脏。
返回void
的方法必須手動結(jié)束請求缩麸,否則對該路由的HTTP請求將永不結(jié)束。RoutingExchange
中的有些方法自己本身就可以結(jié)束請求赡矢,有些方法不能杭朱,此時必須自己調(diào)用end
方法,有關(guān)更多信息吹散,請參考JavaDoc弧械。
路由方法可以接受以下類型的參數(shù):
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
此外,當(dāng)一個方法參數(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)一個方法參數(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)一個方法參數(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;
}
如果要處理失敗界轩,可以聲明一個方法參數(shù)画饥,這個參數(shù)的類型繼承Throwable。
失敗處理示例
@Route(type = HandlerType.FAILURE)
void unsupported(UnsupportedOperationException e, HttpServerResponse response) {
response.setStatusCode(501).end(e.getMessage());
}
返回 Uni
在響應(yīng)式路由中浊猾,可以直接返回一個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)式客戶端時抖甘,返回Unis
很方便:
@Route(path = "/mail")
Uni<Void> sendEmail(RoutingContext context) {
return mailer.send(...);
}
Uni
產(chǎn)生的東西是:
- 字符串-直接寫入HTTP響應(yīng)
- 緩沖區(qū)-直接寫入HTTP響應(yīng)
- 一個對象-編碼為JSON后寫入HTTP響應(yīng)。
content-type
頭被設(shè)置為application/json
葫慎。
如果返回Uni
失斚纬埂(或Uni
為null
),則會返回HTTP 500幅疼。
返回Uni<Void>
會返回HTTP 204米奸。
返回結(jié)果
可以直接返回結(jié)果:
@Route(path = "/hello")
String helloSync(RoutingContext context) {
return "Hello world";
}
注意,代碼處理過程必須是非阻塞的爽篷,因為響應(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
頭會被自動設(shè)置為application/json
返回Multi
響應(yīng)式路由可以返回一個Multi
。在響應(yīng)中棕硫,這些項目將被一一寫入到一個塊里(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!
該方法可以返回:
- 一個
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));
}
產(chǎn)生如下結(jié)果:
{"name":"superman", "id": 1} // chunk 1
{"name":"batman", "id": 2} // chunk 2
{"name":"spiderman", "id": 3} // chunk 3
流式JSON數(shù)組項
可以通過返回Multi
來生成JSON數(shù)組包各。content-type
會被設(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>
會產(chǎn)生一個空數(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
可以通過實現(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īng)式路由和Bean
驗證結(jié)合在一起。首先铐料,將quarkus-hibernate-validator
擴展添加到項目中渐裂。然后豺旬,將約束條件添加到路由的參數(shù)上(路由參數(shù)首先要用@Param
或@Body
注解):
@Route(produces = "application/json")
Person createPerson(@Body @Valid Person person, @NonNull @Param("id") String primaryKey) {
// ...
}
如果請求的參數(shù)未通過驗證钠惩,則返回HTTP 400
響應(yīng)。如果未通過驗證的請求是JSON格式族阅,則響應(yīng)會返回這樣的格式篓跛。
返回是一個對象或一個Uni
,也可以使用@Valid
注解:
@Route(...)
@Valid Uni<Person> createPerson(@Body @Valid Person person, @NonNull @Param("id") String primaryKey) {
// ...
}
如果請求的參數(shù)未通過驗證坦刀,則返回HTTP 500響應(yīng)愧沟。如果未通過驗證的請求是JSON格式,則響應(yīng)會返回這樣的格式鲤遥。
使用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
和響應(yīng)式路由微酬。
以下代碼注冊了一個攔截器绘趋,來添加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)先級較高的攔截器會被優(yōu)先調(diào)用。2:攔截器來調(diào)用該
next()
方法以調(diào)用鏈下的下一個攔截器颗管。
添加OpenAPI和Swagger UI
可以使用quarkus-smallrye-openapi
擴展來添加OpenAPI和Swagger UI埋心。
運行命令:
./mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-smallrye-openapi"
這會將以下內(nèi)容添加到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:定義響應(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
模式下運行時梗逮,會包含Swagger UI 项秉,你可以選擇是否將Swagger UI添加到prod
模式。有關(guān)更多詳細信息慷彤,請參見《 Swagger UI 指南 》娄蔼。
訪問localhost:8080/q/swagger-ui/: