Writing REST Services with Quarkus REST (formerly RESTEasy Reactive)
本指南解释了如何在 Quarkus 中使用 Quarkus REST 编写 REST 服务。
This guide explains how to write REST Services with Quarkus REST in Quarkus.
本文是 Quarkus REST 的参考指南。为获得更简要的介绍,请参考 Writing JSON REST services guides。 This is the reference guide for Quarkus REST. For a more lightweight introduction, please refer to the Writing JSON REST services guides. |
What is Quarkus REST?
Quarkus REST 是一个新的 Jakarta REST (formerly known as JAX-RS)实现,从头开始编写,可以在我们的通用 Vert.x层上运行,因此完全反应活跃,同时与 Quarkus 紧密集成,从而将大量工作转移到了构建时间。
Quarkus REST is a new Jakarta REST (formerly known as JAX-RS) implementation written from the ground up to work on our common Vert.x layer and is thus fully reactive, while also being very tightly integrated with Quarkus and consequently moving a lot of work to build time.
你应该能够使用它替代任何 Jakarta REST 实现,但除此之外,它对阻塞和非阻塞端点都有出色的性能,并且在 Jakarta REST 提供的内容之上提供了许多新功能。
You should be able to use it in place of any Jakarta REST implementation, but on top of that it has great performance for both blocking and non-blocking endpoints, and a lot of new features on top of what Jakarta REST provides.
Writing endpoints
Getting started
将以下导入添加到你的构建文件中:
Add the following import to your build file:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
implementation("io.quarkus:quarkus-rest")
你现在可以在 `org.acme.rest.Endpoint`类中编写你的第一个端点:
You can now write your first endpoint in the org.acme.rest.Endpoint
class:
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 方法注释(见下文)。
Any class annotated with a @Path
annotation can have its methods exposed as REST endpoints,
provided they have an HTTP method annotation (see below).
该 @Path
注释定义了在这些方法将显示在其下的 URI 前缀。它可以是空的,或包含诸如 `rest`或 `rest/V1`的前缀。
That @Path
annotation defines the URI prefix under which those methods will be exposed. It can
be empty, or contain a prefix such as rest
or rest/V1
.
每个公开的端点方法依次可以有另一个 @Path
注释,该注释将其添加到其包含的类注释中。例如,它定义了一个 `rest/hello`端点:
Each exposed endpoint method can in turn have another @Path
annotation which adds to its containing
class annotation. For example, this defines a rest/hello
endpoint:
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。
See uri-parameters for more information about URI mapping.
你可以使用 `@ApplicationPath`注释设置所有 REST 端点的根路径,如下所示。
You can set the root path for all rest endpoints using the @ApplicationPath
annotation, as shown below.
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`构建时间属性来设置根路径。
This will cause all rest endpoints to be resolve relative to /api
, so the endpoint above with @Path("rest")
would
be accessible at /api/rest/
. You can also set the quarkus.rest.path
build time property to set the root path if you
don’t want to use an annotation.
Declaring endpoints: HTTP methods
每个端点方法都必须使用以下注释之一进行注释,该注释定义了将映射到该方法的 HTTP 方法:
Each endpoint method must be annotated with one of the following annotations, which defines which HTTP method will be mapped to the method:
Annotation | Usage |
---|---|
Obtain a resource representation, should not modify state, idempotent (HTTP docs) |
|
Obtain metadata about a resource, similar to |
|
Create a resource and obtain a link to it (HTTP docs) |
|
Replace a resource or create one, should be idempotent (HTTP docs) |
|
Delete an existing resource, idempotent (HTTP docs) |
|
Obtain information about a resource, idempotent (HTTP docs) |
|
Update a resource, or create one, not idempotent (HTTP docs) |
此外,你也可以通过声明一个具有 @HttpMethod
注解的注解来声明其他 HTTP 方法:
You can also declare other HTTP methods by declaring them as an annotation with the
@HttpMethod
annotation:
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) 值,例如以下内容:
Each endpoint method may consume or produce specific resource representations, which are indicated by
the HTTP Content-Type
header, which in turn contains
MIME (Media Type) values, such as the following:
-
text/plain
which is the default for any endpoint returning aString
. -
text/html
for HTML (such as with Qute templating) -
application/json
for a json -
text/*
which is a sub-type wildcard for any text media type -
/
which is a wildcard for any media type
You may annotate your endpoint class with the @Produces
or @Consumes
annotations, which
allow you to specify one or more media types that your endpoint may accept as HTTP request body
or produce as HTTP response body. Those class annotations apply to each method.
Any method may also be annotated with the @Produces
or @Consumes
annotations, in which
case they override any eventual class annotation.
MediaType
类具有许多常量,你可以使用它来指向特定的预定义媒体类型。
The MediaType
class has many constants you
can use to point to specific pre-defined media types.
请参阅 [negotiation] 部分了解更多信息。
See the [negotiation] section for more information.
Accessing request parameters
不要忘记将你的编译器配置为使用 |
don’t forget to configure your compiler to generate parameter name information with |
端点方法可以获取以下 HTTP 请求元素:
The following HTTP request elements may be obtained by your endpoint method:
HTTP element | Annotation | Usage |
---|---|---|
|
URI template parameter (simplified version of the URI Template specification), see uri-parameters for more information. |
|
Query parameter |
The value of a URI query parameter |
|
Header |
The value of an HTTP header |
|
Cookie |
The value of an HTTP cookie |
|
Form parameter |
The value of an HTTP URL-encoded FORM |
|
Matrix parameter |
The value of an URI path segment parameter |
对于上述每个注释,您可以指定它们引用的元素的名称,否则它们将使用注释的方法参数的名称。
For each of those annotations, you may specify the name of the element they refer to, otherwise, they will use the name of the annotated method parameter.
如果客户端发出了以下 HTTP 调用:
If a client made the following HTTP call:
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
那么您可以使用此端点方法获取所有各种参数:
Then you could obtain all the various parameters with this endpoint method:
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
,但它们要求您指定参数名称。
You can also use any of the Jakarta REST annotations @PathParam
,
@QueryParam
,
@HeaderParam
,
@CookieParam
,
@FormParam
or
@MatrixParam
for this,
but they require you to specify the parameter name.
有关更高级的用例,请参见 [parameter-mapping]。
See [parameter-mapping] for more advanced use-cases.
当在 Quarkus REST 请求参数处理代码中发生异常时,出于安全原因,默认情况下不会将异常打印到日志中。这有时会让人难以理解为什么返回某些 HTTP 状态代码(因为 Jakarta REST 在各种情况下强制使用非直观的错误代码)。在这种情况下,鼓励用户将 When an exception occurs in Quarkus REST request parameter handling code, the exception is not printed by default to the log (for security reasons).
This can sometimes make it hard to understand why certain HTTP status codes are returned (as the Jakarta REST mandates the use of non-intuitive error codes in various cases).
In such cases, users are encouraged to set the logging level for the
|
Grouping parameters in a custom class
您可以将请求参数分组在一个容器类中,而不是声明为端点的使用方法参数,因此我们可以像这样重写之前的示例:
You can group your request parameters in a container class instead of declaring them as method parameters to you endpoint, so we can rewrite the previous example like this:
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 is required to comply with the Jakarta REST specification so that libraries like OpenAPI can introspect the parameters. |
[id="declaring-uri-parameters"] Declaring URI parameters
您可以在路径中声明 URI 参数并使用正则表达式,因此,下述端点将服务 /hello/stef/23
和 /hello
的请求,但不会服务 /hello/stef/0x23
:
You can declare URI parameters and use regular expressions in your path, so for instance
the following endpoint will serve requests for /hello/stef/23
and /hello
but not
/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 类型的映射之后。
Any method parameter with no annotation will receive the method body.Unless it is a path-parameter or a context-objects., after it has been mapped from its HTTP representation to the Java type of the parameter.
支持现成的下列参数类型:
The following parameter types will be supported out of the box:
Type | Usage |
---|---|
The entire request body in a temporary file |
|
|
The entire request body, not decoded |
|
The entire request body, decoded |
The entire request body, decoded |
|
The request body in a blocking stream |
|
The request body in a blocking stream |
|
All Java primitives and their wrapper classes |
Java primitive types |
Large integers and decimals. |
|
JSON value types |
|
Vert.x Buffer |
|
any other type |
Will be json |
您可以为更多 body parameter types 添加支持。 |
You can add support for more readers-writers. |
Handling Multipart Form data
如需处理使用 multipart/form-data
作为其内容类型的 HTTP 请求,您可以使用常规 @RestForm
注释,但我们有专门的类型,允许您访问部分内容,如文件或实体。我们看看其使用示例。
To handle HTTP requests that have multipart/form-data
as their content type, you can use the regular
@RestForm
annotation, but we have special types
that allow you to access the parts as files or as entities.
Let us look at an example of its use.
假定一个包含文件上传、JSON 实体和包含字符串描述的表单值的 HTTP 请求,我们可以编写以下端点:
Assuming an HTTP request containing a file upload, a JSON entity and a form value containing a string description, we could write the following endpoint:
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
实体。
The description
parameter will contain the data contained in the part of HTTP request called description
(because
@RestForm
does not define a value, the field name is used),
while the file
parameter will contain data about the uploaded file in the image
part of HTTP request, and
the person
parameter will read the Person
entity using the JSON
json.
多分部请求中每个部分的大小必须符合 quarkus.http.limits.max-form-attribute-size
的值,其默认值为 2048 字节。任何具有超出此配置大小的部分的请求都将导致 HTTP 状态代码 413。
The size of every part in a multipart request must conform to the value of quarkus.http.limits.max-form-attribute-size
, for which the default is 2048 bytes.
Any request with a part of a size exceeding this configuration will result in HTTP status code 413.
|
|
如果您需要访问所有部分的所有上传文件,而不管它们的名称如何,可以使用 @RestForm(FileUpload.ALL) List<FileUpload>
来完成。
If you need access to all uploaded files for all parts regardless of their names, you can do it with @RestForm(FileUpload.ALL) List<FileUpload>
.
|
|
就像其他任何请求参数类型一样,您还可将它们分组到 container class 中。 |
Just like for any other request parameter type, you can also group them into a parameter-grouping. |
处理文件上传时,非常重要的是将文件移动到您处理 POJO 的代码中的永久存储(如数据库、专用文件系统或云存储)中。否则,当请求终止时,将无法再访问此文件。此外,如果 quarkus.http.body.delete-uploaded-files-on-end
设置为 true,Quarkus 将在发送 HTTP 响应时删除已上传文件。如果禁用此设置,此文件将驻留在服务器的文件系统(在由 quarkus.http.body.uploads-directory
配置选项定义的目录中),但由于已上传文件使用 UUID 文件名保存,并且未保存其他元数据,所以这些文件基本上是随机转储的文件。
When handling file uploads, it is very important to move the file to permanent storage (like a database, a dedicated file system or a cloud storage) in your code that handles the POJO.
Otherwise, the file will no longer be accessible when the request terminates.
Moreover, if quarkus.http.body.delete-uploaded-files-on-end
is set to true, Quarkus will delete the uploaded file when the HTTP response is sent. If the setting is disabled,
the file will reside on the file system of the server (in the directory defined by the quarkus.http.body.uploads-directory
configuration option), but as the uploaded files are saved
with a UUID file name and no additional metadata is saved, these files are essentially a random dump of files.
当资源方法需要处理多种类型多分部请求时,可使用 When a Resource method needs to handle various types of multipart requests, then the 以下代码展示一个简单的示例,我们在其中遍历部分并返回聚合数据的列表: The following code shows a simple example where we iterate over the parts and return a list of aggregated data:
|
Handling malformed input
作为读取多分部正文的一部分,Quarkus REST 为请求的每个部分调用正确的 MessageBodyReader MessageBodyReader
。如果其中一个部分发生 IOException
(例如,如果 Jackson 无法反序列化 JSON 部分),则会抛出 org.jboss.resteasy.reactive.server.multipart.MultipartPartReadingException
。如果未按照 Exception mapping 中所述处理此异常,则默认返回 HTTP 400 响应。
As part of reading the multipart body, Quarkus REST invokes the proper MessageBodyReaderMessageBodyReader
for each part of the request.
If an IOException
occurs for one of these parts (for example if Jackson was unable to deserialize a JSON part), then a org.jboss.resteasy.reactive.server.multipart.MultipartPartReadingException
is thrown.
If this exception is not handled by the application as mentioned in Exception mapping, an HTTP 400 response is returned by default.
Multipart output
同样,Quarkus REST 可以生成多分部表单数据,以允许用户从服务器下载文件。例如,我们可以编写一个 POJO,它将保存我们希望以以下形式公开的信息:
Similarly, Quarkus REST can produce Multipart Form data to allow users download files from the server. For example, we could write a POJO that will hold the information we want to expose as:
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,如下所示:
And then expose this POJO via a Resource like so:
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
类手动追加表单部分,如下所示:
Additionally, you can also manually append the parts of the form using the class MultipartFormDataOutput
as:
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;
}
}
最后一种方法允许您为输出部分添加额外的标头。
This last approach allows you adding extra headers to the output part.
目前,返回多分部数据仅限于阻塞端点。
For the time being, returning Multipart data is limited to be blocking endpoints.
Returning a response body
为了返回 HTTP 响应,只需从你的方法中返回你想要加载的资源。该方法返回类型和其可选内容类型将用于决定如何将其序列化到 HTTP 响应(有关更多高级信息,请参见 [negotiation] 部分)。
In order to return an HTTP response, simply return the resource you want from your method. The method return type and its optional content type will be used to decide how to serialise it to the HTTP response (see the [negotiation] section for more advanced information).
你可以从 HTTP response 中读取任何预定义类型,而任何其他类型都将被映射为 from that type to JSON 。
You can return any of the pre-defined types that you can read from the resource-types, and any other type will be mapped json.
此外,还支持以下返回类型:
In addition, the following return types are also supported:
Type | Usage |
---|---|
The contents of the file specified by the given path |
|
The partial contents of the file specified by the given path |
|
The partial contents of a file |
|
Vert.x AsyncFile, which can be in full, or partial |
或者,你还可以返回一个 reactive type ,例如 Uni
、 Multi
或 CompletionStage
,它们解析为其中提到的返回类型之一。
Alternately, you can also return a reactive such as Uni
,
Multi
or
CompletionStage
that resolve to one of the mentioned return types.
Setting other response properties
Manually setting the response
如果你需要在 HTTP 响应中设置比正文更多的属性,比如状态代码或标头,则可以从资源方法中让你的方法返回 org.jboss.resteasy.reactive.RestResponse
。这方面的一个示例如下所示:
If you need to set more properties on the HTTP response than just the body, such as the status code
or headers, you can make your method return org.jboss.resteasy.reactive.RestResponse
from a resource method.
An example of this could look like:
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 类型 |
You can also use the Jakarta REST type |
Using annotations
或者,如果你只需要使用静态值设置状态代码和/或 HTTP 标头,则你可以分别使用 @org.jboss.resteasy.reactive.ResponseStatus
和/或 ResponseHeader
。这方面的一个示例如下所示:
Alternatively, if you only need to set the status code and / or HTTP headers with static values, you can use @org.jboss.resteasy.reactive.ResponseStatus
and /or ResponseHeader
respectively.
An example of this could look like:
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
终结点是惯例,这样用户可以在不再次触发操作的情况下重新加载页面。有多种方法可以实现这一点。
When handling a @POST
, @PUT
or @DELETE
endpoint, it is common practice to redirect to a @GET
endpoint after the action has been performed to allow the user to reload the page without triggering the action a second time.
There are multiple ways to achieve this.
Using RestResponse
使用 RestResponse
作为返回类型,同时确保创建了正确的重定向 URI,如下例所示:
Using RestResponse
as the return type while making sure the proper redirection URI is created can be done as in the following example:
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
实例解析为值,该值将完全按照先前描述的规则映射到响应中:
If your endpoint method needs to accomplish an asynchronous or reactive task before
being able to answer, you can declare your method to return the
Uni
type (from Mutiny), in which
case the current HTTP request will be automatically suspended after your method, until
the returned Uni
instance resolves to a value,
which will be mapped to a response exactly according to the previously described rules:
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 。
This allows you to not block the event-loop thread while the book is being fetched from the database, and allows Quarkus to serve more requests until your book is ready to be sent to the client and terminate this request. See execution-model for more information.
还支持 CompletionStage
返回类型。
The CompletionStage
return
type is also supported.
Streaming support
If you want to stream your response element by element, you can make your endpoint method return a
Multi
type (from Mutiny).
This is especially useful for streaming text or binary data.
这个示例使用 Reactive Messaging HTTP 展示如何流传输文本数据:
This example, using Reactive Messaging HTTP shows how to stream text data:
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 状态码的错误印象,这在初始响应后是不正确的。异常映射器也不会被调用,因为响应的一部分可能已经被写入。 |
Response filters are not invoked on streamed responses, because they would give a false impression that you can set headers or HTTP status codes, which is not true after the initial response. Exception mappers are also not invoked because part of the response may already have been written. |
Customizing headers and status
如果您需要设置自定义 HTTP 标头和/或 HTTP 响应,则可以返回“org.jboss.resteasy.reactive.RestMulti
”,如下所示:
If you need to set custom HTTP headers and / or the HTTP response, then you can return org.jboss.resteasy.reactive.RestMulti
instead, like this:
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
”需要被使用。以下是一个使用案例示例:
In more advanced cases where the headers and / or status can only be obtained from the results of an async call, the RestMulti.fromUniResponse
needs to be used.
Here is an example of such a use case:
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)
”。
By default, RestMulti
ensures serial/sequential order of the items/elements produced by the wrapped
Multi
by using a value of 1 for the demand signaled to the publishers. To enable concurrent
processing/generation of multiple items, use withDemand(long demand)
.
当需要返回多个项目,且每个项目都需要花费一些时间来生成时,使用高于 1 的需求非常有用,即当并行/并发生成能够改善服务响应时间时。注意,并发处理还需要更多的资源,并对生成项目所需的服务或资源造成更高的负载。还要考虑使用“Multi.capDemandsTo(long)
”和“Multi.capDemandsUsing(LongFunction)
”。
Using a demand higher than 1 is useful when multiple items shall be returned and the production of each
item takes some time, i.e. when parallel/concurrent production improves the service response time. Be
aware the concurrent processing also requires more resources and puts a higher load on services or
resources that are needed to produce the items. Also consider using Multi.capDemandsTo(long)
and
Multi.capDemandsUsing(LongFunction)
.
下面的示例生成了 5 个 (JSON) 字符串,但无法保证返回的 JSON 数组中字符串的“order”。下面的示例适用于 JSON 对象,而不仅仅是简单类型。
The example below produces 5 (JSON) strings, but the order of the strings in the returned JSON array is not guaranteed. The below example also works for JSON objects and not just simple types.
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();
}
}
响应示例,顺序是不确定的。
Example response, the order is non-deterministic.
"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)
”为默认值)。请注意,以这种方式流化多个对象需要在客户端进行稍微不同的解析,但对象可以被解析和使用,而无需一次性反序列化一个可能非常大的结果。
By default, RestMulti
returns items/elements produced by the wrapped Multi
as a JSON array, if the
media-type is application/json
. To return separate JSON objects that are not wrapped in a JSON array,
use encodeAsArray(false)
(encodeAsArray(true)
is the default). Note that streaming multiple
objects this way requires a slightly different parsing on the client side, but objects can be parsed and
consumed as they appear without having to deserialize a possibly huge result at once.
下面的示例生成了 5 个 (JSON) 字符串,这些字符串未包装在数组中,如下所示:
The example below produces 5 (JSON) strings, that are not wrapped in an array, like this:
"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”即可。
If you want to stream JSON objects in your response, you can use
Server-Sent Events
by just annotating your endpoint method with
@Produces(MediaType.SERVER_SENT_EVENTS)
and specifying that each element should be json with
@RestStreamElementType(MediaType.APPLICATION_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
”实例。
Sometimes it’s useful to create a customized SSE message, for example if you need to specify the event
field of a SSE message to distinguish various event types.
A resource method may return Multi<jakarta.ws.rs.sse.OutboundSseEvent>
and an injected jakarta.ws.rs.sse.Sse
can be used to create OutboundSseEvent
instances.
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 | Inject the server-side entry point for creating `OutboundSseEvent`s. |
2 | Create a new outbound event builder. |
3 | Set the event name, i.e. the value of the event field of a SSE message. |
4 | Set the data, i.e. the value of the data field of a SSE message. |
由于在返回 SSE 响应时无法延迟标头和状态码,直至响应可用,因此无法通过“RestMulti.fromUniResponse
”来操作返回的 HTTP 标头和状态码。
Manipulation of the returned HTTP headers and status code is not possible via RestMulti.fromUniResponse
because when returning SSE responses the headers and status code cannot be delayed until the response becomes available.
Controlling HTTP Caching features
Quarkus REST provides the @Cache
and @NoCache
annotations to facilitate
handling HTTP caching semantics, i.e. setting the 'Cache-Control' HTTP header.
这些注释既可以放在资源方法上,也可以放在资源类中(在这种情况下,它适用于该类的所有资源方法,这些方法*not*”未包含相同的注释),并允许用户返回域对象,而无需处理显式构建“Cache-Control
”HTTP 标头。
These annotations can be placed either on a Resource Method or a Resource Class (in which case it applies to all Resource Methods of the class that do not contain the same annotation) and allow users
to return domain objects and not have to deal with building up the Cache-Control
HTTP header explicitly.
While @Cache
builds a complex Cache-Control
header, @NoCache
is a simplified notation to say that you don’t want anything cached; i.e. Cache-Control: nocache
.
有关 |
More information on the |
Accessing context objects
如果你的端点方法带有以下类型的参数,那么框架将提供很多上下文对象:
There are a number of contextual objects that the framework will give you, if your endpoint method takes parameters of the following type:
Type | Usage |
---|---|
All the request headers |
|
Information about the current endpoint method and class (requires reflection) |
|
Access to the current user and roles |
|
Information about the current endpoint method and class (no reflection required) |
|
Provides information about the current endpoint and application URI |
|
Advanced: Current Jakarta REST application class |
|
Advanced: Configuration about the deployed Jakarta REST application |
|
Advanced: Runtime access to Jakarta REST providers |
|
Advanced: Access to the current HTTP method and [preconditions] |
|
Advanced: access to instances of endpoints |
|
Advanced: Quarkus REST access to the current request/response |
|
Advanced: Complex SSE use-cases |
|
Advanced: Vert.x HTTP Request |
|
Advanced: Vert.x HTTP Response |
例如,下面是如何返回当前登录用户的姓名:
For example, here is how you can return the name of the currently logged-in user:
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
在相同类型的字段中注入这些上下文对象:
You can also inject those context objects using
@Inject
on fields of the same
type:
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>";
}
}
甚至可以在你的端点构造函数中:
Or even on your endpoint constructor:
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 的支持:
Instead of importing io.quarkus:quarkus-rest
, you can import either of the following modules to get support for JSON:
GAV | Usage |
---|---|
|
|
|
在两种情况下,导入这些模块都将允许从 JSON 读取 HTTP 消息正文,并将其序列化为 JSON,供 all the types not already registered with a more specific serialisation 使用。
In both cases, importing those modules will allow HTTP message bodies to be read from JSON and serialised to JSON, for resource-types.
Advanced Jackson-specific features
在使用 quarkus-rest-jackson
扩展时,Quarkus REST 支持一些高级特性。
When using the quarkus-rest-jackson
extension there are some advanced features that Quarkus REST supports.
Secure serialization
当与 Jackson 一起用于执行 JSON 序列化时,Quarkus REST 可以根据当前用户的角色来限制要序列化的字段。只需使用 `@io.quarkus.resteasy.reactive.jackson.SecureField`来注释要返回的 POJO 的字段 (或 getter) 即可实现这一点。
When used with Jackson to perform JSON serialization, Quarkus REST provides the ability to limit the set of fields that are serialized based on the roles of the current user.
This is achieved by simply annotating the fields (or getters) of the POJO being returned with @io.quarkus.resteasy.reactive.jackson.SecureField
.
一个简单的示例可以是以下内容:
A simple example could be the following:
假设我们有一个名为 Person
的 POJO,如下所示:
Assume we have a POJO named Person
which looks like so:
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 | The io.quarkus.resteasy.reactive.jackson.SecureField.rolesAllowed property supports property expressions
exactly in the same fashion the jakarta.annotation.security.RolesAllowed annotation does. For more information, please
refer to the Standard security annotations
section of the Authorization of web endpoints guide. |
使用 Person
的极其简单的 Jakarta REST 资源可以为:
A very simple Jakarta REST Resource that uses Person
could be:
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 | The @SecureField annotation is only effective when Quarkus recognizes that produced content type is the 'application/json' type. |
目前,您无法使用 @SecureField
注解来保护从返回 io.smallrye.mutiny.Multi
响应式类型的资源方法返回的数据。
Currently you cannot use the @SecureField
annotation to secure your data returned from resource methods returning the io.smallrye.mutiny.Multi
reactive type.
应测试所有返回使用 @SecureField
注解保护的数据的资源方法。请确保数据受到您预期的保护。
All resource methods returning data secured with the @SecureField
annotation should be tested.
Please make sure data are secured as you intended.
假设已为应用程序设置了安全性(请参阅我们的 guide 了解更多详情),当具有 admin
角色的用户对 /person/1
执行 HTTP GET 时,他们将收到:
Assuming security has been set up for the application (see our guide for more details), when a user with the admin
role
performs an HTTP GET on /person/1
they will receive:
{
"id": 1,
"first": "foo",
"last": "bar",
"address", "Brick Lane"
}
作为响应。
as the response.
但是,任何没有 admin
角色的用户都将收到:
Any user however that does not have the admin
role will receive:
{
"first": "foo",
"last": "bar"
}
无需为进行这种安全序列化应用其他配置。但是,用户可以使用 |
No additional configuration needs to be applied for this secure serialization to take place. However, users can use the |
即使使用 @io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization
注解,在应用程序启动期间也会验证使用 SecureField.rolesAllowed
属性设置的配置表达式。
Configuration expressions set with the SecureField.rolesAllowed
property are validated during application startup even when the @io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization
annotation is used.
@JsonView support
Jakarta REST 方法可以通过 @JsonView 进行注释,以便根据每个方法定制返回的 POJO 的序列化。最好通过示例来解释这一点。
Jakarta REST methods can be annotated with @JsonView in order to customize the serialization of the returned POJO, on a per method-basis. This is best explained with an example.
@JsonView
的典型用法是在某些方法上隐藏某些字段。有鉴于此,我们定义两个视图:
A typical use of @JsonView
is to hide certain fields on certain methods. In that vein, let’s define two views:
public class Views {
public static class Public {
}
public static class Private extends Public {
}
}
假设我们拥有 User
POJO,我们希望在序列化期间隐藏一些字段。下面是一个简单的示例:
Let’s assume we have the User
POJO on which we want to hide some field during serialization. A simple example of this is:
public class User {
@JsonView(Views.Private.class)
public int id;
@JsonView(Views.Public.class)
public String name;
}
根据返回此用户的 Jakarta REST 方法,我们可能希望从序列化中排除 id
字段。例如,您可能希望不安全的字段不公开该字段,我们可以在 Quarkus REST 中实现的方式如下例所示:
Depending on the Jakarta REST method that returns this user, we might want to exclude the id
field from serialization.
For example, you might want an insecure method to not expose this field.
The way we can achieve that in Quarkus REST is shown in the following example:
@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
。
When the result the userPublic
method is serialized, the id
field will not be contained in the response as the Public
view does not include it.
The result of userPrivate
however will include the id
as expected when serialized.
Reflection-free Jackson serialization
开箱即用的 Jackson 序列化通过大量使用反射对其进行内省,将对象转换为其 JSON 表示形式。然而,一般的 Quarkus 理念是尽可能避免反射,通常用构建时代码生成替换它。因此,有可能在构建时自动生成 Jackson StdSerializer
的实现,每个要转换为 JSON 的类实现一个。随后 Quarkus 可以在运行时使用这些生成的序列化器来执行 REST 端点返回对象的 JSON 序列化,而无需使用反射。
Out-of-the-box Jackson serialization converts objects into their JSON representation by introspecting them through a heavy use of reflection. However, the general Quarkus philosophy is to avoid reflection as much as possible, often replacing it with build time code generation. For this reason it is possible to automatically generate at build time implementations of the Jackson StdSerializer
, one for each class to be converted in JSON. These generated serializers can be subsequently used by Quarkus at runtime to perform the JSON serialization of the objects returned by a REST endpoint without any use of reflection.
此功能默认关闭,但可以通过设置配置属性 quarkus.rest.jackson.optimization.enable-reflection-free-serializers=true
启用。
This feature is turned off by default, but it can be enabled by setting the configuration property 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
。
There are times when you need to completely customize the serialization/deserialization of a POJO on a per Jakarta REST method basis or on a per Jakarta REST resource basis. For such use cases, you can use the @io.quarkus.resteasy.reactive.jackson.CustomSerialization
and @io.quarkus.resteasy.reactive.jackson.CustomDeserialization
annotations in the REST method or in the REST resource at class level. These annotations allow you to fully configure the com.fasterxml.jackson.databind.ObjectWriter
/com.fasterxml.jackson.databind.ObjectReader
.
以下是如何自定义 com.fasterxml.jackson.databind.ObjectWriter
的示例用例:
Here is an example use case to customize the com.fasterxml.jackson.databind.ObjectWriter
:
@CustomSerialization(UnquotedFields.class)
@GET
@Path("/invalid-use-of-custom-serializer")
public User invalidUseOfCustomSerializer() {
return testUser();
}
其中 UnquotedFields
是一个 BiFunction
,定义如下:
where UnquotedFields
is a BiFunction
defined as so:
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 不在字段名称中包含引号。
Essentially what this class does is force Jackson to not include quotes in the field names.
值得注意的是,此自定义仅对使用 @CustomSerialization(UnquotedFields.class)
的 Jakarta REST 方法的序列化执行。
It is important to note that this customization is only performed for the serialization of the Jakarta REST methods that use @CustomSerialization(UnquotedFields.class)
.
遵循前面的示例,现在让我们自定义 com.fasterxml.jackson.databind.ObjectReader
以读取带有未加引号字段名称的 JSON 请求:
Following the previous example, let’s now customize the com.fasterxml.jackson.databind.ObjectReader
to read JSON requests with unquoted field names:
@CustomDeserialization(SupportUnquotedFields.class)
@POST
@Path("/use-of-custom-deserializer")
public void useOfCustomSerializer(User request) {
// ...
}
其中 SupportUnquotedFields
是一个 BiFunction
,定义如下:
where SupportUnquotedFields
is a BiFunction
defined as so:
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
扩展添加到您的项目。
To enable XML support, add the quarkus-rest-jaxb
extension to your project.
GAV | Usage |
---|---|
|
导入此模块将允许从 XML 读取 HTTP 消息正文,并将其序列化为 all the types not already registered with a more specific serialisation 的 XML。
Importing this module will allow HTTP message bodies to be read from XML and serialised to XML, for resource-types.
JAXB Quarkus REST 扩展将自动检测资源中使用的类并需要 JAXB 序列化。然后,它将这些类注册到默认 JAXBContext
中,后者由 JAXB 消息读取器和写入器在内部使用。
The JAXB Quarkus REST extension will automatically detect the classes that are used in the resources and require JAXB serialization. Then, it will register these classes into the default JAXBContext
which is internally used by the JAXB message reader and writer.
但是,在某些情况下,这些类会导致 JAXBContext
失败:例如,当你在不同的 Java 包中使用相同的类名称时。在这些情况下,应用程序将在构建时失败并打印导致问题的 JAXB 异常,以便您可以正确修复它。或者,您还可以使用属性 quarkus.jaxb.exclude-classes
排除导致问题的类。在排除任何资源所需的类时,JAXB Quarkus REST 扩展将创建一个自定义 JAXBContext
,并将包含已排除的类,这会导致最小的性能下降。
However, in some situations, these classes cause the JAXBContext
to fail: for example, when you’re using the same class name in different java packages. In these cases, the application will fail at build time and print the JAXB exception that caused the issue, so you can properly fix it. Alternatively, you can also exclude the classes that cause the issue by using the property quarkus.jaxb.exclude-classes
. When excluding classes that are required by any resource, the JAXB Quarkus REST extension will create and cache a custom JAXBContext
that will include the excluded class, causing a minimal performance degradance.
属性 The property 例如,在设置 For instance, when setting
|
Advanced JAXB-specific features
在使用 quarkus-resteasy-reactive-jaxb
扩展时,有一些高级功能是 Quarkus REST 支持的。
When using the quarkus-resteasy-reactive-jaxb
extension there are some advanced features that Quarkus REST supports.
Inject JAXB components
JAXB Quarkus REST 扩展将透明地为用户序列化和反序列化请求和响应。但是,如果您需要对 JAXB 组件进行更精细的控制,您可以将 JAXBContext、Marshaller 或 Unmarshaller 组件注入到您的 Bean 中:
The JAXB Quarkus REST extension will serialize and unserialize requests and responses transparently for users. However, if you need finer grain control over JAXB components, you can inject either the JAXBContext, Marshaller, or Unmarshaller components into your beans:
@ApplicationScoped
public class MyService {
@Inject
JAXBContext jaxbContext;
@Inject
Marshaller marshaller;
@Inject
Unmarshaller unmarshaller;
// ...
}
Quarkus 将自动查找所有用 Quarkus will automatically find all the classes annotated with |
Customize the JAXB configuration
要为 JAXB 上下文和/或 Marshaller/Unmarshaller 组件自定义 JAXB 配置,建议的方法是定义类型为 io.quarkus.jaxb.runtime.JaxbContextCustomizer
的 CDI Bean。
To customize the JAXB configuration for either the JAXB context, and/or the Marshaller/Unmarshaller components, the suggested approach is to define a CDI bean of type io.quarkus.jaxb.runtime.JaxbContextCustomizer
.
需要注册自定义模块的示例如下:
An example where a custom module needs to be registered would look like so:
@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 {
// ...
}
}
只需实施三种方法中的一个,而不是所有三种方法,这取决于你需要采用哪种。 It’s not necessary to implement all three methods, but only the want you need. |
或者,可以通过执行以下操作提供你自己的 JAXBContext
bean:
Alternatively, you can provide your own JAXBContext
bean by doing:
public class CustomJaxbContext {
// Replaces the CDI producer for JAXBContext built into Quarkus
@Singleton
@Produces
JAXBContext jaxbContext() {
// ...
}
}
请注意,如果你提供了自定义 JAXB 上下文实例,则需要为你希望用于 XML 序列化的类注册这些类。这意味着 Quarkus 不会使用自动发现的类来更新你的自定义 JAXB 上下文实例。
Note that if you provide your custom JAXB context instance, you will need to register the classes you want to use for the XML serialization. This means that Quarkus will not update your custom JAXB context instance with the auto-discovered classes.
Web Links support
要启用 Web 链接支持,请将 quarkus-rest-links
扩展添加到项目中。
To enable Web Links support, add the quarkus-rest-links
extension to your project.
GAV | Usage |
---|---|
|
导入此模块将允许通过仅仅使用 @InjectRestLinks
注释对你的端点资源进行注释来将 Web 链接注入到响应 HTTP 头中。要声明要返回的 Web 链接,必须在关联的方法中使用 @RestLink
注释。假设 Record
如下所示:
Importing this module will allow injecting web links into the response HTTP headers by just annotating your endpoint resources with the @InjectRestLinks
annotation. To declare the web links that will be returned, you must use the @RestLink
annotation in the linked methods.
Assuming a Record
looks like:
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 链接支持的一个示例如下所示:
An example of enabling Web Links support would look like:
@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 链接头:
When calling the endpoint /records
which is defined by the method getAll
within the above resource using curl, you would get the web links header:
& curl -i localhost:8080/records
Link: <http://localhost:8080/records>; rel="list"
由于此资源并未返回类型为 Record
的单个实例,因此 get
、update
和 delete
方法的链接不会被注入。现在,在调用端点 /records/1
时,你会获得以下 Web 链接:
As this resource does not return a single instance of type Record
, the links for the methods get
, update
, and delete
are not injected. Now, when calling the endpoint /records/1
, you would get the following web links:
& 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”的字段:
The get
, update
, and delete
methods use the path param "id" and as the field "id" exists in the entity type "Record", the web link properly populates the value "1" in the returned links. In addition to this, we can also generate web links with path params that do not match with any field of the entity type. For example, the following method is using a path param "text" and the entity Record does not have any field named "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"`。
The generated web link for this resource is Link: <[role="bare"]http://localhost:8080/search/{text}>; rel="search records by free text"
.
最后,在调用 delete
资源时,你不会看到任何 Web 链接,因为方法 delete
未使用 @InjectRestLinks
注释进行注释。
Finally, when calling the delete
resource, you should not see any web links as the method delete
is not annotated with the @InjectRestLinks
annotation.
Programmatically access to the web links registry
只需注入 RestLinksProvider
bean,你就可以对 Web 链接注册表进行编程访问:
You can programmatically have access to the web links registry just by injecting the RestLinksProvider
bean:
@Path("/records")
public class RecordsResource {
@Inject
RestLinksProvider linksProvider;
// ...
}
使用这种类型为 RestLinksProvider
的注入 bean,你可以使用 RestLinksProvider.getTypeLinks
方法按类型获取链接,或使用 RestLinksProvider.getInstanceLinks
方法按具体实例获取链接。
Using this injected bean of type RestLinksProvider
, you can get the links by type using the method RestLinksProvider.getTypeLinks
or get the links by a concrete instance using the method RestLinksProvider.getInstanceLinks
.
JSON Hypertext Application Language (HAL) support
HAL 标准是用于表示 Web 链接的简单格式。
The HAL standard is a simple format to represent web links.
要启用 HAL 支持,请将 quarkus-hal
扩展添加到项目中。此外,由于 HAL 需要 JSON 支持,因此你需要添加 quarkus-rest-jsonb
或 quarkus-rest-jackson
扩展。
To enable the HAL support, add the quarkus-hal
extension to your project. Also, as HAL needs JSON support, you need to add either the quarkus-rest-jsonb
or the quarkus-rest-jackson
extension.
GAV | Usage |
---|---|
|
添加扩展后,我们现在可以对 REST 资源进行注释以生成媒体类型 application/hal+json
(或使用 RestMediaType.APPLICATION_HAL_JSON)。例如:
After adding the extensions, we can now annotate the REST resources to produce the media type application/hal+json
(or use RestMediaType.APPLICATION_HAL_JSON). For example:
@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 格式打印记录。
Now, the endpoints /records
and /records/{id}
will accept the media type both json
and hal+json
to print the records in Hal format.
例如,如果我们使用 curl 调用 /records
端点以返回记录列表,则 HAL 格式如下所示:
For example, if we invoke the /records
endpoint using curl to return a list of records, the HAL format will look like as follows:
& 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
时,输出如下:
When we call a resource /records/1
that returns only one instance, then the output is:
& 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 链接,如下面的示例中所述:
Finally, you can also provide additional HAL links programmatically in your resource just by returning either HalCollectionWrapper<T>
(to return a list of entities) or HalEntityWrapper<T>
(to return a single object) as described in the following example:
@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)是一种机制,可以通过第一个资源提供的域之外的其他域请求网页上受限的资源。
Cross-origin resource sharing (CORS) is a mechanism that allows restricted resources on a web page to be requested from another domain outside the domain from which the first resource was served.
Quarkus 在 HTTP 层级别包含了一个 CORS 过滤器。有关 CORS 过滤器及其用法的更多信息,请参阅 Quarkus “跨源资源共享”指南的 CORS filter 部分。
Quarkus includes a CORS filter at the HTTP layer level. For more information about the CORS filters and their usage, see the CORS filter section of the Quarkus "Cross-origin resource sharing" guide.
More advanced usage
以下是一些更高级的主题,你可能最初不需要了解,但对于更复杂的使用案例可能很有用。
Here are some more advanced topics that you may not need to know about initially, but could prove useful for more complex use cases.
Execution model, blocking, non-blocking
Quarkus REST 是使用两种主要线程类型实现的:
Quarkus REST is implemented using two main thread types:
-
Event-loop threads: which are responsible, among other things, for reading bytes from the HTTP request and writing bytes back to the HTTP response
-
Worker threads: they are pooled and can be used to offload long-running operations
事件循环线程(也称为 IO 线程)负责以异步方式执行所有 IO 操作,并触发对这些 IO 操作的完成感兴趣的任何侦听器。
The event-loop threads (also called IO threads) are responsible for actually performing all the IO operations in an asynchronous way, and to trigger any listener interested in the completion of those IO operations.
默认情况下,取决于方法的签名,Quarkus REST 将在该签名下运行端点方法。如果方法返回以下任一类型,则认为它是非阻塞的,并且默认情况下将在 IO 线程上运行:
By default, the thread Quarkus REST will run endpoint methods on depends on the signature of the method. If a method returns one of the following types then it is considered non-blocking, and will be run on the IO thread by default:
-
io.smallrye.mutiny.Uni
-
io.smallrye.mutiny.Multi
-
java.util.concurrent.CompletionStage
-
org.reactivestreams.Publisher
-
Kotlin
suspended
methods
这种“最佳猜测”方法意味着默认情况下大多数操作都将在正确的线程上运行。如果您正在编写响应式代码,那么您的方法通常会返回以下其中一种类型,并将在 IO 线程上执行。如果您正在编写阻塞代码,那么您的方法通常会直接返回结果,并将它们在工作者线程上运行。
This 'best guess' approach means most operations will run on the correct thread by default. If you are writing reactive code, your method will generally return one of these types and will be executed on the IO thread. If you are writing blocking code, your methods will usually return the result directly, and these will be run on a worker thread.
可以使用 @Blocking
和 @NonBlocking
注释来覆盖此行为。这可以在方法、类或`jakarta.ws.rs.core.Application`级别应用。
You can override this behaviour using the
@Blocking
and
@NonBlocking
annotations. This can be applied at the method, class or jakarta.ws.rs.core.Application
level.
下面的示例将覆盖默认行为,即使它返回`Uni`,也始终在工作线程上运行。
The example below will override the default behaviour and always run on a worker thread, even though it returns a 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)来实现相同的阻塞操作:
Most of the time, there are ways to achieve the same blocking operations in an asynchronous/reactive way, using Mutiny, Hibernate Reactive or any of the Quarkus Reactive extensions for example:
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`会覆盖此行为。
If a method or class is annotated with jakarta.transaction.Transactional
then it will also be treated as a blocking
method. This is because JTA is a blocking technology, and is generally used with other blocking technology such as
Hibernate and JDBC. An explicit @Blocking
or @NonBlocking
on the class will override this behaviour.
Overriding the default behaviour
如果您要覆盖默认行为,可以在应用程序中使用 `@Blocking`或 `@NonBlocking`注释一个 `jakarta.ws.rs.core.Application`子类,这将为没有显式注释的每个方法设置默认值。
If you want to override the default behavior, you can annotate a jakarta.ws.rs.core.Application
subclass in your application with @Blocking
or @NonBlocking
,
and this will set the default for every method that does not have an explicit annotation.
不过,仍然可以通过直接注释类或方法级别来覆盖行为,但是,现在没有注释的所有端点都将遵循默认值,无论其方法签名如何。
Behavior can still be overridden on a class or method level by annotating them directly, however, all endpoints without an annotation will now follow the default, no matter their method signature.
Exception mapping
如果您的应用程序需要在错误的情况下返回非标称的 HTTP 代码,最佳方法是引发异常,这将导致框架使用 WebApplicationException
或其任何子类型发送正确的 HTTP 响应:
If your application needs to return non-nominal HTTP codes in error cases, the best is
to throw exceptions that will result in the proper HTTP response being sent by the
framework using WebApplicationException
or any of its subtypes:
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`异常的日志级别: You can change the log level of the thrown
|
如果您的端点方法正在委派调用到另一个服务层,而该服务层不知道 Jakarta REST,则您需要一种方法来将服务异常转换为 HTTP 响应,您可以通过使用 @ServerExceptionMapper
注释在方法上执行此操作,该注释带有一个您要处理的异常类型的参数,并将该异常转换为 RestResponse
(或 Uni<RestResponse<?>>
):
If your endpoint method is delegating calls to another service layer which
does not know of Jakarta REST, you need a way to turn service exceptions to an
HTTP response, and you can do that using the
@ServerExceptionMapper
annotation on a method, with one parameter of the exception type you want to handle, and turning
that exception into a RestResponse
(or a
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 拦截器(如用于实现安全性方法级别安全性的拦截器)。 By default, methods annotated with 但是,用户可以通过将相应注释添加到该方法来选择拦截器。 Users however can opt into interceptors by adding the corresponding annotations to the method. |
在将异常映射到 `@ServerExceptionMapper`方法时,通常不会涉及异常的原因。 When mapping an exception to a 但是,Java 中的一些异常类型仅用作其他异常的包装器。通常,将异常检查包装到 However, some exception types in Java only serve as wrappers for other exceptions. Often, checked exceptions are wrapped into 若希望确保异常映射器在自身异常类型中被调用,即使它被其中一个包装器异常包装,也可以在异常包装器类型上使用 If you wish to make sure your exception mapper is called for your exception type even when it is wrapped by one of those wrapper exceptions, you can use
若不控制该异常包装器类型,则可以在任何类上放置该注解,并指定它适用的异常包装器类型作为注解参数: If you don’t control that exception wrapper type, you can place the annotation on any class and specify the exception wrapper types it applies to as annotation parameter:
|
在 REST 端点类中定义的例外映射器仅当异常在同一类中抛出时才会被调用。如果您想定义全局异常映射器,只需在 REST 端点类外部定义它们: Εxception mappers defined in REST endpoint classes will only be called if the exception is thrown in the same class. If you want to define global exception mappers, simply define them outside a REST endpoint class:
You can also declare exception mappers in the Jakarta REST way. |
您的异常映射程序可以声明下列任何一种参数类型:
Your exception mapper may declare any of the following parameter types:
Type | Usage |
---|---|
An exception type |
Defines the exception type you want to handle |
Any of the context-objects |
|
A context object to access the current request |
它可以声明以下任何返回类型:
It may declare any of the following return types:
Type | Usage |
---|---|
The response to send to the client when the exception occurs |
|
An asynchronous response to send to the client when the exception occurs |
当发生异常时,Quarkus REST 默认不会记录日志(出于安全原因)。这有时会让人难以理解为什么调用了某些异常处理代码(或未调用)。为了让 Quarkus REST 在运行异常映射代码之前记录实际异常,可以将 When an exception occurs, Quarkus REST does not log it by default (for security reasons).
This can sometimes make it hard to understand why certain exception handling code was invoked (or not invoked).
To make Quarkus REST log the actual exception before an exception mapping code is run the
|
Request or response filters
Via annotations
在请求处理的以下阶段中,你可以声明要调用的函数:
You can declare functions that are invoked in the following phases of the request processing:
-
Before the endpoint method is identified: pre-matching request filter
-
After routing, but before the endpoint method is called: normal request filter
-
After the endpoint method is called: response filter
这些过滤器允许您执行各种操作,例如检查请求 URI、HTTP 方法、影响路由、查看或更改请求头、中止请求或修改响应。
These filters allow you to do various things such as examine the request URI, HTTP method, influence routing, look or change request headers, abort the request, or modify the response.
可以使用 @ServerRequestFilter
注释声明请求过滤器:
Request filters can be declared with the
@ServerRequestFilter
annotation:
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
对该方法进行注释(或根本不进行注释),则这些过滤器也会在同一事件循环线程上运行。
Request filters are usually executed on the same thread that the method that handles the request will be executed.
That means that if the method servicing the request is annotated with @Blocking
, then the filters will also be run
on the worker thread.
If the method is annotated with @NonBlocking
(or is not annotated at all), then the filters will also be run
on the same event-loop thread.
但是,如果需要在事件循环上运行过滤器,而不管服务请求的方法将在工作线程上运行,则可以使用 @ServerRequestFilter(nonBlocking=true)
。但是,请注意,这些过滤器需要在不使用该设置并将在工作线程上运行的 any 过滤器之前运行。
If however a filter needs to be run on the event-loop despite the fact that the method servicing the request will be
run on a worker thread, then @ServerRequestFilter(nonBlocking=true)
can be used.
Note however, that these filters need to be run before any filter that does not use that setting and would run on a worker thread.
但是,请记住,以上信息不适用于匹配前过滤器 (@ServerRequestFilter(preMatching = true)
)。这些过滤器在事件循环线程上运行。
Keep in mind however that the information above does not apply to pre-matching filters (@ServerRequestFilter(preMatching = true)
).
These filters are always run on an event-loop thread.
同样,可以使用 @ServerResponseFilter
注释声明响应过滤器:
Similarly, response filters can be declared with the
@ServerResponseFilter
annotation:
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 异常也将调用此类响应过滤器。
Such a response filter will also be called for exception-mapping exceptions.
您的过滤器可以声明以下任何参数类型:
Your filters may declare any of the following parameter types:
Type | Usage |
---|---|
Any of the context-objects |
|
A context object to access the current request |
|
A context object to access the current response |
|
Any thrown and exception-mapping exception, or |
它可以声明以下任何返回类型:
It may declare any of the following return types:
Type | Usage |
---|---|
The response to send to the client instead of continuing the filter chain, or |
|
An optional response to send to the client instead of continuing the filter chain, or an empty value if the filter chain should proceed |
|
An asynchronous response to send to the client instead of continuing the filter chain, or |
您可以使用 |
You can restrict the Resource methods for which a filter runs, by using |
The Jakarta REST way
可以通过分别提供 ContainerRequestFilter
或 ContainerResponseFilter
实现来拦截 HTTP 请求和响应。这些过滤器适用于处理与消息关联的元数据:HTTP 头、查询参数、媒体类型和其他元数据。它们还能够中止请求处理,例如,当用户没有权限访问端点时。
Both HTTP request and response can be intercepted by providing ContainerRequestFilter
or ContainerResponseFilter
implementations respectively. These filters are suitable for processing the metadata associated with a message: HTTP
headers, query parameters, media type, and other metadata. They also have the capability to abort the request
processing, for instance when the user does not have the permissions to access the endpoint.
让我们使用 ContainerRequestFilter
为我们的服务添加日志记录功能。我们可以通过以下方式实现:实现 ContainerRequestFilter
并使用 @Provider
注释对其进行注释:
Let’s use ContainerRequestFilter
to add logging capability to our service. We can do that by implementing
ContainerRequestFilter
and annotating it with the @Provider
annotation:
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 方法时,请求都会记录到控制台中:
Now, whenever a REST method is invoked, the request will be logged into the console:
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 例外。 A |
Readers and Writers: mapping entities and HTTP bodies
每当端点方法返回对象(或者在实体中返回 RestResponse<?>
或 Response
时),Quarkus REST 会寻找方法将其映射到 HTTP 响应正文。
Whenever your endpoint methods return an object (or when they return a
RestResponse<?>
or Response
with
an entity), Quarkus REST will look for a way to map that into an HTTP response body.
类似地,每当端点方法将对象作为参数时,我们都会寻找一种方法将 HTTP 请求正文映射到该对象。
Similarly, whenever your endpoint method takes an object as parameter, we will look for a way to map the HTTP request body into that object.
这是通过可插拔的 MessageBodyReader
和 MessageBodyWriter
界面完成的,这些界面负责定义它们从何种 Java 类型映射到何种类型、用于哪些媒体类型以及如何将 HTTP 主体转换为该类型的 Java 实例,反之亦然。
This is done via a pluggable system of MessageBodyReader
and MessageBodyWriter
interfaces,
which are responsible for defining which Java type they map from/to, for which media types,
and how they turn HTTP bodies to/from Java instances of that type.
例如,如果我们在端点上拥有自己的 `Cheese`类型:
For example, if we have our own Cheese
type on our endpoint:
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
注释的主体读取器/写入器定义如何读取和写入它:
Then we can define how to read and write it with our body reader/writers, annotated
with @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 层:
If you want to get the most performance out of your writer, you can extend the
ServerMessageBodyWriter
instead of MessageBodyWriter
where you will be able to use less reflection and bypass the blocking IO layer:
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
进行扩展来截取读取器和写入器。
Just as you can intercept requests and responses, you can also intercept readers and writers, by
extending the ReaderInterceptor
or
WriterInterceptor
on a class annotated with
@Provider
.
如果我们观察这个端点:
If we look at this endpoint:
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);
}
}
我们可以像这样添加读取器和写入器拦截器:
We can add reader and writer interceptors like this:
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 中的单词意义)。
In Quarkus, the Quarkus REST extension and the REST Client extension share the same infrastructure. One important consequence of this consideration is that they share the same list of providers (in the Jakarta REST meaning of the word).
例如,如果你声明一个 WriterInterceptor
,它将默认拦截服务器调用和客户端调用,但这可能不是期望的行为。
For instance, if you declare a WriterInterceptor
, it will by default intercept both the servers calls and the client calls,
which might not be the desired behavior.
但是,您可以更改此默认行为,并将一个提供程序约束为:
However, you can change this default behavior and constrain a provider to:
-
only consider server calls by adding the
@ConstrainedTo(RuntimeType.SERVER)
annotation to your provider; -
only consider client calls by adding the
@ConstrainedTo(RuntimeType.CLIENT)
annotation to your provider.
Parameter mapping
所有 Request Parameters都可以声明为 String
,但也可以是以下类型之一:
All request-parameters can be declared as String
,
but also any of the following types:
-
Types for which a
ParamConverter
is available via a registeredParamConverterProvider
. -
Primitive types.
-
Types that have a constructor that accepts a single
String
argument. -
Types that have a static method named
valueOf
orfromString
with a singleString
argument that return an instance of the type. If both methods are present thenvalueOf
will be used unless the type is anenum
in which casefromString
will be used. -
List<T>
,Set<T>
, orSortedSet<T>
, whereT
satisfies any above criterion.
以下示例说明了所有这些可能性:
The following example illustrates all those possibilities:
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`值用于捕获在同一查询参数的多次出现中使用的值。例如,对于以下资源方法:
Normally a collection of String
values is used to capture the values used in multiple occurrences of the same query parameter.
For example, for the following resource method:
@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);
}
}
}
以及以下请求:
and the following request:
GET /hello?name=foo&name=bar HTTP/1.1
names
变量将包含 foo
和 bar
,而响应将是 hello foo bar
。
the names
variable will contain both foo
and bar
and the response will be hello foo bar
.
然而,根据某个分隔符将单个查询参数转换为值的集合并不是不常见的。这就是 @org.jboss.resteasy.reactive.Separator
注释发挥作用的地方。
It is not uncommon however to need to convert a single query parameter into a collection of values based on some delimiting character. That is where the @org.jboss.resteasy.reactive.Separator
annotation comes into play.
如果我们更新资源方法为:
If we update the resource method to:
@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);
}
}
}
并使用以下请求:
and use the following request:
GET /hello?name=foo,bar HTTP/1.1
那么响应将是 hello foo bar
。
then the response will be hello foo bar
.
Handling dates
Quarkus REST 支持将 java.time.Temporal
实现(如 java.time.LocalDateTime
)用作查询、路径或表单参数。此外,它还提供了 @org.jboss.resteasy.reactive.DateFormat
注释,可用于设置自定义的预期模式。否则,将隐式地使用 JDK 的每种类型的默认格式。
Quarkus REST supports the use of the implementations of java.time.Temporal
(like java.time.LocalDateTime
) as query, path, or form params.
Furthermore, it provides the @org.jboss.resteasy.reactive.DateFormat
annotation, which can be used to set a custom expected pattern.
Otherwise, the JDK’s default format for each type is used implicitly.
Preconditions
HTTP allows requests to be conditional,根据许多条件,例如:
HTTP allows requests to be conditional, based on a number of conditions, such as:
-
Date of last resource modification
-
A resource tag, similar to a hash code of the resource to designate its state or version
让我们看看如何使用 Request
上下文对象执行条件请求验证:
Let’s see how you can do conditional request validation using the
Request
context object:
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
时,我们将得到以下响应:
When we call GET /conditional
the first time, we will get this response:
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
因此,现在如果我们想检查是否需要获取新版本,我们可以发出以下请求:
So now if we want to check if we need to fetch a new version, we can make the following request:
GET /conditional HTTP/1.1
Host: localhost:8080
If-Modified-Since: Wed, 09 Dec 2020 16:10:19 GMT
我们将得到以下响应:
And we would get the following response:
HTTP/1.1 304 Not Modified
由于自那日期以来该资源尚未修改,这可以节省发送资源,但也可以帮助用户检测并发修改。例如,一个客户端想要更新资源,但另一个用户已经修改了它。您可以使用此更新来遵循先前的 GET
请求:
Because the resource has not been modified since that date, this saves on sending the resource but can also help your users detect the concurrent modification.
For example, one client wants to update the resource, but another user has modified it since.
You can follow the previous GET
request with this update:
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
之间有其他用户修改了资源,您将得到此答复:
And if some other user has modified the resource between your GET
and your PUT
you would
get this answer back:
HTTP/1.1 412 Precondition Failed
ETag: "v2"
Content-Length: 0
Negotiation
REST ( and HTTP) 的一个主要思想是您的资源独立于其表示,并且客户端和服务器都可以自由地以他们想要的任意媒体类型表示其资源。这允许服务器声明对多个表示的支持,并让客户端声明它支持哪些表示并获得适当的服务。
One of the main ideas of REST (and HTTP) is that your resource is independent of its representation, and that both the client and server are free to represent their resources in as many media types as they want. This allows the server to declare support for multiple representations and let the client declare which ones it supports and get served something appropriate.
以下端点支持以纯文本或 JSON 形式提供奶酪:
The following endpoint supports serving cheese in plain text or 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 的话:
The user will be able to select which representation it gets with the
Accept
header, in the case of 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"}
针对文本:
And for text:
> 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:
Similarly, you can PUT
two different representations. 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"}
或纯文本:
Or plain text:
> 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 压缩支持。
The body of an HTTP response is not compressed by default.
You can enable the HTTP compression support by means of quarkus.http.enable-compression=true
.
如果启用了压缩支持,则在以下情况下会压缩响应正文:
If compression support is enabled then the response body is compressed if:
-
the resource method is annotated with
@io.quarkus.vertx.http.Compressed
, or -
the
Content-Type
header is set and the value is a compressed media type as configured viaquarkus.http.compress-media-types
.
如果:
The response body is never compressed if:
-
the resource method is annotated with
@io.quarkus.vertx.http.Uncompressed
, or -
the
Content-Type
header is not set.
默认情况下,压缩以下媒体类型列表: |
By default, the following list of media types is compressed: |
如果客户端不支持 HTTP 压缩,那么不会压缩响应体。 |
If the client does not support HTTP compression then the response body is not compressed. |
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 类。
Quarkus enables the inclusion or exclusion of Jakarta REST Resources, Providers and Features directly thanks to build time conditions in the same that it does for CDI beans.
Thus, the various Jakarta REST classes can be annotated with profile conditions (@io.quarkus.arc.profile.IfBuildProfile
or @io.quarkus.arc.profile.UnlessBuildProfile
) and/or with property conditions (io.quarkus.arc.properties.IfBuildProperty
or io.quarkus.arc.properties.UnlessBuildProperty
) to indicate to Quarkus at build time under which conditions these Jakarta REST classes should be included.
在下面的示例中,仅当构建配置 app1
已启用时,Quarkus 才包括 ResourceForApp1Only
资源类。
In the following example, Quarkus includes the ResourceForApp1Only
Resource class if and only if the build profile app1
has been enabled.
@IfBuildProfile("app1")
public class ResourceForApp1Only {
@GET
@Path("sayHello")
public String sayHello() {
return "hello";
}
}
请注意,如果已检测到 Jakarta REST 应用程序并且已覆盖了 getClasses()
和/或 getSingletons()
方法,Quarkus 将忽略构建时间条件,并且仅考虑在 Jakarta REST 应用程序中定义的内容。
Please note that if a Jakarta REST Application has been detected and the method getClasses()
and/or getSingletons()
has/have been overridden, Quarkus will ignore the build time conditions and consider only what has been defined in the Jakarta REST Application.
Using a runtime property
Quarkus 还可根据 @io.quarkus.resteasy.reactive.server.EndpointDisabled
批注的运行时属性值有条件地禁用基于 Jakarta REST 的资源。
Quarkus can also conditionally disable Jakarta REST Resources based on the value of runtime properties using the @io.quarkus.resteasy.reactive.server.EndpointDisabled
annotation.
在下面的示例中,如果应用程序已配置 some.property
为 "disable"
,Quarkus 将在运行时排除 RuntimeResource
。
In the following example, Quarkus will exclude RuntimeResource
at runtime if the application has some.property
configured to "disable"
.
@EndpointDisabled(name = "some.property", stringValue = "disable")
public class RuntimeResource {
@GET
@Path("sayHello")
public String sayHello() {
return "hello";
}
}
此功能在使用原生构建时不起作用。
This feature does not work when using native build.
REST Client
除了服务器端以外,Quarkus REST 还附带了一个新的 MicroProfile REST 客户端实现,其核心是无阻塞的。
In addition to the Server side, Quarkus REST comes with a new MicroProfile REST Client implementation that is non-blocking at its core.
请注意, quarkus-resteasy-client
扩展可能不会与 Quarkus REST 搭配使用,而应使用 quarkus-rest-client
。
Please note that the quarkus-resteasy-client
extension may not be used with Quarkus REST, use quarkus-rest-client
instead.
有关 REST 客户端的更多信息,请参阅 REST Client Guide。
See the REST Client Guide for more information about the REST client.