编辑此页面

使用 Quarkus REST (formerly RESTEasy Reactive) 编写 REST 服务

本指南将解释如何在 Quarkus 中使用 Quarkus REST 编写 REST 服务。

这是 Quarkus REST 的参考指南。有关更轻量级的介绍,请参阅编写 JSON REST 服务指南

什么是 Quarkus REST?

Quarkus REST 是一个全新的Jakarta REST(以前称为 JAX-RS)实现,它从头开始编写,以便在我们的通用Vert.x层上运行,因此是完全反应式的,同时与 Quarkus 紧密集成,从而将许多工作转移到构建时。

您应该能够将其替换为任何 Jakarta REST 实现,但除此之外,它在阻塞和非阻塞端点方面都具有出色的性能,并且在 Jakarta REST 提供的基础上还增加了许多新功能。

编写端点

入门

将以下导入添加到您的构建文件中

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-rest")

现在您可以在 org.acme.rest.Endpoint 类中编写您的第一个端点了。

package org.acme.rest;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("")
public class Endpoint {

    @GET
    public String hello() {
        return "Hello, World!";
    }
}

术语

REST

REpresentational State Transfer (表述性状态转移)

端点

用于处理 REST 调用的 Java 方法

URL / URI (Uniform Resource Locator / Identifier,统一资源定位符 / 标识符)

用于标识 REST 资源的位置(规范

资源

代表您的领域对象。这是您的 API 提供和修改的内容。在 Jakarta REST 中也称为实体

Representation (表述)

您的资源在网络上传输时的表示形式,可能因内容类型而异。

Content type (内容类型)

指定特定的表述(也称为媒体类型),例如 text/plainapplication/json

HTTP

用于路由 REST 调用的底层网络协议(请参阅HTTP 规范)。

HTTP request (HTTP 请求)

HTTP 调用的一部分,包含 HTTP 方法、目标 URI、标头和可选的消息正文。

HTTP response (HTTP 响应)

HTTP 调用的一部分,包含 HTTP 响应状态、标头和可选的消息正文。

声明端点:URI 映射

任何带有 @Path 注释的类,只要其方法带有 HTTP 方法注释(如下所示),都可以公开为 REST 端点。

@Path 注释定义了这些方法将被公开的 URI 前缀。它可以为空,或包含如 restrest/V1 这样的前缀。

每个公开的端点方法又可以带有另一个 @Path 注释,该注释会添加到其包含的类注释上。例如,这会定义一个 rest/hello 端点。

package org.acme.rest;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("rest")
public class Endpoint {

    @Path("hello")
    @GET
    public String hello() {
        return "Hello, World!";
    }
}

有关 URI 映射的更多信息,请参阅URI 参数

您可以使用 @ApplicationPath 注释为所有 REST 端点设置根路径,如下所示。

package org.acme.rest;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("/api")
public static class MyApplication extends Application {

}

这将导致所有 REST 端点都相对于 /api 解析,因此上面带有 @Path("rest") 的端点将可以在 /api/rest/ 访问。如果您不想使用注释,也可以设置 quarkus.rest.path 构建时属性来设置根路径。

声明端点:HTTP 方法

每个端点方法都必须带有以下注释之一,该注释定义了将映射到方法的 HTTP 方法。

表 1. HTTP 方法注释
注解 用法

@GET

获取资源表述,不应修改状态,幂等HTTP 文档)。

@HEAD

获取有关资源的元数据,类似于 GET 但没有正文(HTTP 文档)。

@POST

创建资源并获取其链接(HTTP 文档)。

@PUT

替换资源或创建资源,应幂等HTTP 文档)。

@DELETE

删除现有资源,幂等HTTP 文档)。

@OPTIONS

获取有关资源的信息,幂等HTTP 文档)。

@PATCH

更新资源或创建资源,非幂等HTTP 文档)。

您还可以通过使用 @HttpMethod 注释将其声明为注释来声明其他 HTTP 方法。

package org.acme.rest;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.Path;

@Retention(RetentionPolicy.RUNTIME)
@HttpMethod("CHEESE")
@interface CHEESE {
}

@Path("")
public class Endpoint {

    @CHEESE
    public String hello() {
        return "Hello, Cheese World!";
    }
}

声明端点:表述/内容类型

每个端点方法都可以消耗或生成特定的资源表述,这由 HTTP Content-Type 标头指示,该标头又包含 MIME(媒体类型)值,例如以下内容:

  • text/plain,这是返回 String 的任何端点的默认值。

  • text/html 用于 HTML(例如,使用Qute 模板)。

  • application/json 用于 JSON REST 端点

  • text/*,这是任何文本媒体类型的子类型通配符。

  • */*,这是任何媒体类型的通配符。

您可以使用 @Produces@Consumes 注释来注释您的端点类,这允许您指定端点可以接受的 HTTP 请求正文或生成的 HTTP 响应正文的媒体类型。这些类注释适用于每个方法。

任何方法也可以用 @Produces@Consumes 注释来注释,在这种情况下,它们将覆盖任何最终的类注释。

MediaType 类有许多常量,您可以用来指向特定的预定义媒体类型。

有关更多信息,请参阅协商部分。

访问请求参数

别忘了配置您的编译器以使用 -parameters (javac) 或 <parameters><maven.compiler.parameters> (Maven) 生成参数名称信息。

您的端点方法可以获取以下 HTTP 请求元素:

表 2. HTTP 请求参数注释
HTTP element (HTTP 元素) 注解 用法

Path parameter (路径参数)

@RestPath(或无)

URI 模板参数(URI 模板规范的简化版本),有关更多信息,请参阅URI 参数

查询参数

@RestQuery

URI 查询参数的值。

Header (标头)

@RestHeader

HTTP 标头的值。

Cookie (Cookie)

@RestCookie

HTTP Cookie 的值。

Form parameter (表单参数)

@RestForm

HTTP URL 编码表单的值。

Matrix parameter (矩阵参数)

@RestMatrix

URI 路径段参数的值。

对于这些注释中的每一个,您可以指定它们引用的元素的名称,否则,它们将使用带注释的方法参数的名称。

如果客户端发出了以下 HTTP 调用

POST /cheeses;variant=goat/tomme?age=matured HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Cookie: level=hardcore
X-Cheese-Secret-Handshake: fist-bump

smell=strong

那么您可以通过此端点方法获取所有各种参数。

package org.acme.rest;

import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;

import org.jboss.resteasy.reactive.RestCookie;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.RestHeader;
import org.jboss.resteasy.reactive.RestMatrix;
import org.jboss.resteasy.reactive.RestPath;
import org.jboss.resteasy.reactive.RestQuery;

@Path("/cheeses/{type}")
public class Endpoint {

    @POST
    public String allParams(@RestPath String type,
                            @RestMatrix String variant,
                            @RestQuery String age,
                            @RestCookie String level,
                            @RestHeader("X-Cheese-Secret-Handshake")
                            String secretHandshake,
                            @RestForm String smell) {
        return type + "/" + variant + "/" + age + "/" + level + "/"
            + secretHandshake + "/" + smell;
    }
}
@RestPath 注释是可选的:任何名称与现有 URI 模板变量匹配的参数都将自动假定具有 @RestPath

您还可以为此使用任何 Jakarta REST 注释 @PathParam@QueryParam@HeaderParam@CookieParam@FormParam@MatrixParam,但它们要求您指定参数名称。

有关更高级的用例,请参阅参数映射

当 Quarkus REST 请求参数处理代码中发生异常时,默认情况下(出于安全原因)异常不会打印到日志中。这有时会使理解为什么会返回某些 HTTP 状态码变得困难(因为 Jakarta REST 在各种情况下强制使用不直观的错误代码)。在这种情况下,建议将 org.jboss.resteasy.reactive.server.handlers.ParameterHandler 类别(category)的日志级别设置为 DEBUG,如下所示。

quarkus.log.category."org.jboss.resteasy.reactive.server.handlers.ParameterHandler".level=DEBUG

在自定义类中分组参数

您可以将请求参数分组到容器类中,而不是将它们声明为端点的方法参数,因此我们可以像这样重写前面的示例。

package org.acme.rest;

import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;

import org.jboss.resteasy.reactive.RestCookie;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.RestHeader;
import org.jboss.resteasy.reactive.RestMatrix;
import org.jboss.resteasy.reactive.RestPath;
import org.jboss.resteasy.reactive.RestQuery;

@Path("/cheeses/{type}")
public class Endpoint {

    public static class Parameters {
        @RestPath
        String type;

        @RestMatrix
        String variant;

        @RestQuery
        String age;

        @RestCookie
        String level;

        @RestHeader("X-Cheese-Secret-Handshake")
        String secretHandshake;

        @RestForm
        String smell;
    }

    @POST
    public String allParams(@BeanParam Parameters parameters) { (1)
        return parameters.type + "/" + parameters.variant + "/" + parameters.age
            + "/" + parameters.level + "/" + parameters.secretHandshake
            + "/" + parameters.smell;
    }
}
1 BeanParam 是必需的,以符合 Jakarta REST 规范,以便像 OpenAPI 这样的库可以内省参数。

记录类(Record classes)也受支持,因此您可以将前面的示例重写为记录。

    public record Parameters(
        @RestPath
        String type,

        @RestMatrix
        String variant,

        @RestQuery
        String age,

        @RestCookie
        String level,

        @RestHeader("X-Cheese-Secret-Handshake")
        String secretHandshake,

        @RestForm
        String smell){}

声明 URI 参数

您可以声明 URI 参数并在路径中使用正则表达式,例如,以下端点将处理 /hello/stef/23/hello 的请求,但不会处理 /hello/stef/0x23 的请求。

package org.acme.rest;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("hello")
public class Endpoint {

    @Path("{name}/{age:\\d+}")
    @GET
    public String personalisedHello(String name, int age) {
        return "Hello " + name + " is your age really " + age + "?";
    }

    @GET
    public String genericHello() {
        return "Hello stranger";
    }
}

访问请求正文

任何没有注释的方法参数都将接收方法正文。[1],在将其从 HTTP 表述映射到参数的 Java 类型之后。

开箱即用将支持以下参数类型:

表 3. 请求正文参数类型
类型 用法

File (文件)

整个请求正文(作为临时文件)。

byte[]

整个请求正文,未解码。

char[]

整个请求正文,已解码。

String

整个请求正文,已解码。

InputStream

请求正文(作为阻塞流)。

Reader

请求正文(作为阻塞流)。

所有 Java 基本类型及其包装类。

Java primitive types (Java 基本类型)

BigDecimal, BigInteger

大整数和十进制数。

JsonArray, JsonObject, JsonStructure, JsonValue

JSON value types (JSON 值类型)

Buffer

Vert.x Buffer。

any other type (任何其他类型)

从 JSON 映射到该类型

您可以添加对更多正文参数类型的支持。

处理 Multipart 表单数据

要处理内容类型为 multipart/form-data 的 HTTP 请求,您可以使用常规的 @RestForm 注释,但我们有特殊的类型允许您将部分作为文件或实体访问。让我们来看一个使用示例。

假设一个 HTTP 请求包含文件上传、JSON 实体和包含字符串描述的表单值,我们可以编写以下端点。

import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.MediaType;

import org.jboss.resteasy.reactive.PartType;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.multipart.FileUpload;

@Path("multipart")
public class MultipartResource {
    public static class Person {
        public String firstName;
        public String lastName;
    }

    @POST
    public void multipart(@RestForm String description,
            @RestForm("image") FileUpload file,
            @RestForm @PartType(MediaType.APPLICATION_JSON) Person person) {
        // do something
    }
}

description 参数将包含 HTTP 请求中名为 description 的部分的数据(因为 @RestForm 没有定义值,因此使用字段名称),而 file 参数将包含 HTTP 请求的 image 部分中关于上传文件的数据,person 参数将使用 JSON 正文读取器读取 Person 实体。

multipart 请求中每个部分的大小必须符合 quarkus.http.limits.max-form-attribute-size 的值,其默认值为 2048 字节。任何部分大小超过此配置的请求都将导致 HTTP 状态码 413。

FileUpload 提供对上传文件的各种元数据的访问。但是,如果您只需要访问上传文件的句柄,可以使用 java.nio.file.Pathjava.io.File

如果您需要访问所有部分的上传文件,无论其名称如何,都可以使用 @RestForm(FileUpload.ALL) List<FileUpload> 来实现。

@PartType 用于帮助将请求的相应部分反序列化为所需的 Java 类型。仅当您需要为该特定参数使用特殊正文参数类型时才需要。
就像任何其他请求参数类型一样,您也可以将它们分组到容器类中。
在处理文件上传时,将文件移动到 POJO 的持久化存储(如数据库、专用文件系统或云存储)中非常重要。否则,在请求终止后将无法访问该文件。此外,如果 quarkus.http.body.delete-uploaded-files-on-end 设置为 true,Quarkus 将在发送 HTTP 响应时删除上传的文件。如果禁用该设置,文件将保留在服务器的文件系统中(在 quarkus.http.body.uploads-directory 配置选项定义的目录中),但由于上传的文件保存时使用 UUID 文件名且未保存其他元数据,因此这些文件本质上是随机文件转储。

当资源方法需要处理各种类型的 multipart 请求时,可以使用 org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput 方法类型,因为它提供了对请求所有部分的访问。

以下代码展示了一个简单示例,我们遍历部分并返回聚合数据的列表。

@Path("/test")
public static class Resource {

    @POST
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Produces(MediaType.APPLICATION_JSON)
    public List<Item> hello(MultipartFormDataInput input) throws IOException {
        Map<String, Collection<FormValue>> map = input.getValues();
        List<Item> items = new ArrayList<>();
        for (var entry : map.entrySet()) {
            for (FormValue value : entry.getValue()) {
                items.add(new Item(
                        entry.getKey(),
                        value.isFileItem() ? value.getFileItem().getFileSize() : value.getValue().length(),
                        value.getCharset(),
                        value.getFileName(),
                        value.isFileItem(),
                        value.getHeaders()));
            }

        }
        return items;
    }

    public static class Item {
        public final String name;
        public final long size;
        public final String charset;
        public final String fileName;
        public final boolean isFileItem;
        public final Map<String, List<String>> headers;

        public Item(String name, long size, String charset, String fileName, boolean isFileItem,
                Map<String, List<String>> headers) {
            this.name = name;
            this.size = size;
            this.charset = charset;
            this.fileName = fileName;
            this.isFileItem = isFileItem;
            this.headers = headers;
        }
    }
}

处理格式错误的输入

在读取 multipart 正文的过程中,Quarkus REST 会为请求的每个部分调用适当的 MessageBodyReaderMessageBodyReader。如果其中一个部分发生 IOException(例如,Jackson 无法反序列化 JSON 部分),则会抛出 org.jboss.resteasy.reactive.server.multipart.MultipartPartReadingException。如果应用程序未按异常映射中所述处理此异常,则默认返回 HTTP 400 响应。

Multipart 输出

类似地,Quarkus REST 可以生成 Multipart Form 数据,允许用户从服务器下载文件。例如,我们可以编写一个 POJO 来保存我们想要公开的信息。

import jakarta.ws.rs.core.MediaType;

import org.jboss.resteasy.reactive.PartType;
import org.jboss.resteasy.reactive.RestForm;

public class DownloadFormData {

    @RestForm
    String name;

    @RestForm
    @PartType(MediaType.APPLICATION_OCTET_STREAM)
    File file;
}

然后像这样通过资源公开此 POJO。

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("multipart")
public class Endpoint {

    @GET
    @Produces(MediaType.MULTIPART_FORM_DATA)
    @Path("file")
    public DownloadFormData getFile() {
        // return something
    }
}

此外,您还可以使用 MultipartFormDataOutput 类以编程方式附加表单部分,如下所示。

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataOutput;

@Path("multipart")
public class Endpoint {

    @GET
    @Produces(MediaType.MULTIPART_FORM_DATA)
    @Path("file")
    public MultipartFormDataOutput getFile() {
        MultipartFormDataOutput form = new MultipartFormDataOutput();
        form.addFormData("person", new Person("John"), MediaType.APPLICATION_JSON_TYPE);
        form.addFormData("status", "a status", MediaType.TEXT_PLAIN_TYPE)
                .getHeaders().putSingle("extra-header", "extra-value");
        return form;
    }
}

最后一种方法允许您向输出部分添加额外的标头。

目前,返回 Multipart 数据仅限于阻塞端点。

返回响应正文

为了返回 HTTP 响应,只需从方法中返回您想要的资源。方法的返回类型及其可选内容类型将用于决定如何将其序列化到 HTTP 响应中(有关更高级信息,请参阅协商部分)。

您可以返回任何可以从HTTP 响应读取的预定义类型,任何其他类型都将从该类型映射到 JSON

此外,还支持以下返回类型:

表 4. 附加响应正文参数类型
类型 用法

Path (路径)

给定路径指定的文件的内容。

PathPart

给定路径指定文件的部分内容。

FilePart

文件的部分内容。

AsyncFile

Vert.x AsyncFile,可以是完整内容,也可以是部分内容。

或者,您也可以返回一个反应式类型,例如 UniMultiCompletionStage,它们解析为上述类型之一。

设置其他响应属性

手动设置响应

如果您需要设置 HTTP 响应的更多属性,而不仅仅是正文,例如状态码或标头,您可以让方法从资源方法返回 org.jboss.resteasy.reactive.RestResponse。这方面的一个例子如下。

package org.acme.rest;

import java.time.Duration;
import java.time.Instant;
import java.util.Date;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.NewCookie;

import org.jboss.resteasy.reactive.RestResponse;
import org.jboss.resteasy.reactive.RestResponse.ResponseBuilder;

@Path("")
public class Endpoint {

    @GET
    public RestResponse<String> hello() {
        // HTTP OK status with text/plain content type
        return ResponseBuilder.ok("Hello, World!", MediaType.TEXT_PLAIN_TYPE)
         // set a response header
         .header("X-Cheese", "Camembert")
         // set the Expires response header to two days from now
         .expires(Date.from(Instant.now().plus(Duration.ofDays(2))))
         // send a new cookie
         .cookie(new NewCookie("Flavour", "chocolate"))
         // end of builder API
         .build();
    }
}
您也可以使用 Jakarta REST 类型 Response,但它不是强类型的实体。

使用注释

或者,如果您只需要设置静态值的状态码和/或 HTTP 标头,您可以使用 @org.jboss.resteasy.reactive.ResponseStatus 和/或 ResponseHeader。这方面的一个例子如下。

package org.acme.rest;

import org.jboss.resteasy.reactive.Header;
import org.jboss.resteasy.reactive.ResponseHeader;
import org.jboss.resteasy.reactive.ResponseStatus;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("")
public class Endpoint {

    @ResponseStatus(201)
    @ResponseHeader(name = "X-Cheese", value = "Camembert")
    @GET
    public String hello() {
        return "Hello, World!";
    }
}

重定向支持

在处理 @POST@PUT@DELETE 端点时,在执行操作后重定向到 @GET 端点是一种常见做法,以便用户可以重新加载页面而无需再次触发操作。有多种方法可以实现这一点。

使用 RestResponse

RestResponse 用作返回类型,同时确保创建正确的重定向 URI,可以按照以下示例进行。

package org.acme.rest;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.UriInfo;
import org.jboss.resteasy.reactive.RestResponse;

@Path("/fruits")
public class FruitResource {

    public static class Fruit {
        public Long id;
        public String name;
        public String description;

        public Fruit() {
        }

        public Fruit(Long id, String name, String description) {
            this.id = id;
            this.name = name;
            this.description = description;
        }
    }

    private final Map<Long, Fruit> fruits = new ConcurrentHashMap<>();
    private final AtomicLong ids = new AtomicLong(0);


    public FruitResource() {
        Fruit apple = new Fruit(ids.incrementAndGet(), "Apple", "Winter fruit");
        fruits.put(apple.id, apple);
        Fruit pinneapple = new Fruit(ids.incrementAndGet(), "Pineapple", "Tropical fruit");
        fruits.put(pinneapple.id, pinneapple);
    }

    // when invoked, this method will result in an HTTP redirect to the GET method that obtains the fruit by id
    @POST
    public RestResponse<Fruit> add(Fruit fruit, @Context UriInfo uriInfo) {
        fruit.id = ids.incrementAndGet();
        fruits.put(fruit.id, fruit);
        // seeOther results in an HTTP 303 response with the Location header set to the value of the URI
        return RestResponse.seeOther(uriInfo.getAbsolutePathBuilder().path(Long.toString(fruit.id)).build());
    }

    @GET
    @Path("{id}")
    public Fruit byId(Long id) {
        return fruits.get(id);
    }
}

使用 RedirectException

用户还可以从方法体中抛出 jakarta.ws.rs.RedirectionException,以便 Quarkus REST 执行所需的重定向。

异步/反应式支持

如果您的端点方法需要在响应之前完成异步或反应式任务,您可以将方法声明为返回 Uni 类型(来自 Mutiny),在这种情况下,当前 HTTP 请求将在您的方法之后自动挂起,直到返回的 Uni 实例解析为一个值,该值将根据先前描述的规则映射到响应。

package org.acme.rest;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import io.smallrye.mutiny.Uni;

@Path("escoffier")
public class Endpoint {

    @GET
    public Uni<Book> culinaryGuide() {
        return Book.findByIsbn("978-2081229297");
    }
}

这使您可以在获取书籍时避免阻塞事件循环线程,并允许 Quarkus 在书籍准备好发送给客户端并终止此请求之前处理更多请求。有关更多信息,请参阅执行模型文档

CompletionStage 返回类型也受支持。

流式传输支持

如果您想逐个元素地流式传输响应,可以使您的端点方法返回 Multi 类型(来自 Mutiny)。这对于流式传输文本或二进制数据特别有用。

此示例使用 Reactive Messaging HTTP,展示了如何流式传输文本数据。

package org.acme.rest;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.eclipse.microprofile.reactive.messaging.Channel;

import io.smallrye.mutiny.Multi;

@Path("logs")
public class Endpoint {

    @Inject
    @Channel("log-out")
    Multi<String> logs;

    @GET
    public Multi<String> streamLogs() {
        return logs;
    }
}
响应过滤器 **不** 会对流式响应调用,因为它们会给人一种错误印象,认为您可以设置标头或 HTTP 状态码,而这在初始响应之后是不可能的。异常映射器也不会被调用,因为响应的一部分可能已经被写入。

自定义标头和状态

如果您需要设置自定义 HTTP 标头和/或 HTTP 响应,那么您可以返回 org.jboss.resteasy.reactive.RestMulti,如下所示。

package org.acme.rest;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.eclipse.microprofile.reactive.messaging.Channel;

import io.smallrye.mutiny.Multi;
import org.jboss.resteasy.reactive.RestMulti;

@Path("logs")
public class Endpoint {

    @Inject
    @Channel("log-out")
    Multi<String> logs;

    @GET
    public Multi<String> streamLogs() {
        return RestMulti.fromMultiData(logs).status(222).header("foo", "bar").build();
    }
}

在更高级的情况下,当标头和/或状态只能从异步调用的结果中获得时,需要使用 RestMulti.fromUniResponse。这是一个这样的用例示例。

package org.acme.rest;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import java.util.List;import java.util.Map;import org.eclipse.microprofile.reactive.messaging.Channel;

import io.smallrye.mutiny.Multi;
import org.jboss.resteasy.reactive.RestMulti;

@Path("logs")
public class Endpoint {

    interface SomeService {
        Uni<SomeResponse> get();
    }

    interface SomeResponse {
        Multi<byte[]> data();

        String myHeader();
    }

    private final SomeService someService;

    public Endpoint(SomeService someService) {
        this.someService = someService;
    }

    @GET
    public Multi<String> streamLogs() {
        return RestMulti.fromUniResponse(someService.get(), SomeResponse::data, (r -> Map.of("MyHeader", List.of(r.myHeader()))));
    }
}

并发流元素处理

默认情况下,RestMulti 通过将发布者的需求信号值设置为 1 来确保项目/元素的串行/顺序。要启用多个项目的并发处理/生成,请使用 withDemand(long demand)

当需要返回多个项目且每个项目的生产需要一些时间时,即当并行/并发生产可以提高服务响应时间时,使用大于 1 的需求很有用。请注意,并发处理还需要更多资源,并对生产项目所需的服务或资源产生更高的负载。还可以考虑使用 Multi.capDemandsTo(long)Multi.capDemandsUsing(LongFunction)

下面的示例生成 5 个(JSON)字符串,但字符串在返回的 JSON 数组中的*顺序*不保证。下面的示例也适用于 JSON 对象,而不仅仅是简单类型。

package org.acme.rest;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import io.smallrye.mutiny.Multi;
import org.jboss.resteasy.reactive.RestMulti;

@Path("message-stream")
public class Endpoint {
    @GET
    public Multi<String> streamMessages() {
        Multi<String> sourceMulti = Multi
            .createBy()
            .merging()
            .streams(
                Multi.createFrom().items(
                    "message-1",
                    "message-2",
                    "message-3",
                    "message-4",
                    "message-5"
                )
            );

        return RestMulti
            .fromMultiData(sourceMulti)
            .withDemand(5)
            .build();
    }
}

示例响应,顺序是非确定性的。

"message-3"
"message-5"
"message-4"
"message-1"
"message-2"

返回多个 JSON 对象

默认情况下,如果媒体类型为 application/jsonRestMulti 会将由包装的 Multi 生成的项目/元素作为 JSON 数组返回。要返回不包含在 JSON 数组中的独立 JSON 对象,请使用 encodeAsArray(false)encodeAsArray(true) 是默认值)。请注意,以这种方式流式传输多个对象需要客户端进行稍微不同的解析,但对象可以在出现时进行解析和消耗,而无需一次性反序列化可能非常大的结果。

下面的示例生成 5 个(JSON)字符串,这些字符串不包含在数组中,如下所示。

"message-1"
"message-2"
"message-3"
"message-4"
"message-5"
package org.acme.rest;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import io.smallrye.mutiny.Multi;
import org.jboss.resteasy.reactive.RestMulti;

@Path("message-stream")
public class Endpoint {
    @GET
    public Multi<String> streamMessages() {
        Multi<String> sourceMulti = Multi
            .createBy()
            .merging()
            .streams(
                Multi.createFrom().items(
                    "message-1",
                    "message-2",
                    "message-3",
                    "message-4",
                    "message-5"
                )
            );

        return RestMulti
            .fromMultiData(sourceMulti)
            .encodeAsJsonArray(false)
            .build();
    }
}

Server-Sent Event (SSE) 支持

如果您想在响应中流式传输 JSON 对象,您可以通过注释您的端点方法 @Produces(MediaType.SERVER_SENT_EVENTS),并使用 @RestStreamElementType(MediaType.APPLICATION_JSON) 指定每个元素应序列化为 JSON

package org.acme.rest;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import org.jboss.resteasy.reactive.RestStreamElementType;

import io.smallrye.mutiny.Multi;

import org.eclipse.microprofile.reactive.messaging.Channel;

@Path("escoffier")
public class Endpoint {

    // Inject our Book channel
    @Inject
    @Channel("book-out")
    Multi<Book> books;

    @GET
    // Each element will be sent as JSON
    @RestStreamElementType(MediaType.APPLICATION_JSON)
    // by using @RestStreamElementType, we don't need to add @Produces(MediaType.SERVER_SENT_EVENTS)
    public Multi<Book> stream() {
        return books;
    }
}

有时创建自定义 SSE 消息很有用,例如,如果您需要指定 SSE 消息的 event 字段来区分各种事件类型。资源方法可以返回 Multi<jakarta.ws.rs.sse.OutboundSseEvent>,并且注入的 jakarta.ws.rs.sse.Sse 可用于创建 OutboundSseEvent 实例。

package org.acme.rest;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.sse.OutboundSseEvent;
import jakarta.ws.rs.sse.Sse;

import org.jboss.resteasy.reactive.RestStreamElementType;

import io.smallrye.mutiny.Multi;

import org.eclipse.microprofile.reactive.messaging.Channel;

@Path("escoffier")
public class Endpoint {

    @Inject
    @Channel("book-out")
    Multi<Book> books;

    @Inject
    Sse sse; (1)

    @GET
    @RestStreamElementType(MediaType.TEXT_PLAIN)
    public Multi<OutboundSseEvent> stream() {
        return books.map(book -> sse.newEventBuilder() (2)
            .name("book")  (3)
            .data(book.title) (4)
            .build());
    }
}
1 注入用于创建 OutboundSseEvent 的服务器端入口点。
2 创建一个新的出站事件构建器。
3 设置事件名称,即 SSE 消息的 event 字段的值。
4 设置数据,即 SSE 消息的 data 字段的值。

无法通过 RestMulti.fromUniResponse 来操纵返回的 HTTP 标头和状态码,因为在返回 SSE 响应时,标头和状态码不能延迟到响应可用为止。

控制 HTTP 缓存功能

Quarkus REST 提供了 @Cache@NoCache 注释,以方便处理 HTTP 缓存语义,即设置“Cache-Control”HTTP 标头。

这些注释可以放置在 Resource Method 或 Resource Class 上(在这种情况下,它适用于类中所有包含相同注释的 Resource Methods),并允许用户返回域对象,而无需显式构建 Cache-Control HTTP 标头。

虽然 @Cache 构建复杂的 Cache-Control 标头,但 @NoCache 是一种简化的表示方式,表示您不希望任何内容被缓存;即 Cache-Control: nocache

有关 Cache-Control 标头的更多信息,请参阅 RFC 7234

访问上下文对象

如果您的端点方法接受以下类型的参数,框架将为您提供许多上下文对象。

表 5. 上下文对象
类型 用法

HttpHeaders

所有请求标头。

ResourceInfo

有关当前端点方法和类的信息(需要反射)。

SecurityContext

访问当前用户和角色。

SimpleResourceInfo

有关当前端点方法和类的信息(无需反射)。

UriInfo

提供有关当前端点和应用程序 URI 的信息。

应用程序

Advanced: Current Jakarta REST application class (高级:当前 Jakarta REST 应用程序类)

配置

Advanced: Configuration about the deployed Jakarta REST application (高级:关于已部署的 Jakarta REST 应用程序的配置)

Providers

Advanced: Runtime access to Jakarta REST providers (高级:运行时访问 Jakarta REST 提供程序)

Request

Advanced: Access to the current HTTP method and Preconditions (高级:访问当前 HTTP 方法和先决条件)

ResourceContext

Advanced: access to instances of endpoints (高级:访问端点实例)

ServerRequestContext

Advanced: Quarkus REST access to the current request/response (高级:Quarkus REST 访问当前请求/响应)

Sse

Advanced: Complex SSE use-cases (高级:复杂的 SSE 用例)

HttpServerRequest

Advanced: Vert.x HTTP Request (高级:Vert.x HTTP 请求)

HttpServerResponse

Advanced: Vert.x HTTP Response (高级:Vert.x HTTP 响应)

例如,这是您返回当前登录用户名的方法。

package org.acme.rest;

import java.security.Principal;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.SecurityContext;

@Path("user")
public class Endpoint {

    @GET
    public String userName(SecurityContext security) {
        Principal user = security.getUserPrincipal();
        return user != null ? user.getName() : "<NOT LOGGED IN>";
    }
}

您还可以使用 @Inject 将这些上下文对象注入到相同类型的字段中。

package org.acme.rest;

import java.security.Principal;

import jakarta.inject.Inject;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.SecurityContext;

@Path("user")
public class Endpoint {

    @Inject
    SecurityContext security;

    @GET
    public String userName() {
        Principal user = security.getUserPrincipal();
        return user != null ? user.getName() : "<NOT LOGGED IN>";
    }
}

甚至在您的端点构造函数中。

package org.acme.rest;

import java.security.Principal;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.SecurityContext;

@Path("user")
public class Endpoint {

    SecurityContext security;

    Endpoint(SecurityContext security) {
        this.security = security;
    }

    @GET
    public String userName() {
        Principal user = security.getUserPrincipal();
        return user != null ? user.getName() : "<NOT LOGGED IN>";
    }
}

JSON 序列化

与其导入 io.quarkus:quarkus-rest,不如导入以下任一模块以获得 JSON 支持。

GAV 用法

io.quarkus:quarkus-rest-jackson

Jackson support (Jackson 支持)

io.quarkus:quarkus-rest-jsonb

JSON-B support (JSON-B 支持)

在这两种情况下,导入这些模块都将允许从 JSON 读取 HTTP 消息正文并序列化为 JSON,适用于所有未被更具体序列化类型注册的类型

Jackson 特定功能

异常处理

默认情况下,Quarkus 为 MismatchedInputException 提供了一个内置的 ExceptionMapper,该映射器在开发和测试模式下返回 HTTP 400 状态码以及有关序列化实体时出现问题的良好错误消息。

在某些情况下,需要以统一的方式处理各种 Jackson 相关异常。例如,应用程序可能需要以相同的方式处理所有 JsonMappingException。在考虑 JAX-RS / Jakarta REST 规则时,这会成为一个问题,因为 MismatchedInputException 的异常映射器 ExceptionMapper 将被用于 JsonMappingException 的用户提供的 ExceptionMapper(因为 MismatchedInputExceptionJsonMappingException 的子类型)。

这种情况的一个解决方案是配置以下内容:

quarkus.class-loading.removed-resources."io.quarkus\:quarkus-rest-jackson"=io/quarkus/resteasy/reactive/jackson/runtime/mappers/BuiltinMismatchedInputExceptionMapper.class

这实际上使 Quarkus 完全忽略了 MismatchedInputExceptionExceptionMapper

安全序列化

当与 Jackson 一起用于执行 JSON 序列化时,Quarkus REST 能够基于当前用户的角色限制要序列化的字段集。这可以通过简单地注释正在返回的 POJO 的字段(或 getter)为 @io.quarkus.resteasy.reactive.jackson.SecureField 来实现。

一个简单的例子可能是以下内容。

假设我们有一个名为 Person 的 POJO,它看起来像这样。

package org.acme.rest;

import io.quarkus.resteasy.reactive.jackson.SecureField;

public class Person {

    @SecureField(rolesAllowed = "admin")
    private final Long id;
    private final String first;
    private final String last;
    @SecureField(rolesAllowed = "${role:admin}")                 (1)
    private String address;

    public Person(Long id, String first, String last, String address) {
        this.id = id;
        this.first = first;
        this.last = last;
        this.address = address;
    }

    public Long getId() {
        return id;
    }

    public String getFirst() {
        return first;
    }

    public String getLast() {
        return last;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}
1 io.quarkus.resteasy.reactive.jackson.SecureField.rolesAllowed 属性支持属性表达式,其方式与 jakarta.annotation.security.RolesAllowed 注释完全相同。有关更多信息,请参阅授权 Web 端点指南的标准安全注释部分。

一个非常简单的 Jakarta REST 资源,它使用 Person,可以是这样的。

package org.acme.rest;

import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;

@Path("person")
public class PersonResource {

    @Path("{id}")
    @GET
    public Person getPerson(Long id) {
        return new Person(id, "foo", "bar", "Brick Lane");
    }

    @Produces(APPLICATION_JSON) (1)
    @Path("/friend/{id}")
    @GET
    public Response getPersonFriend(Long id) {
        var person = new Person(id, "foo", "bar", "Brick Lane");
        return Response.ok(person).build();
    }
}
1 @SecureField 注释仅在 Quarkus 识别出生成的媒体类型是“application/json”类型时才有效。
目前,您不能使用 @SecureField 注释来保护从返回 io.smallrye.mutiny.Multi 反应式类型的资源方法返回的数据。

所有返回用 @SecureField 注释保护的数据的资源方法都应经过测试。请确保数据按您的预期受到保护。Quarkus 始终尝试检测带有 @SecureField 注释的字段,但它可能无法推断返回类型并错过 @SecureField 注释实例。如果发生这种情况,请使用 @EnableSecureSerialization 注释在资源端点上显式启用安全序列化。

假设已为应用程序设置了安全性(有关更多详细信息,请参阅我们的指南),当具有 admin 角色的用户执行对 /person/1 的 HTTP GET 请求时,他们将收到。

{
  "id": 1,
  "first": "foo",
  "last": "bar",
  "address", "Brick Lane"
}

作为响应。

然而,任何不具有 admin 角色的用户都将收到。

{
  "first": "foo",
  "last": "bar"
}
无需其他配置即可实现此安全序列化。但是,用户可以使用 @io.quarkus.resteasy.reactive.jackson.EnableSecureSerialization@io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization 注释来选择加入或退出特定的 Jakarta REST 资源类或方法。
使用 SecureField.rolesAllowed 属性设置的配置表达式在应用程序启动期间进行验证,即使使用了 @io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization 注释也是如此。
@JsonView 支持

Jakarta REST 方法可以注释 @JsonView,以便在每个方法的基础上自定义返回的 POJO 的序列化。这可以通过示例最好地解释。

@JsonView 的典型用法是在某些方法上隐藏某些字段。为此,让我们定义两个视图。

public class Views {

    public static class Public {
    }

    public static class Private extends Public {
    }
}

假设我们有一个 User POJO,我们想在序列化期间隐藏其中一些字段。一个简单的例子是。

public class User {

    @JsonView(Views.Private.class)
    public int id;

    @JsonView(Views.Public.class)
    public String name;
}

根据返回此用户的 Jakarta REST 方法,我们可能希望从序列化中排除 id 字段。例如,您可能希望一个不安全的方法不公开此字段。在 Quarkus REST 中实现这一点的方法如下所示。

@JsonView(Views.Public.class)
@GET
@Path("/public")
public User userPublic() {
    return testUser();
}

@JsonView(Views.Private.class)
@GET
@Path("/private")
public User userPrivate() {
    return testUser();
}

userPublic 方法的结果被序列化时,由于 Public 视图不包含它,因此响应中将不包含 id 字段。而 userPrivate 的结果将如预期那样包含 id

@JsonView 也支持反序列化请求正文。例如,在以下代码中。

        @POST
        @Produces(MediaType.APPLICATION_JSON)
        @Consumes(MediaType.APPLICATION_JSON)
        public RestResponse<User> create(@JsonView(Views.Public.class) User user) {
            return RestResponse.status(CREATED, user);
        }

create 方法内部,user 将只包含 name 的值,而 id 将始终为 null(无论 JSON 请求是否包含它)。

无反射的 Jackson 序列化和反序列化

默认情况下,Jackson 使用反射来将对象转换为 JSON 或从 JSON 转换。但是,Quarkus 遵循构建时优化理念。为了实现这一目标,请将您的应用程序配置为通过使用 quarkus-rest-jackson 扩展来最小化反射。

此功能作为技术预览引入,默认禁用。

通过消除对反射的依赖,应用程序可以实现更好的性能和更低的内存消耗,尤其是在原生应用程序中,其中反射会引入开销。如果您实现了此功能,请运行测试以评估其对您应用程序的影响。

要启用此功能,请将以下配置属性设置为 true

quarkus.rest.jackson.optimization.enable-reflection-free-serializers=true.

设置此属性会将无反射优化应用于反序列化和序列化。

启用后,Quarkus 会在构建时为每个需要 JSON 转换的类生成 StdSerializerStdDeserializer 实现。然后,应用程序使用这些生成的序列化器和反序列化器处理从 REST 端点返回的对象,从而消除对反射的依赖并提高性能。

开发人员可以通过实现 ObjectMapperCustomizer 接口来进一步自定义 JSON 处理。此接口允许对 ObjectMapper 进行精细控制,允许注册自定义序列化器和反序列化器,同时确保与无反射优化兼容。如果需要其他配置,请实现 ObjectMapperCustomizer bean 并注册任何必需的模块或设置。

完全自定义的每个方法序列化/反序列化

有时您需要为每个 Jakarta REST 方法或每个 Jakarta REST 资源完全自定义 POJO 的序列化/反序列化。对于这些用例,您可以使用 REST 方法或类级别的 @io.quarkus.resteasy.reactive.jackson.CustomSerialization@io.quarkus.resteasy.reactive.jackson.CustomDeserialization 注释。这些注释允许您完全配置 com.fasterxml.jackson.databind.ObjectWriter/com.fasterxml.jackson.databind.ObjectReader

这是一个自定义 com.fasterxml.jackson.databind.ObjectWriter 的示例用例。

@CustomSerialization(UnquotedFields.class)
@GET
@Path("/invalid-use-of-custom-serializer")
public User invalidUseOfCustomSerializer() {
    return testUser();
}

其中 UnquotedFields 是一个定义如下的 BiFunction

public static class UnquotedFields implements BiFunction<ObjectMapper, Type, ObjectWriter> {

    @Override
    public ObjectWriter apply(ObjectMapper objectMapper, Type type) {
        return objectMapper.writer().without(JsonWriteFeature.QUOTE_FIELD_NAMES);
    }
}

基本上,此类所做的是强制 Jackson 不要在字段名称中包含引号。

需要注意的是,此自定义仅针对使用 @CustomSerialization(UnquotedFields.class) 的 Jakarta REST 方法的序列化执行。

遵循前面的示例,现在让我们自定义 com.fasterxml.jackson.databind.ObjectReader 来读取带有未加引号字段名称的 JSON 请求。

@CustomDeserialization(SupportUnquotedFields.class)
@POST
@Path("/use-of-custom-deserializer")
public void useOfCustomSerializer(User request) {
    // ...
}

其中 SupportUnquotedFields 是一个定义如下的 BiFunction

public static class SupportUnquotedFields implements BiFunction<ObjectMapper, Type, ObjectReader> {

    @Override
    public ObjectReader apply(ObjectMapper objectMapper, Type type) {
        return objectMapper.reader().with(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES);
    }
}

XML 序列化

要启用 XML 支持,请将 quarkus-rest-jaxb 扩展添加到您的项目中。

GAV 用法

io.quarkus:quarkus-rest-jaxb

XML support (XML 支持)

导入此模块将允许从 XML 读取 HTTP 消息正文并序列化为 XML,适用于所有未被更具体序列化类型注册的类型

JAXB Quarkus REST 扩展将自动检测在资源中使用的类,并需要 JAXB 序列化。然后,它会将这些类注册到 JAXB 消息读取器和写入器内部使用的默认 JAXBContext 中。

但是,在某些情况下,这些类会导致 JAXBContext 失败:例如,当您在不同的 Java 包中使用相同的类名时。在这些情况下,应用程序将在构建时失败并打印导致问题的 JAXB 异常,以便您可以正确修复它。或者,您也可以通过使用属性 quarkus.jaxb.exclude-classes 来排除导致问题的类。排除任何资源所需的类时,JAXB Quarkus REST 扩展将创建并缓存一个自定义 JAXBContext,其中包含被排除的类,从而导致最小的性能下降。

quarkus.jaxb.exclude-classes 属性接受一个逗号分隔的类名列表,可以是完全限定的类名或包名。包名必须以 .* 结尾,并且指定包及其子包中的所有类都将被排除。

例如,当设置 quarkus.jaxb.exclude-classes=org.acme.one.Model,org.acme.two.Model,org.acme.somemodel.* 时,以下元素将被排除。

  • org.acme.one.Model

  • org.acme.two.Model

  • org.acme.somemodel 包及其子包中的所有类。

JAXB 高级特定功能

使用 quarkus-resteasy-reactive-jaxb 扩展时,Quarkus REST 支持一些高级功能。

注入 JAXB 组件

JAXB Quarkus REST 扩展将为用户透明地序列化和反序列化请求和响应。但是,如果您需要对 JAXB 组件进行更精细(fine grain)的控制,您可以将 JAXBContext、Marshaller 或 Unmarshaller 组件注入到您的 bean 中。

@ApplicationScoped
public class MyService {

    @Inject
    JAXBContext jaxbContext;

    @Inject
    Marshaller marshaller;

    @Inject
    Unmarshaller unmarshaller;

    // ...
}

Quarkus 将自动查找所有带有 @XmlRootElement 注释的类,然后将它们绑定到 JAXB 上下文。

自定义 JAXB 配置

要自定义 JAXB 上下文和/或 Marshaller/Unmarshaller 组件的 JAXB 配置,建议的方法是定义一个类型为 io.quarkus.jaxb.runtime.JaxbContextCustomizer 的 CDI bean。

需要注册自定义模块的示例如下。

@Singleton
public class RegisterCustomModuleCustomizer implements JaxbContextCustomizer {

    // For JAXB context configuration
    @Override
    public void customizeContextProperties(Map<String, Object> properties) {

    }

    // For Marshaller configuration
    @Override
    public void customizeMarshaller(Marshaller marshaller) throws PropertyException {
        marshaller.setProperty("jaxb.formatted.output", Boolean.TRUE);
    }

    // For Unmarshaller configuration
    @Override
    public void customizeUnmarshaller(Unmarshaller unmarshaller) throws PropertyException {
        // ...
    }
}

并非必须实现所有三个方法,只需实现您需要的方法即可。

或者,您可以通过以下方式提供自己的 JAXBContext bean。

public class CustomJaxbContext {

    // Replaces the CDI producer for JAXBContext built into Quarkus
    @Singleton
    @Produces
    JAXBContext jaxbContext() {
        // ...
    }
}

请注意,如果您提供自定义 JAXB 上下文实例,您将需要注册要用于 XML 序列化的类。这意味着 Quarkus 不会使用自动发现的类来更新您的自定义 JAXB 上下文实例。

GAV 用法

io.quarkus:quarkus-rest-links

Web Links support (Web Links 支持)

导入此模块将允许通过注释您的端点资源 @InjectRestLinks 将 Web Links 注入到响应 HTTP 标头中。要声明将返回的 Web Links,您必须使用链接方法中的 @RestLink 注释。假设一个 Record 如下所示。

public class Record {

    // The class must contain/inherit either and `id` field, an `@Id` or `@RestLinkId` annotated field.
    // When resolving the id the order of preference is: `@RestLinkId` > `@Id` > `id` field.
    private int id;

    public Record() {
    }

    protected Record(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

启用 Web Links 支持的示例如下。

@Path("/records")
public class RecordsResource {

    @GET
    @RestLink(rel = "list")
    @InjectRestLinks
    public List<Record> getAll() {
        // ...
    }

    @GET
    @Path("/{id}")
    @RestLink(rel = "self")
    @InjectRestLinks(RestLinkType.INSTANCE)
    public Record get(@PathParam("id") int id) {
        // ...
    }

    @PUT
    @Path("/{id}")
    @RestLink
    @InjectRestLinks(RestLinkType.INSTANCE)
    public Record update(@PathParam("id") int id) {
        // ...
    }

    @DELETE
    @Path("/{id}")
    @RestLink
    public Record delete(@PathParam("id") int id) {
        // ...
    }
}

当使用 curl 调用上面资源中的 getAll 方法定义的 /records 端点时,您将获得 Web Links 标头。

& curl -i localhost:8080/records
Link: <https://:8080/records>; rel="list"

由于此资源不返回 Record 类型的单个实例,因此 getupdatedelete 方法的链接未注入。现在,当调用 /records/1 端点时,您将获得以下 Web Links。

& curl -i localhost:8080/records/1
Link: <https://:8080/records>; rel="list"
Link: <https://:8080/records/1>; rel="self"
Link: <https://:8080/records/1>; rel="update"
Link: <https://:8080/records/1>; rel="delete"

getupdatedelete 方法使用路径参数“id”,由于字段“id”存在于“Record”实体类型中,Web Link 会正确地将值“1”填充到返回的链接中。此外,我们还可以生成不匹配实体类型字段的路径参数的 Web Links。例如,以下方法使用路径参数“text”,而实体 Record 没有名为“text”的字段。

@Path("/records")
public class RecordsResource {

    // ...

    @GET
    @Path("/search/{text}")
    @RestLink(rel = "search records by free text")
    @InjectRestLinks
    public List<Record> search(@PathParam("text") String text) { (4)
        // ...
    }

    // ...
}

此资源的生成的 Web Link 是 Link: <https://:8080/search/{text}>; rel="search records by free text"

最后,当调用 delete 资源时,您不应看到任何 Web Links,因为 delete 方法未被 @InjectRestLinks 注释标记。

您可以通过注入 RestLinksProvider bean 来以编程方式访问 Web Links 注册表。

@Path("/records")
public class RecordsResource {

    @Inject
    RestLinksProvider linksProvider;

    // ...
}

使用此类型为 RestLinksProvider 的注入 bean,您可以使用 RestLinksProvider.getTypeLinks 方法按类型获取链接,或使用 RestLinksProvider.getInstanceLinks 方法通过具体实例获取链接。

JSON Hypertext Application Language (HAL) 支持

HAL 标准是一种表示 Web Links 的简单格式。

要启用 HAL 支持,请将 quarkus-hal 扩展添加到您的项目中。此外,由于 HAL 需要 JSON 支持,您需要添加 quarkus-rest-jsonbquarkus-rest-jackson 扩展。

GAV 用法

io.quarkus:quarkus-hal

HAL

添加扩展后,我们现在可以注释 REST 资源以生成媒体类型 application/hal+json(或使用 RestMediaType.APPLICATION_HAL_JSON)。例如。

@Path("/records")
public class RecordsResource {

    @GET
    @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON })
    @RestLink(rel = "list")
    @InjectRestLinks
    public List<Record> getAll() {
        // ...
    }

    @GET
    @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON })
    @Path("/{id}")
    @RestLink(rel = "self")
    @InjectRestLinks(RestLinkType.INSTANCE)
    public Record get(@PathParam("id") int id) {
        // ...
    }
}

现在,/records/records/{id} 端点将接受 jsonhal+json 媒体类型,以 Hal 格式打印记录。

例如,如果我们使用 curl 调用 /records 端点以返回记录列表,HAL 格式将如下所示。

& curl -H "Accept:application/hal+json" -i localhost:8080/records
{
    "_embedded": {
        "items": [
            {
                "id": 1,
                "slug": "first",
                "value": "First value",
                "_links": {
                    "self": {
                        "href": "https://:8081/records/1"
                    },
                    "list": {
                        "href": "https://:8081/records"
                    }
                }
            },
            {
                "id": 2,
                "slug": "second",
                "value": "Second value",
                "_links": {
                    "self": {
                        "href": "https://:8081/records/2"
                    },
                    "list": {
                        "href": "https://:8081/records"
                    }
                }
            }
        ]
    },
    "_links": {
        "list": {
            "href": "https://:8081/records"
        }
    }
}

当我们调用返回单个实例的资源 /records/1 时,输出是。

& curl -H "Accept:application/hal+json" -i localhost:8080/records/1
{
    "id": 1,
    "slug": "first",
    "value": "First value",
    "_links": {
        "self": {
            "href": "https://:8081/records/1"
        },
        "list": {
            "href": "https://:8081/records"
        }
    }
}

最后,您还可以通过返回 HalCollectionWrapper<T>(返回实体列表)或 HalEntityWrapper<T>(返回单个对象)以编程方式提供其他 HAL 链接,如以下示例所示。

@Path("/records")
public class RecordsResource {

    @Inject
    HalService halService;

    @GET
    @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON })
    @RestLink(rel = "list")
    public HalCollectionWrapper<Record> getAll() {
        List<Record> list = // ...
        HalCollectionWrapper<Record> halCollection = halService.toHalCollectionWrapper( list, "collectionName", Record.class);
        halCollection.addLinks(Link.fromPath("/records/1").rel("first-record").build());
        return halCollection;
    }

    @GET
    @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON })
    @Path("/{id}")
    @RestLink(rel = "self")
    @InjectRestLinks(RestLinkType.INSTANCE)
    public HalEntityWrapper<Record> get(@PathParam("id") int id) {
        Record entity = // ...
        HalEntityWrapper<Record> halEntity = halService.toHalWrapper(entity);
        halEntity.addLinks(Link.fromPath("/records/1/parent").rel("parent-record").build());
        return halEntity;
    }
}

CORS 过滤器

Cross-origin resource sharing (CORS,跨域资源共享) 是一种机制,允许从页面所属域以外的另一个域请求网页上的受限资源。

Quarkus 在 HTTP 层级别包含一个 CORS 过滤器。有关 CORS 过滤器及其用法的更多信息,请参阅 Quarkus “跨域资源共享”指南的CORS 过滤器部分。

更高级的用法

以下是一些您可能不需要立即了解的高级主题,但对于更复杂的用例可能很有用。

执行模型、阻塞、非阻塞

Quarkus REST 是使用两种主要线程类型实现的。

  • 事件循环线程:除其他外,它们负责从 HTTP 请求读取字节并将字节写回 HTTP 响应。

  • 工作线程:它们被池化,可用于卸载长时间运行的操作。

事件循环线程(也称为 IO 线程)负责以异步方式实际执行所有 IO 操作,并触发任何对这些 IO 操作完成感兴趣的侦听器。

默认情况下,Quarkus REST 将在哪个线程上运行端点方法取决于方法的签名。如果方法返回以下类型之一,则被视为非阻塞,并且默认将在 IO 线程上运行。

  • io.smallrye.mutiny.Uni

  • io.smallrye.mutiny.Multi

  • java.util.concurrent.CompletionStage

  • org.reactivestreams.Publisher

  • Kotlin suspended methods (Kotlin 挂起方法)

这种“最佳猜测”方法意味着大多数操作默认将在正确的线程上运行。如果您正在编写反应式代码,您的方法通常会返回这些类型之一,并且将在 IO 线程上执行。如果您正在编写阻塞代码,您的方法通常会直接返回结果,并且这些将在工作线程上运行。

您可以使用 @Blocking@NonBlocking 注释来覆盖此行为。这可以应用于方法、类或 jakarta.ws.rs.core.Application 级别。

下面的示例将覆盖默认行为,并始终在工作线程上运行,即使它返回 Uni

package org.acme.rest;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import io.smallrye.common.annotation.Blocking;

@Path("yawn")
public class Endpoint {

    @Blocking
    @GET
    public Uni<String> blockingHello() throws InterruptedException {
        // do a blocking operation
        Thread.sleep(1000);
        return Uni.createFrom().item("Yaaaawwwwnnnnnn…");
    }
}

大多数时候,可以使用异步/反应式方式实现相同的阻塞操作,例如使用 MutinyHibernate Reactive 或任何 Quarkus 反应式扩展

package org.acme.rest;

import java.time.Duration;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import io.smallrye.mutiny.Uni;

@Path("yawn")
public class Endpoint {

    @GET
    public Uni<String> blockingHello() throws InterruptedException {
        return Uni.createFrom().item("Yaaaawwwwnnnnnn…")
                // do a non-blocking sleep
                .onItem().delayIt().by(Duration.ofSeconds(2));
    }
}

如果方法或类被注释为 jakarta.transaction.Transactional,那么它也将被视为阻塞方法。这是因为 JTA 是一种阻塞技术,并且通常与 Hibernate 和 JDBC 等其他阻塞技术一起使用。类上的显式 @Blocking@NonBlocking 将覆盖此行为。

覆盖默认行为

如果您想覆盖默认行为,可以注释应用程序中的 jakarta.ws.rs.core.Application 子类,并使用 @Blocking@NonBlocking,这将为所有未显式注释的方法设置默认值。

行为仍可通过在类或方法级别上直接注释来覆盖,但是,所有未带注释的端点现在都将遵循默认行为,而不管其方法签名如何。

异常映射

如果您的应用程序需要在错误情况下返回非标称 HTTP 代码,最好是抛出异常,这些异常将导致框架使用 WebApplicationException 或其任何子类型来发送正确的 HTTP 响应。

package org.acme.rest;

import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.Path;

@Path("cheeses/{cheese}")
public class Endpoint {

    @GET
    public String findCheese(String cheese) {
        if(cheese == null)
            // send a 400
            throw new BadRequestException();
        if(!cheese.equals("camembert"))
            // send a 404
            throw new NotFoundException("Unknown cheese: " + cheese);
        return "Camembert is a very nice cheese";
    }
}

您可以通过配置 quarkus.log.category."WebApplicationException".level 属性来更改抛出的 WebApplicationException 异常的日志级别,如下所示。

quarkus.log.category."WebApplicationException".level=DEBUG

如果您的端点方法将调用委托给另一个不知道 Jakarta REST 的服务层,您需要一种方法将服务异常转换为 HTTP 响应,您可以使用注释在方法上的 @ServerExceptionMapper 来实现,参数为要处理的异常类型,并将该异常转换为 RestResponse(或 Uni<RestResponse<?>>)。

package org.acme.rest;

import java.util.Map;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;

import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
import org.jboss.resteasy.reactive.RestResponse;

class UnknownCheeseException extends RuntimeException {
    public final String name;

    public UnknownCheeseException(String name) {
        this.name = name;
    }
}

@ApplicationScoped
class CheeseService {
    private static final Map<String, String> cheeses =
            Map.of("camembert", "Camembert is a very nice cheese",
                   "gouda", "Gouda is acceptable too, especially with cumin");

    public String findCheese(String name) {
        String ret = cheeses.get(name);
        if(ret != null)
            return ret;
        throw new UnknownCheeseException(name);
    }
}

@Path("cheeses/{cheese}")
public class Endpoint {

    @Inject
    CheeseService cheeses;

    @ServerExceptionMapper
    public RestResponse<String> mapException(UnknownCheeseException x) {
        return RestResponse.status(Response.Status.NOT_FOUND, "Unknown cheese: " + x.name);
    }

    @GET
    public String findCheese(String cheese) {
        if(cheese == null)
            // send a 400
            throw new BadRequestException();
        return cheeses.findCheese(cheese);
    }
}

默认情况下,用 @ServerExceptionMapper 注释的方法会调用类中其他方法(如实现方法级安全所需的)的 CDI 拦截器。

但是,用户可以通过将相应的注释添加到方法来选择加入拦截器。

当将异常映射到 @ServerExceptionMapper 方法时,异常的原因通常不起作用。

然而,Java 中的某些异常类型仅作为其他异常的包装器。通常,受检查异常被包装到 RuntimeException 中,只是为了不将它们声明在方法的 throws 参数中。例如,使用 CompletionStage 将需要 CompletionException。存在许多此类异常类型,它们只是实际异常原因的包装器。

如果您希望确保即使异常类型被这些包装异常之一包装,您的异常映射器也会被调用,您可以使用 @UnwrapException 注释在异常包装类型上。

@UnwrapException
public class MyExceptionWrapper extends RuntimeException {
    public MyExceptionWrapper(Exception cause) {
        super(cause);
    }
}

如果您不控制该异常包装类型,您可以将注释放在任何类上,并将它适用的异常包装类型指定为注释参数。

@UnwrapException({CompletionException.class, RuntimeException.class})
public class Mapper {

    @ServerExceptionMapper
    public Response handleMyException(MyException x) {
        // ...
    }

}

在 REST 端点类中定义的异常映射器仅在异常在同一类中抛出时才会被调用。如果您想定义全局异常映射器,只需将其定义在 REST 端点类之外。

package org.acme.rest;

import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
import org.jboss.resteasy.reactive.RestResponse;

class ExceptionMappers {
    @ServerExceptionMapper
    public RestResponse<String> mapException(UnknownCheeseException x) {
        return RestResponse.status(Response.Status.NOT_FOUND, "Unknown cheese: " + x.name);
    }
}

您还可以以 Jakarta REST 的方式声明异常映射器。

您的异常映射器可以声明以下参数类型中的任何一种。

表 6. 异常映射器参数
类型 用法

An exception type (异常类型)

定义您要处理的异常类型。

Any of the Context objects (任何上下文对象)

ContainerRequestContext

一个上下文对象,用于访问当前请求。

它可以声明以下返回类型中的任何一种。

表 7. 异常映射器返回类型
类型 用法

RestResponse or Response

发生异常时要发送给客户端的响应。

Uni<RestResponse> or Uni<Response>

发生异常时要发送给客户端的异步响应。

发生异常时,Quarkus REST 默认不会记录它(出于安全原因)。这有时会使理解为什么调用了(或未调用)某些异常处理代码变得困难。为了让 Quarkus REST 在异常映射代码运行之前记录实际异常,可以将 org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext 日志类别设置为 DEBUG,如下所示。

quarkus.log.category."org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext".level=DEBUG

请求或响应过滤器

通过注释

您可以声明在请求处理的以下阶段调用的函数。

  • 在识别端点方法之前:预匹配请求过滤器。

  • 路由之后,但在调用端点方法之前:正常请求过滤器。

  • 调用端点方法之后:响应过滤器。

这些过滤器允许您执行各种操作,例如检查请求 URI、HTTP 方法、影响路由、查看或更改请求标头、中止请求或修改响应。

请求过滤器可以用 @ServerRequestFilter 注释声明。

import java.util.Optional;

class Filters {

    @ServerRequestFilter(preMatching = true)
    public void preMatchingFilter(ContainerRequestContext requestContext) {
        // make sure we don't lose cheese lovers
        if("yes".equals(requestContext.getHeaderString("Cheese"))) {
            requestContext.setRequestUri(URI.create("/cheese"));
        }
    }

    @ServerRequestFilter
    public Optional<RestResponse<Void>> getFilter(ContainerRequestContext ctx) {
        // only allow GET methods for now
        if(!ctx.getMethod().equals(HttpMethod.GET)) {
            return Optional.of(RestResponse.status(Response.Status.METHOD_NOT_ALLOWED));
        }
        return Optional.empty();
    }
}

请求过滤器通常在处理请求的方法所在的同一线程上执行。这意味着,如果服务请求的方法被注释为 @Blocking,那么过滤器也将运行在工作线程上。如果方法被注释为 @NonBlocking(或根本没有注释),那么过滤器也将运行在同一个事件循环线程上。

但是,如果过滤器需要在事件循环上运行,尽管服务请求的方法将在工作线程上运行,那么可以使用 @ServerRequestFilter(nonBlocking=true)。但请注意,这些过滤器需要在任何不使用该设置并且将在工作线程上运行的过滤器之前运行。

但请记住,上述信息适用于预匹配过滤器(@ServerRequestFilter(preMatching = true))。这些过滤器始终在事件循环线程上运行。

同样,响应过滤器可以用 @ServerResponseFilter 注释声明。

class Filters {
    @ServerResponseFilter
    public void getFilter(ContainerResponseContext responseContext) {
        Object entity = responseContext.getEntity();
        if(entity instanceof String) {
            // make it shout
            responseContext.setEntity(((String)entity).toUpperCase());
        }
    }
}

对于已处理的异常,也将调用此类响应过滤器。

您的过滤器可以声明以下参数类型中的任何一种。

表 8. 过滤器参数
类型 用法

Any of the Context objects (任何上下文对象)

ContainerRequestContext

一个上下文对象,用于访问当前请求。

ContainerResponseContext

一个上下文对象,用于访问当前响应。

Throwable

任何已抛出并已处理的异常,或 null(仅适用于响应过滤器)。

它可以声明以下返回类型中的任何一种。

表 9. 过滤器返回类型
类型 用法

RestResponse<?> or Response

在过滤器链继续之前要发送给客户端的响应,或 null 表示应继续过滤器链。

Optional<RestResponse<?>> or Optional<Response>

在过滤器链继续之前要发送给客户端的可选响应,或空值表示应继续过滤器链。

Uni<RestResponse<?>> or Uni<Response>

在过滤器链继续之前要发送给客户端的异步响应,或 null 表示应继续过滤器链。

您可以使用 @NameBinding 元注释来限制过滤器运行的资源方法。

Jakarta REST 的方式

可以通过提供 ContainerRequestFilterContainerResponseFilter 实现来拦截 HTTP 请求和响应。这些过滤器适用于处理与消息关联的元数据:HTTP 标头、查询参数、媒体类型和其他元数据。它们还具有中止请求处理的能力,例如,当用户没有权限访问端点时。

让我们使用 ContainerRequestFilter 为我们的服务添加日志记录功能。我们可以通过实现 ContainerRequestFilter 并使用 @Provider 注释来做到这一点。

package org.acme.rest.json;

import io.vertx.core.http.HttpServerRequest;
import org.jboss.logging.Logger;

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.UriInfo;
import jakarta.ws.rs.ext.Provider;

@Provider
public class LoggingFilter implements ContainerRequestFilter {

    private static final Logger LOG = Logger.getLogger(LoggingFilter.class);

    @Context
    UriInfo info;

    @Context
    HttpServerRequest request;

    @Override
    public void filter(ContainerRequestContext context) {

        final String method = context.getMethod();
        final String path = info.getPath();
        final String address = request.remoteAddress().toString();

        LOG.infof("Request %s %s from IP %s", method, path, address);
    }
}

现在,每当调用 REST 方法时,请求都会被记录到控制台中。

2019-06-05 12:44:26,526 INFO  [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request GET /legumes from IP 127.0.0.1
2019-06-05 12:49:19,623 INFO  [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request GET /fruits from IP 0:0:0:0:0:0:0:1
2019-06-05 12:50:44,019 INFO  [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request POST /fruits from IP 0:0:0:0:0:0:0:1
2019-06-05 12:51:04,485 INFO  [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request GET /fruits from IP 127.0.0.1

对于已处理的异常,也将调用 ContainerResponseFilter

读取器和写入器:映射实体和 HTTP 正文

无论何时,当您的端点方法返回一个对象(或者当它们返回一个RestResponse<?>Response 并带有实体)时,Quarkus REST 将寻找一种方法将其映射到 HTTP 响应正文。

同样,无论何时您的端点方法将一个对象作为参数,我们都会寻找一种方法将 HTTP 请求正文映射到该对象。

这是通过一个可插拔的 MessageBodyReaderMessageBodyWriter 接口系统来完成的,这些接口负责定义它们映射的 Java 类型、支持的媒体类型以及它们如何将 HTTP 正文转换为/从该类型的 Java 实例。

例如,如果我们有自己的 Cheese 类型在我们的端点上。

package org.acme.rest;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;

class Cheese {
    public String name;

    public Cheese(String name) {
        this.name = name;
    }
}

@Path("cheese")
public class Endpoint {

    @GET
    public Cheese sayCheese() {
        return new Cheese("Cheeeeeese");
    }

    @PUT
    public void addCheese(Cheese cheese) {
        System.err.println("Received a new cheese: " + cheese.name);
    }
}

然后我们可以通过我们的正文读取器/写入器来定义如何读取和写入它,并使用 @Provider 注释。

package org.acme.rest;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;

import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.ws.rs.ext.MessageBodyWriter;
import jakarta.ws.rs.ext.Provider;

@Provider
public class CheeseBodyHandler implements MessageBodyReader<Cheese>,
                                           MessageBodyWriter<Cheese> {

    @Override
    public boolean isWriteable(Class<?> type, Type genericType,
                               Annotation[] annotations, MediaType mediaType) {
        return type == Cheese.class;
    }

    @Override
    public void writeTo(Cheese t, Class<?> type, Type genericType,
                        Annotation[] annotations, MediaType mediaType,
                        MultivaluedMap<String, Object> httpHeaders,
                        OutputStream entityStream)
            throws IOException, WebApplicationException {
        entityStream.write(("[CheeseV1]" + t.name)
                           .getBytes(StandardCharsets.UTF_8));
    }

    @Override
    public boolean isReadable(Class<?> type, Type genericType,
                              Annotation[] annotations, MediaType mediaType) {
        return type == Cheese.class;
    }

    @Override
    public Cheese readFrom(Class<Cheese> type, Type genericType,
                            Annotation[] annotations, MediaType mediaType,
                            MultivaluedMap<String, String> httpHeaders,
                            InputStream entityStream)
            throws IOException, WebApplicationException {
        String body = new String(entityStream.readAllBytes(), StandardCharsets.UTF_8);
        if(body.startsWith("[CheeseV1]"))
            return new Cheese(body.substring(11));
        throw new IOException("Invalid cheese: " + body);
    }

}

如果您想从写入器中获得最高的性能,您可以扩展 ServerMessageBodyWriter 而不是 MessageBodyWriter,在那里您可以使用更少的反射并绕过阻塞 IO 层。

package org.acme.rest;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;

import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.ws.rs.ext.Provider;

import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo;
import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter;
import org.jboss.resteasy.reactive.server.spi.ServerRequestContext;

@Provider
public class CheeseBodyHandler implements MessageBodyReader<Cheese>,
                                           ServerMessageBodyWriter<Cheese> {

    // …

    @Override
    public boolean isWriteable(Class<?> type, ResteasyReactiveResourceInfo target,
                               MediaType mediaType) {
        return type == Cheese.class;
    }

    @Override
    public void writeResponse(Cheese t, ServerRequestContext context)
      throws WebApplicationException, IOException {
        context.serverResponse().end("[CheeseV1]" + t.name);
    }
}
您可以通过在提供程序类上添加 Consumes/Produces 注释来限制您的读取器/写入器适用的内容类型。

读取器和写入器拦截器

就像您可以拦截请求和响应一样,您也可以通过在注释了 @Provider 的类上扩展 ReaderInterceptorWriterInterceptor 来拦截读取器和写入器。

如果我们看这个端点。

package org.acme.rest;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;

@Path("cheese")
public class Endpoint {

    @GET
    public String sayCheese() {
        return "Cheeeeeese";
    }

    @PUT
    public void addCheese(String cheese) {
        System.err.println("Received a new cheese: " + cheese);
    }
}

我们可以像这样添加读取器和写入器拦截器。

package org.acme.rest;

import java.io.IOException;

import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.ext.Provider;
import jakarta.ws.rs.ext.ReaderInterceptor;
import jakarta.ws.rs.ext.ReaderInterceptorContext;
import jakarta.ws.rs.ext.WriterInterceptor;
import jakarta.ws.rs.ext.WriterInterceptorContext;

@Provider
public class CheeseIOInterceptor implements ReaderInterceptor, WriterInterceptor {

    @Override
    public void aroundWriteTo(WriterInterceptorContext context)
      throws IOException, WebApplicationException {
        System.err.println("Before writing " + context.getEntity());
        context.proceed();
        System.err.println("After writing " + context.getEntity());
    }

    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context)
      throws IOException, WebApplicationException {
        System.err.println("Before reading " + context.getGenericType());
        Object entity = context.proceed();
        System.err.println("After reading " + entity);
        return entity;
    }
}

Quarkus REST 和 REST Client 交互

在 Quarkus 中,Quarkus REST 扩展和REST Client 扩展共享相同的底层架构。这一考虑的一个重要结果是,它们共享相同的提供程序列表(在 Jakarta REST 的意义上)。

例如,如果您声明一个 WriterInterceptor,它将默认拦截服务器调用和客户端调用,这可能不是期望的行为。

但是,您可以通过添加 @ConstrainedTo(RuntimeType.CLIENT) 注释到提供程序来更改此默认行为并限制提供程序:

  • 只考虑服务器调用,通过将 @ConstrainedTo(RuntimeType.SERVER) 注释添加到您的提供者;

  • 只考虑客户端调用,通过将 @ConstrainedTo(RuntimeType.CLIENT) 注释添加到您的提供者。

参数映射

所有 请求参数 都可以声明为 String,也可以声明为以下任何类型

  1. 通过已注册的 ParamConverterProvider 可用的 ParamConverter 的类型。

  2. 原始类型。

  3. 具有接受单个 String 参数的构造函数的类型。

  4. 具有名为 valueOffromString 的静态方法,该方法接受单个 String 参数并返回该类型实例的类型。如果两种方法都存在,则使用 valueOf,除非该类型是 enum,在这种情况下将使用 fromString

  5. List<T>Set<T>SortedSet<T>,其中 T 满足上述 1、3 或 4。

对于 3 和 4,构造函数实例化优先于方法实例化。支持公共、受保护和包私有的构造函数和方法进行实例化。

以下示例说明了所有这些可能性

package org.acme.rest;

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.List;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.ext.ParamConverter;
import jakarta.ws.rs.ext.ParamConverterProvider;
import jakarta.ws.rs.ext.Provider;

import org.jboss.resteasy.reactive.RestQuery;

@Provider
class MyConverterProvider implements ParamConverterProvider {

    @Override
    public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType,
                                              Annotation[] annotations) {
        // declare a converter for this type
        if(rawType == Converter.class) {
            return (ParamConverter<T>) new MyConverter();
        }
        return null;
    }

}

// this is my custom converter
class MyConverter implements ParamConverter<Converter> {

    @Override
    public Converter fromString(String value) {
        return new Converter(value);
    }

    @Override
    public String toString(Converter value) {
        return value.value;
    }

}

// this uses a converter
class Converter {
    String value;
    Converter(String value) {
        this.value = value;
    }
}

class Constructor {
    String value;
    // this will use the constructor
    public Constructor(String value) {
        this.value = value;
    }
}

class ValueOf {
    String value;
    private ValueOf(String value) {
        this.value = value;
    }
    // this will use the valueOf method
    public static ValueOf valueOf(String value) {
        return new ValueOf(value);
    }
}

@Path("hello")
public class Endpoint {

    @Path("{converter}/{constructor}/{primitive}/{valueOf}")
    @GET
    public String conversions(Converter converter, Constructor constructor,
                              int primitive, ValueOf valueOf,
                              @RestQuery List<Constructor> list) {
        return converter + "/" + constructor + "/" + primitive
               + "/" + valueOf + "/" + list;
    }
}

分隔查询参数值

通常,使用 String 值集合来捕获在同一查询参数的多个出现中使用到的值。例如,对于以下资源方法

@Path("hello")
public static class HelloResource {

    @GET
    public String hello(@RestQuery("name") List<String> names) {
        if (names.isEmpty()) {
            return "hello world";
        } else {
            return "hello " + String.join(" ", names);
        }
    }
}

以及以下请求

GET /hello?name=foo&name=bar HTTP/1.1

names 变量将包含 foobar,响应将是 hello foo bar

然而,将单个查询参数转换为基于某些分隔符的值集合是很常见的。这就是 @org.jboss.resteasy.reactive.Separator 注释发挥作用的地方。

如果我们更新资源方法为

@Path("hello")
public static class HelloResource {

    @GET
    public String hello(@RestQuery("name") @Separator(",") List<String> names) {
        if (names.isEmpty()) {
            return "hello world";
        } else {
            return "hello " + String.join(" ", names);
        }
    }
}

并使用以下请求

GET /hello?name=foo,bar HTTP/1.1

那么响应将是 hello foo bar

处理日期

Quarkus REST 支持将 java.time.Temporal 的实现(如 java.time.LocalDateTime)用作查询、路径或表单参数。此外,它还提供了 @org.jboss.resteasy.reactive.DateFormat 注释,可用于设置自定义的预期模式。否则,将隐式使用 JDK 对每种类型的默认格式。

先决条件

HTTP 允许请求是有条件的,基于多种条件,例如

  • 上次资源修改日期

  • 资源标签,类似于资源的哈希码,用于指定其状态或版本

让我们看看如何使用 Request 上下文对象进行条件请求验证

package org.acme.rest;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.Date;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.EntityTag;
import jakarta.ws.rs.core.Request;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder;

@Path("conditional")
public class Endpoint {

    // It's important to keep our date on seconds because that's how it's sent to the
    // user in the Last-Modified header
    private Date date = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS));
    private int version = 1;
    private EntityTag tag = new EntityTag("v1");
    private String resource = "Some resource";

    @GET
    public Response get(Request request) {
        // first evaluate preconditions
        ResponseBuilder conditionalResponse = request.evaluatePreconditions(date, tag);
        if(conditionalResponse != null)
            return conditionalResponse.build();
        // preconditions are OK
        return Response.ok(resource)
                .lastModified(date)
                .tag(tag)
                .build();
    }

    @PUT
    public Response put(Request request, String body) {
        // first evaluate preconditions
        ResponseBuilder conditionalResponse = request.evaluatePreconditions(date, tag);
        if(conditionalResponse != null)
            return conditionalResponse.build();
        // preconditions are OK, we can update our resource
        resource = body;
        date = Date.from(Instant.now().truncatedTo(ChronoUnit.SECONDS));
        version++;
        tag = new EntityTag("v" + version);
        return Response.ok(resource)
                .lastModified(date)
                .tag(tag)
                .build();
    }
}

当我们第一次调用 GET /conditional 时,我们将收到此响应

HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
ETag: "v1"
Last-Modified: Wed, 09 Dec 2020 16:10:19 GMT
Content-Length: 13

Some resource

现在,如果我们想检查是否需要获取新版本,我们可以进行以下请求

GET /conditional HTTP/1.1
Host: localhost:8080
If-Modified-Since: Wed, 09 Dec 2020 16:10:19 GMT

我们将收到以下响应

HTTP/1.1 304 Not Modified

因为自该日期以来资源未被修改,这可以节省发送资源,但也可以帮助您的用户检测并发修改。例如,一个客户端想要更新资源,但另一个用户已修改它。您可以按照之前的 GET 请求进行此更新

PUT /conditional HTTP/1.1
Host: localhost:8080
If-Unmodified-Since: Wed, 09 Dec 2020 16:25:43 GMT
If-Match: v1
Content-Length: 8
Content-Type: text/plain

newstuff

如果在此期间有其他用户修改了资源,您将收到此响应

HTTP/1.1 412 Precondition Failed
ETag: "v2"
Content-Length: 0

协商

REST(和 HTTP)的主要思想之一是您的资源独立于其表示形式,并且客户端和服务器都可以自由地以任意数量的媒体类型表示其资源。这允许服务器声明支持多种表示形式,并让客户端声明它支持哪些表示形式并获得适当的服务。

以下端点支持以纯文本或 JSON 形式提供奶酪

package org.acme.rest;

import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import com.fasterxml.jackson.annotation.JsonCreator;

class Cheese {
    public String name;
    @JsonCreator
    public Cheese(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Cheese: " + name;
    }
}

@Path("negotiated")
public class Endpoint {

    @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
    @GET
    public Cheese get() {
        return new Cheese("Morbier");
    }

    @Consumes(MediaType.TEXT_PLAIN)
    @PUT
    public Cheese putString(String cheese) {
        return new Cheese(cheese);
    }

    @Consumes(MediaType.APPLICATION_JSON)
    @PUT
    public Cheese putJson(Cheese cheese) {
        return cheese;
    }
}

用户将能够使用 Accept 标头选择它获得的表示形式,在 JSON 的情况下

> GET /negotiated HTTP/1.1
> Host: localhost:8080
> Accept: application/json

< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 18
<
< {"name":"Morbier"}

对于文本

> GET /negotiated HTTP/1.1
> Host: localhost:8080
> Accept: text/plain
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Content-Length: 15
<
< Cheese: Morbier

类似地,您可以 PUT 两种不同的表示形式。JSON

> PUT /negotiated HTTP/1.1
> Host: localhost:8080
> Content-Type: application/json
> Content-Length: 16
>
> {"name": "brie"}

< HTTP/1.1 200 OK
< Content-Type: application/json;charset=UTF-8
< Content-Length: 15
<
< {"name":"brie"}

或纯文本

> PUT /negotiated HTTP/1.1
> Host: localhost:8080
> Content-Type: text/plain
> Content-Length: 9
>
> roquefort

< HTTP/1.1 200 OK
< Content-Type: application/json;charset=UTF-8
< Content-Length: 20
<
< {"name":"roquefort"}

HTTP 压缩

HTTP 响应的正文默认不进行压缩。您可以通过 quarkus.http.enable-compression=true 来启用 HTTP 压缩支持。

如果启用了压缩支持,则当满足以下条件时,响应正文将被压缩:

  • 资源方法被 @io.quarkus.vertx.http.Compressed 注释,或

  • 设置了 Content-Type 标头,并且其值是根据 quarkus.http.compress-media-types 配置的压缩媒体类型。

当满足以下条件时,响应正文永远不会被压缩:

  • 资源方法被 @io.quarkus.vertx.http.Uncompressed 注释,或

  • 未设置 Content-Type 标头。

默认情况下,以下媒体类型列表会被压缩:text/htmltext/plaintext/xmltext/csstext/javascriptapplication/javascriptapplication/jsonapplication/graphql+jsonapplication/xhtml+xml
如果客户端不支持 HTTP 压缩,则响应正文不会被压缩。

包含/排除 Jakarta REST 类

使用构建时条件

Quarkus 允许直接包含或排除 Jakarta REST 资源、提供者和功能,这得益于与 CDI Bean 相同的构建时条件。因此,各种 Jakarta REST 类可以被注解上配置文件条件(@io.quarkus.arc.profile.IfBuildProfile@io.quarkus.arc.profile.UnlessBuildProfile)和/或属性条件(io.quarkus.arc.properties.IfBuildPropertyio.quarkus.arc.properties.UnlessBuildProperty)来指示 Quarkus 在构建时应该包含哪些 Jakarta REST 类。

在以下示例中,如果启用了构建配置文件 app1,Quarkus 将仅包含 ResourceForApp1Only 资源类。

@IfBuildProfile("app1")
public class ResourceForApp1Only {

    @GET
    @Path("sayHello")
    public String sayHello() {
        return "hello";
     }
}

请注意,如果检测到 Jakarta REST 应用程序并且 getClasses() 和/或 getSingletons() 方法已被覆盖,Quarkus 将忽略构建时条件,仅考虑 Jakarta REST 应用程序中定义的内容。

使用运行时属性

Quarkus 还可以根据运行时属性的值有条件地禁用 Jakarta REST 资源,使用 @io.quarkus.resteasy.reactive.server.EndpointDisabled 注释。

在以下示例中,如果应用程序配置了 some.property 的值为 "disable",Quarkus 将在运行时排除 RuntimeResource

@EndpointDisabled(name = "some.property", stringValue = "disable")
public class RuntimeResource {

    @GET
    @Path("sayHello")
    public String sayHello() {
        return "hello";
     }
}

此功能在原生构建时不起作用。

REST 客户端

除了服务器端,Quarkus REST 还提供了一个新的 MicroProfile REST Client 实现,该实现从根本上是非阻塞的。

请注意,quarkus-resteasy-client 扩展不能与 Quarkus REST 一起使用,请使用 quarkus-rest-client

有关 REST Client 的更多信息,请参阅 REST Client 指南


1. 除非它是 URI 模板参数上下文对象

相关内容