使用 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 提供的基础上还增加了许多新功能。
编写端点
入门
将以下导入添加到您的构建文件中
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
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
- 端点
-
用于处理 REST 调用的 Java 方法
- URL / URI (Uniform Resource Locator / Identifier,统一资源定位符 / 标识符)
-
用于标识 REST 资源的位置(规范)
- 资源
-
代表您的领域对象。这是您的 API 提供和修改的内容。在 Jakarta REST 中也称为
实体
。 - Representation (表述)
-
您的资源在网络上传输时的表示形式,可能因内容类型而异。
- Content type (内容类型)
-
指定特定的表述(也称为媒体类型),例如
text/plain
或application/json
。 - HTTP
-
用于路由 REST 调用的底层网络协议(请参阅HTTP 规范)。
- HTTP request (HTTP 请求)
-
HTTP 调用的一部分,包含 HTTP 方法、目标 URI、标头和可选的消息正文。
- HTTP response (HTTP 响应)
-
HTTP 调用的一部分,包含 HTTP 响应状态、标头和可选的消息正文。
声明端点:URI 映射
任何带有 @Path
注释的类,只要其方法带有 HTTP 方法注释(如下所示),都可以公开为 REST 端点。
该 @Path
注释定义了这些方法将被公开的 URI 前缀。它可以为空,或包含如 rest
或 rest/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 方法。
注解 | 用法 |
---|---|
获取有关资源的元数据,类似于 |
|
创建资源并获取其链接(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/*
,这是任何文本媒体类型的子类型通配符。 -
*/*
,这是任何媒体类型的通配符。
MediaType
类有许多常量,您可以用来指向特定的预定义媒体类型。
有关更多信息,请参阅协商部分。
访问请求参数
别忘了配置您的编译器以使用 -parameters (javac) 或 <parameters> 或 <maven.compiler.parameters> (Maven) 生成参数名称信息。 |
您的端点方法可以获取以下 HTTP 请求元素:
HTTP element (HTTP 元素) | 注解 | 用法 |
---|---|---|
|
||
查询参数 |
URI 查询参数的值。 |
|
Header (标头) |
HTTP 标头的值。 |
|
Cookie (Cookie) |
HTTP Cookie 的值。 |
|
Form parameter (表单参数) |
||
Matrix parameter (矩阵参数) |
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 在各种情况下强制使用不直观的错误代码)。在这种情况下,建议将
|
在自定义类中分组参数
您可以将请求参数分组到容器类中,而不是将它们声明为端点的方法参数,因此我们可以像这样重写前面的示例。
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 类型之后。
开箱即用将支持以下参数类型:
类型 | 用法 |
---|---|
整个请求正文(作为临时文件)。 |
|
|
整个请求正文,未解码。 |
|
整个请求正文,已解码。 |
整个请求正文,已解码。 |
|
请求正文(作为阻塞流)。 |
|
请求正文(作为阻塞流)。 |
|
所有 Java 基本类型及其包装类。 |
Java primitive types (Java 基本类型) |
大整数和十进制数。 |
|
JSON value types (JSON 值类型) |
|
Vert.x Buffer。 |
|
any other type (任何其他类型) |
您可以添加对更多正文参数类型的支持。 |
处理 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.Path 或 java.io.File 。 |
如果您需要访问所有部分的上传文件,无论其名称如何,都可以使用 @RestForm(FileUpload.ALL) List<FileUpload>
来实现。
就像任何其他请求参数类型一样,您也可以将它们分组到容器类中。 |
在处理文件上传时,将文件移动到 POJO 的持久化存储(如数据库、专用文件系统或云存储)中非常重要。否则,在请求终止后将无法访问该文件。此外,如果 quarkus.http.body.delete-uploaded-files-on-end 设置为 true,Quarkus 将在发送 HTTP 响应时删除上传的文件。如果禁用该设置,文件将保留在服务器的文件系统中(在 quarkus.http.body.uploads-directory 配置选项定义的目录中),但由于上传的文件保存时使用 UUID 文件名且未保存其他元数据,因此这些文件本质上是随机文件转储。 |
当资源方法需要处理各种类型的 multipart 请求时,可以使用 以下代码展示了一个简单示例,我们遍历部分并返回聚合数据的列表。
|
处理格式错误的输入
在读取 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。
此外,还支持以下返回类型:
类型 | 用法 |
---|---|
给定路径指定的文件的内容。 |
|
给定路径指定文件的部分内容。 |
|
文件的部分内容。 |
|
Vert.x AsyncFile,可以是完整内容,也可以是部分内容。 |
或者,您也可以返回一个反应式类型,例如 Uni
、Multi
或 CompletionStage
,它们解析为上述类型之一。
设置其他响应属性
手动设置响应
如果您需要设置 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);
}
}
异步/反应式支持
如果您的端点方法需要在响应之前完成异步或反应式任务,您可以将方法声明为返回 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
返回类型也受支持。
流式传输支持
此示例使用 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/json
,RestMulti
会将由包装的 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 字段的值。 |
无法通过 |
控制 HTTP 缓存功能
这些注释可以放置在 Resource Method 或 Resource Class 上(在这种情况下,它适用于类中所有不包含相同注释的 Resource Methods),并允许用户返回域对象,而无需显式构建 Cache-Control
HTTP 标头。
有关 Cache-Control 标头的更多信息,请参阅 RFC 7234。 |
访问上下文对象
如果您的端点方法接受以下类型的参数,框架将为您提供许多上下文对象。
类型 | 用法 |
---|---|
所有请求标头。 |
|
有关当前端点方法和类的信息(需要反射)。 |
|
访问当前用户和角色。 |
|
有关当前端点方法和类的信息(无需反射)。 |
|
提供有关当前端点和应用程序 URI 的信息。 |
|
Advanced: Current Jakarta REST application class (高级:当前 Jakarta REST 应用程序类) |
|
Advanced: Configuration about the deployed Jakarta REST application (高级:关于已部署的 Jakarta REST 应用程序的配置) |
|
Advanced: Runtime access to Jakarta REST providers (高级:运行时访问 Jakarta REST 提供程序) |
|
Advanced: Access to the current HTTP method and Preconditions (高级:访问当前 HTTP 方法和先决条件) |
|
Advanced: access to instances of endpoints (高级:访问端点实例) |
|
Advanced: Quarkus REST access to the current request/response (高级:Quarkus REST 访问当前请求/响应) |
|
Advanced: Complex SSE use-cases (高级:复杂的 SSE 用例) |
|
Advanced: Vert.x HTTP Request (高级:Vert.x HTTP 请求) |
|
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 | 用法 |
---|---|
|
|
|
在这两种情况下,导入这些模块都将允许从 JSON 读取 HTTP 消息正文并序列化为 JSON,适用于所有未被更具体序列化类型注册的类型。
Jackson 特定功能
异常处理
默认情况下,Quarkus 为 MismatchedInputException
提供了一个内置的 ExceptionMapper
,该映射器在开发和测试模式下返回 HTTP 400 状态码以及有关序列化实体时出现问题的良好错误消息。
在某些情况下,需要以统一的方式处理各种 Jackson 相关异常。例如,应用程序可能需要以相同的方式处理所有 这种情况的一个解决方案是配置以下内容:
这实际上使 Quarkus 完全忽略了 |
安全序列化
当与 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 反应式类型的资源方法返回的数据。 |
所有返回用 |
假设已为应用程序设置了安全性(有关更多详细信息,请参阅我们的指南),当具有 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 转换的类生成 StdSerializer
和 StdDeserializer
实现。然后,应用程序使用这些生成的序列化器和反序列化器处理从 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 | 用法 |
---|---|
|
导入此模块将允许从 XML 读取 HTTP 消息正文并序列化为 XML,适用于所有未被更具体序列化类型注册的类型。
JAXB Quarkus REST 扩展将自动检测在资源中使用的类,并需要 JAXB 序列化。然后,它会将这些类注册到 JAXB 消息读取器和写入器内部使用的默认 JAXBContext
中。
但是,在某些情况下,这些类会导致 JAXBContext
失败:例如,当您在不同的 Java 包中使用相同的类名时。在这些情况下,应用程序将在构建时失败并打印导致问题的 JAXB 异常,以便您可以正确修复它。或者,您也可以通过使用属性 quarkus.jaxb.exclude-classes
来排除导致问题的类。排除任何资源所需的类时,JAXB Quarkus REST 扩展将创建并缓存一个自定义 JAXBContext
,其中包含被排除的类,从而导致最小的性能下降。
例如,当设置
|
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 将自动查找所有带有 |
自定义 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 上下文实例。 |
Web Links 支持
要启用 Web Links 支持,请将 quarkus-rest-links
扩展添加到您的项目中。
GAV | 用法 |
---|---|
|
导入此模块将允许通过注释您的端点资源 @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
类型的单个实例,因此 get
、update
和 delete
方法的链接未注入。现在,当调用 /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"
get
、update
和 delete
方法使用路径参数“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
注释标记。
以编程方式访问 Web Links 注册表
您可以通过注入 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-jsonb
或 quarkus-rest-jackson
扩展。
GAV | 用法 |
---|---|
|
添加扩展后,我们现在可以注释 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}
端点将接受 json
和 hal+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…");
}
}
大多数时候,可以使用异步/反应式方式实现相同的阻塞操作,例如使用 Mutiny、Hibernate 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
将覆盖此行为。
异常映射
如果您的应用程序需要在错误情况下返回非标称 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";
}
}
您可以通过配置
|
如果您的端点方法将调用委托给另一个不知道 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);
}
}
默认情况下,用 但是,用户可以通过将相应的注释添加到方法来选择加入拦截器。 |
当将异常映射到 然而,Java 中的某些异常类型仅作为其他异常的包装器。通常,受检查异常被包装到 如果您希望确保即使异常类型被这些包装异常之一包装,您的异常映射器也会被调用,您可以使用
如果您不控制该异常包装类型,您可以将注释放在任何类上,并将它适用的异常包装类型指定为注释参数。
|
在 REST 端点类中定义的异常映射器仅在异常在同一类中抛出时才会被调用。如果您想定义全局异常映射器,只需将其定义在 REST 端点类之外。
您还可以以 Jakarta REST 的方式声明异常映射器。 |
您的异常映射器可以声明以下参数类型中的任何一种。
类型 | 用法 |
---|---|
An exception type (异常类型) |
定义您要处理的异常类型。 |
Any of the Context objects (任何上下文对象) |
|
一个上下文对象,用于访问当前请求。 |
它可以声明以下返回类型中的任何一种。
类型 | 用法 |
---|---|
发生异常时要发送给客户端的响应。 |
|
发生异常时要发送给客户端的异步响应。 |
发生异常时,Quarkus REST 默认不会记录它(出于安全原因)。这有时会使理解为什么调用了(或未调用)某些异常处理代码变得困难。为了让 Quarkus REST 在异常映射代码运行之前记录实际异常,可以将
|
请求或响应过滤器
通过注释
您可以声明在请求处理的以下阶段调用的函数。
-
在识别端点方法之前:预匹配请求过滤器。
-
路由之后,但在调用端点方法之前:正常请求过滤器。
-
调用端点方法之后:响应过滤器。
这些过滤器允许您执行各种操作,例如检查请求 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();
}
}
请求过滤器通常在处理请求的方法所在的同一线程上执行。这意味着,如果服务请求的方法被注释为 但是,如果过滤器需要在事件循环上运行,尽管服务请求的方法将在工作线程上运行,那么可以使用 但请记住,上述信息不适用于预匹配过滤器( |
同样,响应过滤器可以用 @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());
}
}
}
对于已处理的异常,也将调用此类响应过滤器。
您的过滤器可以声明以下参数类型中的任何一种。
类型 | 用法 |
---|---|
Any of the Context objects (任何上下文对象) |
|
一个上下文对象,用于访问当前请求。 |
|
一个上下文对象,用于访问当前响应。 |
|
任何已抛出并已处理的异常,或 |
它可以声明以下返回类型中的任何一种。
类型 | 用法 |
---|---|
在过滤器链继续之前要发送给客户端的响应,或 |
|
在过滤器链继续之前要发送给客户端的可选响应,或空值表示应继续过滤器链。 |
|
在过滤器链继续之前要发送给客户端的异步响应,或 |
您可以使用 @NameBinding 元注释来限制过滤器运行的资源方法。 |
Jakarta REST 的方式
可以通过提供 ContainerRequestFilter
或 ContainerResponseFilter
实现来拦截 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
对于已处理的异常,也将调用 |
读取器和写入器:映射实体和 HTTP 正文
无论何时,当您的端点方法返回一个对象(或者当它们返回一个RestResponse<?>
或Response
并带有实体)时,Quarkus REST 将寻找一种方法将其映射到 HTTP 响应正文。
同样,无论何时您的端点方法将一个对象作为参数,我们都会寻找一种方法将 HTTP 请求正文映射到该对象。
这是通过一个可插拔的 MessageBodyReader
和 MessageBodyWriter
接口系统来完成的,这些接口负责定义它们映射的 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);
}
}
读取器和写入器拦截器
就像您可以拦截请求和响应一样,您也可以通过在注释了 @Provider
的类上扩展 ReaderInterceptor
或 WriterInterceptor
来拦截读取器和写入器。
如果我们看这个端点。
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)
注释添加到您的提供者。
参数映射
-
通过已注册的
ParamConverterProvider
可用的ParamConverter
的类型。 -
原始类型。
-
具有接受单个
String
参数的构造函数的类型。 -
具有名为
valueOf
或fromString
的静态方法,该方法接受单个String
参数并返回该类型实例的类型。如果两种方法都存在,则使用valueOf
,除非该类型是enum
,在这种情况下将使用fromString
。 -
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
变量将包含 foo
和 bar
,响应将是 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
。
先决条件
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/html 、text/plain 、text/xml 、text/css 、text/javascript 、application/javascript 、application/json 、application/graphql+json 和 application/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.IfBuildProperty
或 io.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 指南。