在Quarkus中使用响应式路由

原标题: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线程上。注意,响应式路由必须是非阻塞的或显式声明其是否阻塞。

Quarkus HTTP Architecture

声明响应式路由

使用响应式路由的第一种方法是使用@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触发的方式,例如GETPOST
  • type-可以是normal(非阻塞),blocking(方法会被调度到工作线程上执行),或failure,以表示这个路由在失败时被调用。
  • order-当多个路由都可以处理请求时,路由的顺序是怎样的。对于普通路由,必须为正值。
  • 使用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这个属性:

    @Route(methods = HttpMethod.POST, path = "/post")
    @Blocking
    public void blocking(RoutingContext rc) {
        // ...
    }
    

    使用@Blocking时,会忽略@Routetype属性。

@Route注解是可重复的,可以为一个方法声明几个路由:

@Route(path = "/first") 
@Route(path = "/second")
public void route(RoutingContext rc) {
    // ...
}

如果未设置content-type头,则会使用最适合的content-typecontent-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.StringroutingContext.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.StringroutingContext.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.StringroutingContext.getBodyAsString()
io.vertx.core.buffer.BufferroutingContext.getBody()
io.vertx.core.json.JsonObjectroutingContext.getBodyAsJson()
io.vertx.core.json.JsonArrayroutingContext.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失败(或Uninull),则会返回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)的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验证

可以将响应式路由和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-resteasyquarkus-vertx-web,该扩展将被自动添加。

拦截HTTP请求

可以注册拦截器,用来拦截HTTP请求。这些过滤器也适用于servletJAX-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扩展来添加OpenAPISwagger 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

devtest模式下运行时,会包含Swagger UI ,你可以选择是否将Swagger UI添加到prod模式。有关更多详细信息,请参见《 Swagger UI 指南 》

访问localhost:8080/q/swagger-ui/

Swagger UI

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值