在Quarkus中使用響應(yīng)式路由

原標(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)式路由必須是非阻塞的或顯式聲明其是否阻塞。

Quarkus HTTP Architecture

聲明響應(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í),路由的順序是怎樣的。對于普通路由,必須為正值。
  • 使用producesconsumes來指明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ì)忽略@Routetype屬性。

@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.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)一個(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)的eventid部分:

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-resteasyquarkus-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ò)展來添加OpenAPISwagger 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

devtest模式下運(yùn)行時(shí),會(huì)包含Swagger UI ,你可以選擇是否將Swagger UI添加到prod模式。有關(guān)更多詳細(xì)信息,請參見《 Swagger UI 指南 》。

訪問localhost:8080/q/swagger-ui/

Swagger UI
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容