Writing REST Services with Quarkus REST (formerly RESTEasy Reactive)
本指南解释了如何在 Quarkus 中使用 Quarkus REST 编写 REST 服务。
本文是 Quarkus REST 的参考指南。为获得更简要的介绍,请参考 Writing JSON REST services guides。 |
- What is Quarkus REST?
- Writing endpoints
- Getting started
- Terminology
- Declaring endpoints: URI mapping
- Declaring endpoints: HTTP methods
- Declaring endpoints: representation / content types
- Accessing request parameters
- [id="declaring-uri-parameters"] Declaring URI parameters
- Accessing the request body
- Handling Multipart Form data
- Returning a response body
- Setting other response properties
- Redirect support
- Async/reactive support
- Streaming support
- Concurrent stream element processing
- Returning multiple JSON objects
- Server-Sent Event (SSE) support
- Controlling HTTP Caching features
- Accessing context objects
- JSON serialisation
- XML serialisation
- Web Links support
- CORS filter
- More advanced usage
- Include/Exclude Jakarta REST classes
- REST Client
What is Quarkus REST?
Quarkus REST 是一个新的 Jakarta REST (formerly known as JAX-RS)实现,从头开始编写,可以在我们的通用 Vert.x层上运行,因此完全反应活跃,同时与 Quarkus 紧密集成,从而将大量工作转移到了构建时间。
你应该能够使用它替代任何 Jakarta REST 实现,但除此之外,它对阻塞和非阻塞端点都有出色的性能,并且在 Jakarta REST 提供的内容之上提供了许多新功能。
Writing endpoints
Getting started
将以下导入添加到你的构建文件中:
<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!";
}
}
Terminology
- REST
- Endpoint
-
Java method which is called to serve a REST call
- URL / URI (Uniform Resource Locator / Identifier)
-
Used to identify the location of REST resources (specification)
- Resource
-
Represents your domain object. This is what your API serves and modifies. Also called an
entity
in Jakarta REST. - Representation
-
How your resource is represented on the wire, can vary depending on content types
- Content type
-
Designates a particular representation (also called a media type), for example
text/plain
orapplication/json
- HTTP
-
Underlying wire protocol for routing REST calls (see HTTP specifications)
- HTTP request
-
The request part of the HTTP call, consisting of an HTTP method, a target URI, headers and an optional message body
- HTTP response
-
The response part of the HTTP call, consisting of an HTTP response status, headers and an optional message body
Declaring endpoints: URI mapping
任何使用 @Path
注释进行注释的类都可以将其方法显示为 REST 端点,前提是这些方法有一个 HTTP 方法注释(见下文)。
该 @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 parameters。
你可以使用 `@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`构建时间属性来设置根路径。
Declaring endpoints: HTTP methods
每个端点方法都必须使用以下注释之一进行注释,该注释定义了将映射到该方法的 HTTP 方法:
Annotation | Usage |
---|---|
获得资源表示,不得修改状态, idempotent ( HTTP docs) |
|
获取有关资源的元数据,类似于没有正文的 |
|
创建资源并获取到它的链接 ( HTTP docs) |
|
替换资源或创建一个资源,应该是 idempotent ( HTTP docs) |
|
删除现有资源, idempotent ( HTTP docs) |
|
获取有关资源的信息, idempotent ( HTTP docs) |
|
更新资源或创建一个资源,不是 idempotent ( HTTP docs) |
此外,你也可以通过声明一个具有 @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!";
}
}
Declaring endpoints: representation / content types
每个端点方法可能消费或产生特定的资源表示,这些表示由 HTTP Content-Type
头部指出,它反过来又包含 MIME (Media Type) 值,例如以下内容:
-
text/plain
,这是返回String
的任何端点的默认值。 -
text/html
,用于 HTML(例如 Qute templating) -
application/json
,用于 JSON REST endpoint -
text/*
,这是任何文本媒体类型的子类型通配符 -
/
,这是任何媒体类型的通配符
MediaType
类具有许多常量,你可以使用它来指向特定的预定义媒体类型。
请参阅 [negotiation] 部分了解更多信息。
Accessing request parameters
不要忘记将你的编译器配置为使用 |
端点方法可以获取以下 HTTP 请求元素:
HTTP element | Annotation | Usage |
---|---|---|
|
URI 模板参数( URI Template specification 的简化版),请参阅 URI parameters 了解更多信息。 |
|
Query parameter |
||
Header |
HTTP header 的值 |
|
Cookie |
HTTP cookie 的值 |
|
Form parameter |
||
Matrix parameter |
对于上述每个注释,您可以指定它们引用的元素的名称,否则它们将使用注释的方法参数的名称。
如果客户端发出了以下 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;
}
}
您还可以为此使用任何 Jakarta REST 注解 @PathParam
、 @QueryParam
、 @HeaderParam
、 @CookieParam
、 @FormParam
或 @MatrixParam
,但它们要求您指定参数名称。
有关更高级的用例,请参见 [parameter-mapping]。
当在 Quarkus REST 请求参数处理代码中发生异常时,出于安全原因,默认情况下不会将异常打印到日志中。这有时会让人难以理解为什么返回某些 HTTP 状态代码(因为 Jakarta REST 在各种情况下强制使用非直观的错误代码)。在这种情况下,鼓励用户将
|
Grouping parameters in a custom class
您可以将请求参数分组在一个容器类中,而不是声明为端点的使用方法参数,因此我们可以像这样重写之前的示例:
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 等库可以内省参数。 |
[id="declaring-uri-parameters"] Declaring URI parameters
您可以在路径中声明 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";
}
}
Accessing the request body
无注释的任何方法参数都将接收方法正文。除非它是 URI template parameter 或 context object,在经过从其 HTTP 展示到参数的 Java 类型的映射之后。
支持现成的下列参数类型:
Type | Usage |
---|---|
临时文件中的整个请求正文 |
|
|
未解码的整个请求正文 |
|
解码的整个请求正文 |
解码的整个请求正文 |
|
阻塞流中的请求正文 |
|
阻塞流中的请求正文 |
|
所有 Java 基元类型及其包装器类 |
Java primitive types |
Large integers and decimals. |
|
JSON value types |
|
Vert.x Buffer |
|
any other type |
您可以为更多 body parameter types 添加支持。 |
Handling Multipart Form data
如需处理使用 multipart/form-data
作为其内容类型的 HTTP 请求,您可以使用常规 @RestForm
注释,但我们有专门的类型,允许您访问部分内容,如文件或实体。我们看看其使用示例。
假定一个包含文件上传、JSON 实体和包含字符串描述的表单值的 HTTP 请求,我们可以编写以下端点:
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
参数将包含称为 description
的 HTTP 请求部分中包含的数据(因为 @RestForm
未定义值,所以使用字段名称),而 file
参数将包含有关 image
部分中上传的文件的数据HTTP 请求,person
参数将使用 JSON
body reader 读取 Person
实体。
多分部请求中每个部分的大小必须符合 quarkus.http.limits.max-form-attribute-size
的值,其默认值为 2048 字节。任何具有超出此配置大小的部分的请求都将导致 HTTP 状态代码 413。
|
如果您需要访问所有部分的所有上传文件,而不管它们的名称如何,可以使用 @RestForm(FileUpload.ALL) List<FileUpload>
来完成。
|
就像其他任何请求参数类型一样,您还可将它们分组到 container class 中。 |
处理文件上传时,非常重要的是将文件移动到您处理 POJO 的代码中的永久存储(如数据库、专用文件系统或云存储)中。否则,当请求终止时,将无法再访问此文件。此外,如果 quarkus.http.body.delete-uploaded-files-on-end
设置为 true,Quarkus 将在发送 HTTP 响应时删除已上传文件。如果禁用此设置,此文件将驻留在服务器的文件系统(在由 quarkus.http.body.uploads-directory
配置选项定义的目录中),但由于已上传文件使用 UUID 文件名保存,并且未保存其他元数据,所以这些文件基本上是随机转储的文件。
当资源方法需要处理多种类型多分部请求时,可使用
|
Handling malformed input
作为读取多分部正文的一部分,Quarkus REST 为请求的每个部分调用正确的 MessageBodyReader MessageBodyReader
。如果其中一个部分发生 IOException
(例如,如果 Jackson 无法反序列化 JSON 部分),则会抛出 org.jboss.resteasy.reactive.server.multipart.MultipartPartReadingException
。如果未按照 Exception mapping 中所述处理此异常,则默认返回 HTTP 400 响应。
Multipart output
同样,Quarkus REST 可以生成多分部表单数据,以允许用户从服务器下载文件。例如,我们可以编写一个 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;
}
}
最后一种方法允许您为输出部分添加额外的标头。
目前,返回多分部数据仅限于阻塞端点。
Returning a response body
为了返回 HTTP 响应,只需从你的方法中返回你想要加载的资源。该方法返回类型和其可选内容类型将用于决定如何将其序列化到 HTTP 响应(有关更多高级信息,请参见 [negotiation] 部分)。
你可以从 HTTP response 中读取任何预定义类型,而任何其他类型都将被映射为 from that type to JSON 。
此外,还支持以下返回类型:
Type | Usage |
---|---|
给定路径指定的文件的内容 |
|
给定路径指定的文件部分内容 |
|
文件的部分内容 |
|
Vert.x AsyncFile,可以是全部或部分 |
或者,你还可以返回一个 reactive type ,例如 Uni
、 Multi
或 CompletionStage
,它们解析为其中提到的返回类型之一。
Setting other response properties
Manually setting the response
如果你需要在 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 类型 |
Using annotations
或者,如果你只需要使用静态值设置状态代码和/或 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!";
}
}
Redirect support
在处理 @POST
、 @PUT
或 @DELETE
终结点时,在执行操作后重定向到 @GET
终结点是惯例,这样用户可以在不再次触发操作的情况下重新加载页面。有多种方法可以实现这一点。
Using 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);
}
}
Async/reactive support
如果你的终结点方法需要在回答之前完成异步或反应式任务,则可以声明你的方法返回 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 在你的书本准备好发给客户端并终止此请求之前处理更多请求。有关更多信息,请参见 Execution Model documentation 。
还支持 CompletionStage
返回类型。
Streaming support
这个示例使用 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;
}
}
响应过滤器在流式响应中调用“not”,因为它们给出了一个您能够设置标头或 HTTP 状态码的错误印象,这在初始响应后是不正确的。异常映射器也不会被调用,因为响应的一部分可能已经被写入。 |
Customizing headers and status
如果您需要设置自定义 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()))));
}
}
Concurrent stream element processing
默认情况下,“RestMulti
”确保项目/元素的串行/顺序顺序由包装“Multi
”生成,方法是将值 1 用于向发布者发出的需求信号。要启用多个项目的并发处理/生成,请使用“withDemand(long demand)
”。
当需要返回多个项目,且每个项目都需要花费一些时间来生成时,使用高于 1 的需求非常有用,即当并行/并发生成能够改善服务响应时间时。注意,并发处理还需要更多的资源,并对生成项目所需的服务或资源造成更高的负载。还要考虑使用“Multi.capDemandsTo(long)
”和“Multi.capDemandsUsing(LongFunction)
”。
下面的示例生成了 5 个 (JSON) 字符串,但无法保证返回的 JSON 数组中字符串的“order”。下面的示例适用于 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"
Returning multiple JSON objects
默认情况下,如果媒体类型为“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) support
如果您想在响应中流化 JSON 对象,则可以使用“ Server-Sent Events”,只需使用“ @Produces(MediaType.SERVER_SENT_EVENTS)
”注释您的端点方法并使用“@RestStreamElementType(MediaType.APPLICATION_JSON)
”指定每个元素应为“serialised to 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 ”字段的值。 |
由于在返回 SSE 响应时无法延迟标头和状态码,直至响应可用,因此无法通过“RestMulti.fromUniResponse
”来操作返回的 HTTP 标头和状态码。
Controlling HTTP Caching features
这些注释既可以放在资源方法上,也可以放在资源类中(在这种情况下,它适用于该类的所有资源方法,这些方法*not*”未包含相同的注释),并允许用户返回域对象,而无需处理显式构建“Cache-Control
”HTTP 标头。
有关 |
Accessing context objects
如果你的端点方法带有以下类型的参数,那么框架将提供很多上下文对象:
Type | Usage |
---|---|
All the request headers |
|
有关当前端点方法和类 (需要反射) 的信息 |
|
访问当前用户和角色 |
|
有关当前端点方法和类 (不需要反射) 的信息 |
|
提供有关当前端点和应用程序 URI 的信息 |
|
高级:当前 Jakarta REST 应用程序类 |
|
高级:已部署的 Jakarta REST 应用程序配置 |
|
高级:对 Jakarta REST 提供程序的运行时访问权 |
|
高级:访问当前 HTTP 方法和 [preconditions] |
|
高级:访问端点的实例 |
|
高级:Quarkus REST 访问当前请求/响应 |
|
Advanced: Complex SSE use-cases |
|
Advanced: Vert.x HTTP Request |
|
Advanced: Vert.x HTTP Response |
例如,下面是如何返回当前登录用户的姓名:
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 serialisation
你无需导入 io.quarkus:quarkus-rest
,而是可以导入以下任意一个模块以获得对 JSON 的支持:
GAV | Usage |
---|---|
|
|
|
在两种情况下,导入这些模块都将允许从 JSON 读取 HTTP 消息正文,并将其序列化为 JSON,供 all the types not already registered with a more specific serialisation 使用。
Advanced Jackson-specific features
在使用 quarkus-rest-jackson
扩展时,Quarkus REST 支持一些高级特性。
Secure serialization
当与 Jackson 一起用于执行 JSON 序列化时,Quarkus REST 可以根据当前用户的角色来限制要序列化的字段。只需使用 `@io.quarkus.resteasy.reactive.jackson.SecureField`来注释要返回的 POJO 的字段 (或 getter) 即可实现这一点。
一个简单的示例可以是以下内容:
假设我们有一个名为 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 注解完全相同的方式支持 property expressions。有关更多信息,请参考 Web 端点授权指南的 Standard security annotations 部分。 |
使用 Person
的极其简单的 Jakarta REST 资源可以为:
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 | 仅当 Quarkus 识别出生成的内容类型为“application/json”类型时,@SecureField 注解才会有效。 |
目前,您无法使用 @SecureField
注解来保护从返回 io.smallrye.mutiny.Multi
响应式类型的资源方法返回的数据。
应测试所有返回使用 @SecureField
注解保护的数据的资源方法。请确保数据受到您预期的保护。
假设已为应用程序设置了安全性(请参阅我们的 guide 了解更多详情),当具有 admin
角色的用户对 /person/1
执行 HTTP GET 时,他们将收到:
{
"id": 1,
"first": "foo",
"last": "bar",
"address", "Brick Lane"
}
作为响应。
但是,任何没有 admin
角色的用户都将收到:
{
"first": "foo",
"last": "bar"
}
无需为进行这种安全序列化应用其他配置。但是,用户可以使用 |
即使使用 @io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization
注解,在应用程序启动期间也会验证使用 SecureField.rolesAllowed
属性设置的配置表达式。
@JsonView support
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
方法的结果序列化时,id
字段将不会包含在响应中,因为 Public
视图不包括它。但 userPrivate
的结果在序列化时会像预期的那样包含 id
。
Reflection-free Jackson serialization
开箱即用的 Jackson 序列化通过大量使用反射对其进行内省,将对象转换为其 JSON 表示形式。然而,一般的 Quarkus 理念是尽可能避免反射,通常用构建时代码生成替换它。因此,有可能在构建时自动生成 Jackson StdSerializer
的实现,每个要转换为 JSON 的类实现一个。随后 Quarkus 可以在运行时使用这些生成的序列化器来执行 REST 端点返回对象的 JSON 序列化,而无需使用反射。
此功能默认关闭,但可以通过设置配置属性 quarkus.rest.jackson.optimization.enable-reflection-free-serializers=true
启用。
Completely customized per method serialization/deserialization
有时您需要根据每个 Jakarta REST 方法或每个 Jakarta REST 资源对 POJO 的序列化/反序列化进行完全自定义。对于此类用例,您可以在 REST 方法中或在类级别的 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 serialisation
要启用 XML 支持,请将 quarkus-rest-jaxb
扩展添加到您的项目。
GAV | Usage |
---|---|
|
导入此模块将允许从 XML 读取 HTTP 消息正文,并将其序列化为 all the types not already registered with a more specific serialisation 的 XML。
JAXB Quarkus REST 扩展将自动检测资源中使用的类并需要 JAXB 序列化。然后,它将这些类注册到默认 JAXBContext
中,后者由 JAXB 消息读取器和写入器在内部使用。
但是,在某些情况下,这些类会导致 JAXBContext
失败:例如,当你在不同的 Java 包中使用相同的类名称时。在这些情况下,应用程序将在构建时失败并打印导致问题的 JAXB 异常,以便您可以正确修复它。或者,您还可以使用属性 quarkus.jaxb.exclude-classes
排除导致问题的类。在排除任何资源所需的类时,JAXB Quarkus REST 扩展将创建一个自定义 JAXBContext
,并将包含已排除的类,这会导致最小的性能下降。
属性
|
Advanced JAXB-specific features
在使用 quarkus-resteasy-reactive-jaxb
扩展时,有一些高级功能是 Quarkus REST 支持的。
Inject JAXB components
JAXB Quarkus REST 扩展将透明地为用户序列化和反序列化请求和响应。但是,如果您需要对 JAXB 组件进行更精细的控制,您可以将 JAXBContext、Marshaller 或 Unmarshaller 组件注入到您的 Bean 中:
@ApplicationScoped
public class MyService {
@Inject
JAXBContext jaxbContext;
@Inject
Marshaller marshaller;
@Inject
Unmarshaller unmarshaller;
// ...
}
Quarkus 将自动查找所有用 |
Customize the JAXB configuration
要为 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 support
要启用 Web 链接支持,请将 quarkus-rest-links
扩展添加到项目中。
GAV | Usage |
---|---|
|
导入此模块将允许通过仅仅使用 @InjectRestLinks
注释对你的端点资源进行注释来将 Web 链接注入到响应 HTTP 头中。要声明要返回的 Web 链接,必须在关联的方法中使用 @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 链接支持的一个示例如下所示:
@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 链接头:
& curl -i localhost:8080/records
Link: <http://localhost:8080/records>; rel="list"
由于此资源并未返回类型为 Record
的单个实例,因此 get
、update
和 delete
方法的链接不会被注入。现在,在调用端点 /records/1
时,你会获得以下 Web 链接:
& curl -i localhost:8080/records/1
Link: <http://localhost:8080/records>; rel="list"
Link: <http://localhost:8080/records/1>; rel="self"
Link: <http://localhost:8080/records/1>; rel="update"
Link: <http://localhost:8080/records/1>; rel="delete"
方法 get
、update
和 delete
使用路径参数“id”,并且由于字段“id”存在于实体类型“Record”中,因此 Web 链接会在返回的链接中正确填充值“1”。除此之外,我们还可以生成与实体类型没有任何字段匹配的路径参数的 Web 链接。例如,以下方法使用路径参数“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: <[role="bare"]http://localhost:8080/search/{text}>
-
rel="search records by free text"`。
最后,在调用 delete
资源时,你不会看到任何 Web 链接,因为方法 delete
未使用 @InjectRestLinks
注释进行注释。
Programmatically access to the web links registry
只需注入 RestLinksProvider
bean,你就可以对 Web 链接注册表进行编程访问:
@Path("/records")
public class RecordsResource {
@Inject
RestLinksProvider linksProvider;
// ...
}
使用这种类型为 RestLinksProvider
的注入 bean,你可以使用 RestLinksProvider.getTypeLinks
方法按类型获取链接,或使用 RestLinksProvider.getInstanceLinks
方法按具体实例获取链接。
JSON Hypertext Application Language (HAL) support
HAL 标准是用于表示 Web 链接的简单格式。
要启用 HAL 支持,请将 quarkus-hal
扩展添加到项目中。此外,由于 HAL 需要 JSON 支持,因此你需要添加 quarkus-rest-jsonb
或 quarkus-rest-jackson
扩展。
GAV | Usage |
---|---|
|
添加扩展后,我们现在可以对 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": "http://localhost:8081/records/1"
},
"list": {
"href": "http://localhost:8081/records"
}
}
},
{
"id": 2,
"slug": "second",
"value": "Second value",
"_links": {
"self": {
"href": "http://localhost:8081/records/2"
},
"list": {
"href": "http://localhost:8081/records"
}
}
}
]
},
"_links": {
"list": {
"href": "http://localhost: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": "http://localhost:8081/records/1"
},
"list": {
"href": "http://localhost: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 filter
Cross-origin resource sharing (CORS)是一种机制,可以通过第一个资源提供的域之外的其他域请求网页上受限的资源。
Quarkus 在 HTTP 层级别包含了一个 CORS 过滤器。有关 CORS 过滤器及其用法的更多信息,请参阅 Quarkus “跨源资源共享”指南的 CORS filter 部分。
More advanced usage
以下是一些更高级的主题,你可能最初不需要了解,但对于更复杂的使用案例可能很有用。
Execution model, blocking, non-blocking
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
这种“最佳猜测”方法意味着默认情况下大多数操作都将在正确的线程上运行。如果您正在编写响应式代码,那么您的方法通常会返回以下其中一种类型,并将在 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 Reactive extensions)来实现相同的阻塞操作:
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`会覆盖此行为。
Exception mapping
如果您的应用程序需要在错误的情况下返回非标称的 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`异常的日志级别:
|
如果您的端点方法正在委派调用到另一个服务层,而该服务层不知道 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`注释的方法会 *not*运行适用于类其他方法的 CDI 拦截器(如用于实现安全性方法级别安全性的拦截器)。 但是,用户可以通过将相应注释添加到该方法来选择拦截器。 |
在将异常映射到
若不控制该异常包装器类型,则可以在任何类上放置该注解,并指定它适用的异常包装器类型作为注解参数:
|
在 REST 端点类中定义的例外映射器仅当异常在同一类中抛出时才会被调用。如果您想定义全局异常映射器,只需在 REST 端点类外部定义它们:
|
您的异常映射程序可以声明下列任何一种参数类型:
Type | Usage |
---|---|
An exception type |
定义你想要处理的异常类型 |
可访问当前请求的上下文对象 |
它可以声明以下任何返回类型:
Type | Usage |
---|---|
当出现异常时发送给客户端的响应 |
|
当出现异常时发送给客户端的异步响应 |
当发生异常时,Quarkus REST 默认不会记录日志(出于安全原因)。这有时会让人难以理解为什么调用了某些异常处理代码(或未调用)。为了让 Quarkus REST 在运行异常映射代码之前记录实际异常,可以将
|
Request or response filters
Via annotations
在请求处理的以下阶段中,你可以声明要调用的函数:
-
在标识端点方法之前:预匹配请求过滤器
-
在路由之后,但在调用端点方法之前:常规请求过滤器
-
调用 endpoint 方法后:响应过滤器
这些过滤器允许您执行各种操作,例如检查请求 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)
。但是,请注意,这些过滤器需要在不使用该设置并将在工作线程上运行的 any 过滤器之前运行。
但是,请记住,以上信息不适用于匹配前过滤器 (@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());
}
}
}
handled 异常也将调用此类响应过滤器。
您的过滤器可以声明以下任何参数类型:
Type | Usage |
---|---|
可访问当前请求的上下文对象 |
|
一个上下文对象,用于访问当前响应 |
|
任何抛出的且 handled 的异常或 |
它可以声明以下任何返回类型:
Type | Usage |
---|---|
响应会发送给客户端,而不是继续过滤器链,或者 |
|
一个可选的响应会发送给客户端,而不是继续过滤器链,或者如果过滤器链继续进行,则为空值 |
|
一个异步响应,会发送给客户端,而不是继续过滤器链,或者 |
您可以使用 |
The Jakarta REST way
可以通过分别提供 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
`ContainerResponseFilter`时, 也将 handled 例外。 |
Readers and Writers: mapping entities and HTTP bodies
每当端点方法返回对象(或者在实体中返回 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);
}
}
Reader and Writer interceptors
正如你可以截取请求和响应,还可以通过在用 @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 and REST Client interactions
在 Quarkus 中,Quarkus REST 扩展和 the REST Client extension共享相同的基础架构。这种考虑的一个重要后果是,它们共享同一列表的提供程序(在 Jakarta REST 中的单词意义)。
例如,如果你声明一个 WriterInterceptor
,它将默认拦截服务器调用和客户端调用,但这可能不是期望的行为。
但是,您可以更改此默认行为,并将一个提供程序约束为:
-
通过向您的提供程序添加
@ConstrainedTo(RuntimeType.SERVER)
注释来仅考虑 server 调用; -
通过向您的提供程序添加
@ConstrainedTo(RuntimeType.CLIENT)
注释来仅考虑 client 调用。
Parameter mapping
所有 Request Parameters都可以声明为 String
,但也可以是以下类型之一:
-
可以通过注册的
ParamConverterProvider
获得ParamConverter
的类型。 -
Primitive types.
-
具有接受单个
String
参数的构造函数的类型。 -
具有名为
valueOf`或 `fromString`且具有单个 `String
参数并返回该类型实例的静态方法的类型。如果存在两个方法,则将使用valueOf
,除非该类型是enum
,在这种情况下将使用fromString
。 -
List<T>
、Set<T>
或SortedSet<T>
,其中 `T`满足上述任何条件。
以下示例说明了所有这些可能性:
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;
}
}
Separating Query parameter values
通常,一组 `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
。
Preconditions
HTTP allows requests to be conditional,根据许多条件,例如:
-
最后一次资源修改的日期
-
资源标记,类似于资源的哈希代码,用于指定其状态或版本
让我们看看如何使用 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
如果在您的 GET
和 PUT
之间有其他用户修改了资源,您将得到此答复:
HTTP/1.1 412 Precondition Failed
ETag: "v2"
Content-Length: 0
Negotiation
REST ( and 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 Compression
默认情况下,不压缩 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
标头,则响应体永远不会被压缩。
默认情况下,压缩以下媒体类型列表: |
如果客户端不支持 HTTP 压缩,那么不会压缩响应体。 |
Include/Exclude Jakarta REST classes
Using Build time conditions
借助于构建时间条件(与 CDI bean 中的情况相同),Quarkus 能够直接包含或排除 Jakarta REST 资源、提供程序和功能。因此,可以使用配置文件条件 (@io.quarkus.arc.profile.IfBuildProfile
或 @io.quarkus.arc.profile.UnlessBuildProfile
) 和/或属性条件 (io.quarkus.arc.properties.IfBuildProperty
或 io.quarkus.arc.properties.UnlessBuildProperty
) 对各种 Jakarta REST 类进行注释,以便在构建期间向 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 应用程序中定义的内容。
Using a runtime property
Quarkus 还可根据 @io.quarkus.resteasy.reactive.server.EndpointDisabled
批注的运行时属性值有条件地禁用基于 Jakarta REST 的资源。
在下面的示例中,如果应用程序已配置 some.property
为 "disable"
,Quarkus 将在运行时排除 RuntimeResource
。
@EndpointDisabled(name = "some.property", stringValue = "disable")
public class RuntimeResource {
@GET
@Path("sayHello")
public String sayHello() {
return "hello";
}
}
此功能在使用原生构建时不起作用。
REST Client
除了服务器端以外,Quarkus REST 还附带了一个新的 MicroProfile REST 客户端实现,其核心是无阻塞的。
请注意, quarkus-resteasy-client
扩展可能不会与 Quarkus REST 搭配使用,而应使用 quarkus-rest-client
。
有关 REST 客户端的更多信息,请参阅 REST Client Guide。