Writing JSON REST Services
JSON 现已成为微服务之间的 lingua franca.
JSON is now the lingua franca between microservices.
本指南介绍了如何让您的 REST 服务使用和生成 JSON 有效载荷。
In this guide, we see how you can get your REST services to consume and produce JSON payloads.
如果您需要 REST client (包括对 JSON 的支持),则还有另一指南。 |
there is another guide if you need a REST client (including support for JSON). |
Architecture
本指南中的应用程序非常简单:用户可以使用窗体在列表中添加元素,然后更新列表。
The application built in this guide is quite simple: the user can add elements in a list using a form and the list is updated.
浏览器和服务器之间的所有信息都采用 JSON 格式。
All the information between the browser and the server are formatted as JSON.
Solution
我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
克隆 Git 存储库: git clone {quickstarts-clone-url}
,或下载 {quickstarts-archive-url}[存档]。
Clone the Git repository: git clone {quickstarts-clone-url}
, or download an {quickstarts-archive-url}[archive].
解决方案位于 rest-json-quickstart
directory.
The solution is located in the rest-json-quickstart
directory.
Creating the Maven project
首先,我们需要一个新项目。使用以下命令创建一个新项目:
First, we need a new project. Create a new project with the following command:
Unresolved directive in rest-json.adoc - include::{includes}/devtools/create-app.adoc[]
此命令生成一个新项目,导入 Quarkus REST/Jakarta REST 和 Jackson扩展,尤其会添加以下依赖项:
This command generates a new project importing the Quarkus REST/Jakarta REST and Jackson extensions, and in particular adds the following dependency:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
implementation("io.quarkus:quarkus-rest-jackson")
为了改善用户体验,Quarkus 注册了三个 Jackson Java 8 modules,因此您无需手动执行此操作。 To improve user experience, Quarkus registers the three Jackson Java 8 modules so you don’t need to do it manually. |
Quarkus 还支持 JSON-B,因此,如果您更喜欢 JSON-B 而不是 Jackson,则可以选择创建依赖于 Quarkus REST JSON-B 扩展的新项目:
Quarkus also supports JSON-B so, if you prefer JSON-B over Jackson, you can create a project relying on the Quarkus REST JSON-B extension instead:
Unresolved directive in rest-json.adoc - include::{includes}/devtools/create-app.adoc[]
此命令生成一个新项目,导入 Quarkus REST/Jakarta REST 和 JSON-B扩展,尤其会添加以下依赖项:
This command generates a new project importing the Quarkus REST/Jakarta REST and JSON-B extensions, and in particular adds the following dependency:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jsonb</artifactId>
</dependency>
implementation("io.quarkus:quarkus-rest-jsonb")
尽管名称为“reactive”,但 Quarkus REST 同样支持传统的阻塞模式和响应模式。 While named "reactive", Quarkus REST supports equally well both traditional blocking patterns and reactive patterns. 有关 Quarkus REST 的更多信息,请参阅 dedicated guide. For more information about Quarkus REST, please refer to the dedicated guide. |
Creating your first JSON REST service
在此示例中,我们将创建一个应用程序来管理水果列表。
In this example, we will create an application to manage a list of fruits.
首先,我们按如下方式创建 `Fruit`bean:
First, let’s create the Fruit
bean as follows:
package org.acme.rest.json;
public class Fruit {
public String name;
public String description;
public Fruit() {
}
public Fruit(String name, String description) {
this.name = name;
this.description = description;
}
}
没什么新奇之处。需要特别注意的是,JSON 序列化层要求有缺省构造函数。
Nothing fancy. One important thing to note is that having a default constructor is required by the JSON serialization layer.
现在,通过以下方式创建 org.acme.rest.json.FruitResource
类:
Now, create the org.acme.rest.json.FruitResource
class as follows:
package org.acme.rest.json;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Set;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
@Path("/fruits")
public class FruitResource {
private Set<Fruit> fruits = Collections.newSetFromMap(Collections.synchronizedMap(new LinkedHashMap<>()));
public FruitResource() {
fruits.add(new Fruit("Apple", "Winter fruit"));
fruits.add(new Fruit("Pineapple", "Tropical fruit"));
}
@GET
public Set<Fruit> list() {
return fruits;
}
@POST
public Set<Fruit> add(Fruit fruit) {
fruits.add(fruit);
return fruits;
}
@DELETE
public Set<Fruit> delete(Fruit fruit) {
fruits.removeIf(existingFruit -> existingFruit.name.contentEquals(fruit.name));
return fruits;
}
}
实现非常简单,您只需要使用 Jakarta REST 注解定义您的端点。
The implementation is pretty straightforward, and you just need to define your endpoints using the Jakarta REST annotations.
The Fruit
objects will be automatically serialized/deserialized by JSON-B or Jackson,
depending on the extension you chose when initializing the project.
当安装诸如 When a JSON extension is installed such as |
Configuring JSON support
Jackson
在 Quarkus 中,通过 CDI(并被 Quarkus 扩展名使用)获取的默认 Jackson ObjectMapper
被配置为忽略未知属性(通过禁用 DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
特性)。
In Quarkus, the default Jackson ObjectMapper
obtained via CDI (and consumed by the Quarkus extensions) is configured to ignore unknown properties
(by disabling the DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
feature).
您可以通过在 application.properties
中或通过 @JsonIgnoreProperties(ignoreUnknown = false)
为每个类设置 quarkus.jackson.fail-on-unknown-properties=true
来还原 Jackson 的默认行为。
You can restore the default behavior of Jackson by setting quarkus.jackson.fail-on-unknown-properties=true
in your application.properties
or on a per-class basis via @JsonIgnoreProperties(ignoreUnknown = false)
.
此外,ObjectMapper
被配置为使用 ISO-8601 格式化日期和时间(通过禁用 SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
特性)。
Furthermore, the ObjectMapper
is configured to format dates and time in ISO-8601
(by disabling the SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
feature).
可以通过在 application.properties
中设置 quarkus.jackson.write-dates-as-timestamps=true
来还原 Jackson 的默认行为。如果您想要更改单个字段的格式,可以使用 @JsonFormat
注解。
The default behaviour of Jackson can be restored by setting quarkus.jackson.write-dates-as-timestamps=true
in your application.properties
. If you want to change the format for a single field, you can use the
@JsonFormat
annotation.
此外,Quarkus 使得通过 CDI Bean 配置各种 Jackson 设置变得非常容易。最简单(也是建议的)方法是在 io.quarkus.jackson.ObjectMapperCustomizer
类型内定义一个 CDI Bean,在该类型内可以应用任何 Jackson 配置。
Also, Quarkus makes it very easy to configure various Jackson settings via CDI beans.
The simplest (and suggested) approach is to define a CDI bean of type io.quarkus.jackson.ObjectMapperCustomizer
inside of which any Jackson configuration can be applied.
需要注册自定义模块的示例如下:
An example where a custom module needs to be registered would look like so:
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.jackson.ObjectMapperCustomizer;
import jakarta.inject.Singleton;
@Singleton
public class RegisterCustomModuleCustomizer implements ObjectMapperCustomizer {
public void customize(ObjectMapper mapper) {
mapper.registerModule(new CustomModule());
}
}
如果用户愿意,甚至可以提供他们自己的 ObjectMapper
bean。如果这样做,在生成 ObjectMapper
的 CDI 生成器中手动注入和应用所有 io.quarkus.jackson.ObjectMapperCustomizer
bean 非常重要。如果不这样做,将阻止应用各种扩展名提供的 Jackson 特定自定义。
Users can even provide their own ObjectMapper
bean if they so choose.
If this is done, it is very important to manually inject and apply all io.quarkus.jackson.ObjectMapperCustomizer
beans in the CDI producer that produces ObjectMapper
.
Failure to do so will prevent Jackson specific customizations provided by various extensions from being applied.
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.arc.All;
import io.quarkus.jackson.ObjectMapperCustomizer;
import java.util.List;
import jakarta.inject.Singleton;
public class CustomObjectMapper {
// Replaces the CDI producer for ObjectMapper built into Quarkus
@Singleton
@Produces
ObjectMapper objectMapper(@All List<ObjectMapperCustomizer> customizers) {
ObjectMapper mapper = myObjectMapper(); // Custom `ObjectMapper`
// Apply all ObjectMapperCustomizer beans (incl. Quarkus)
for (ObjectMapperCustomizer customizer : customizers) {
customizer.customize(mapper);
}
return mapper;
}
}
Mixin support
Quarkus 通过 io.quarkus.jackson.JacksonMixin
注解自动注册 Jackson 的 Mixin 支持。此注释可以放置在旨在用作 Jackson Mixin 的类上,而它们旨在自定义的类被定义为注释的值。
Quarkus automates the registration of Jackson’s Mixin support, via the io.quarkus.jackson.JacksonMixin
annotation.
This annotation can be placed on classes that are meant to be used as Jackson mixins while the classes they are meant to customize
are defined as the value of the annotation.
JSON-B
如上所述,Quarkus 提供了通过使用 quarkus-resteasy-jsonb
扩展名来使用 JSON-B 而不是 Jackson 的选项。
As stated above, Quarkus provides the option of using JSON-B instead of Jackson via the use of the quarkus-resteasy-jsonb
extension.
按照上一部分中描述的相同方法,可以使用 io.quarkus.jsonb.JsonbConfigCustomizer
bean 配置 JSON-B。
Following the same approach as described in the previous section, JSON-B can be configured using a io.quarkus.jsonb.JsonbConfigCustomizer
bean.
例如,如果需要用 com.example.Foo
注册 FooSerializer
类型为自定义序列化器的 JSON-B,添加以下类似 bean 就足够了:
If for example a custom serializer named FooSerializer
for type com.example.Foo
needs to be registered with JSON-B, the addition of a bean like the following would suffice:
import io.quarkus.jsonb.JsonbConfigCustomizer;
import jakarta.inject.Singleton;
import jakarta.json.bind.JsonbConfig;
import jakarta.json.bind.serializer.JsonbSerializer;
@Singleton
public class FooSerializerRegistrationCustomizer implements JsonbConfigCustomizer {
public void customize(JsonbConfig config) {
config.withSerializers(new FooSerializer());
}
}
更高级的选项是直接提供 jakarta.json.bind.JsonbConfig
bean(带有 Dependent
作用域)或在极端情况下提供 jakarta.json.bind.Jsonb
类型 bean(带有 Singleton
作用域)。如果采用后一种方法,在生成 jakarta.json.bind.Jsonb
的 CDI 生成器中手动注入和应用所有 io.quarkus.jsonb.JsonbConfigCustomizer
bean 非常重要。如果不这样做,将阻止应用各种扩展名提供的 JSON-B 特定自定义。
A more advanced option would be to directly provide a bean of jakarta.json.bind.JsonbConfig
(with a Dependent
scope) or in the extreme case to provide a bean of type jakarta.json.bind.Jsonb
(with a Singleton
scope).
If the latter approach is leveraged it is very important to manually inject and apply all io.quarkus.jsonb.JsonbConfigCustomizer
beans in the CDI producer that produces jakarta.json.bind.Jsonb
.
Failure to do so will prevent JSON-B specific customizations provided by various extensions from being applied.
import io.quarkus.jsonb.JsonbConfigCustomizer;
import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.inject.Instance;
import jakarta.json.bind.JsonbConfig;
public class CustomJsonbConfig {
// Replaces the CDI producer for JsonbConfig built into Quarkus
@Dependent
JsonbConfig jsonConfig(Instance<JsonbConfigCustomizer> customizers) {
JsonbConfig config = myJsonbConfig(); // Custom `JsonbConfig`
// Apply all JsonbConfigCustomizer beans (incl. Quarkus)
for (JsonbConfigCustomizer customizer : customizers) {
customizer.customize(config);
}
return config;
}
}
Creating a frontend
现在,我们添加一个简单的网页来与我们的 FruitResource
互动。Quarkus 自动提供位于 META-INF/resources
目录下的静态资源。在 src/main/resources/META-INF/resources
目录中,添加一个 fruits.html
文件,其中包含此 fruits.html 文件的内容。
Now let’s add a simple web page to interact with our FruitResource
.
Quarkus automatically serves static resources located under the META-INF/resources
directory.
In the src/main/resources/META-INF/resources
directory, add a fruits.html
file with the content from this fruits.html file in it.
你现在可以与你的 REST 服务进行交互:
You can now interact with your REST service:
-
start Quarkus with:include::{includes}/devtools/dev.adoc[]
-
open a browser to
http://localhost:8080/fruits.html
-
add new fruits to the list via the form
Building a native executable
您可以使用以下常用命令构建一个本机可执行文件:
You can build a native executable with the usual command:
Unresolved directive in rest-json.adoc - include::{includes}/devtools/build-native.adoc[]
运行它与执行 ./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner
一样简单。
Running it is as simple as executing ./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner
.
然后您可以将浏览器指向 http://localhost:8080/fruits.html
,并使用您的应用程序。
You can then point your browser to http://localhost:8080/fruits.html
and use your application.
About serialization
JSON 序列化库使用 Java 反射来获取对象的属性并对其进行序列化。
JSON serialization libraries use Java reflection to get the properties of an object and serialize them.
在将本地可执行文件与 GraalVM 一起使用时,所有将与 Reflection 一起使用的类都需要进行注册。好消息是 Quarkus 在大多数情况下会为您完成这项工作。到目前为止,我们甚至没有为 Fruit
注册任何类,用于 Reflection 用途,而且一切工作正常。
When using native executables with GraalVM, all classes that will be used with reflection need to be registered.
The good news is that Quarkus does that work for you most of the time.
So far, we haven’t registered any class, not even Fruit
, for reflection usage and everything is working fine.
当 Quarkus 能够从 REST 方法推断出序列化类型时,它会执行一些神奇操作。当您有以下 REST 方法时,Quarkus 确定 Fruit
将被序列化:
Quarkus performs some magic when it is capable of inferring the serialized types from the REST methods.
When you have the following REST method, Quarkus determines that Fruit
will be serialized:
@GET
public List<Fruit> list() {
// ...
}
Quarkus 会在构建时自动分析 REST 方法,自动执行上述操作,这就是为什么我们在本指南的第一部分不需要任何 Reflection 注册的原因。
Quarkus does that for you automatically by analyzing the REST methods at build time and that’s why we didn’t need any reflection registration in the first part of this guide.
Jakarta REST 世界中的另一个常见模式是使用 Response
对象。Response
带有一些优点:
Another common pattern in the Jakarta REST world is to use the Response
object.
Response
comes with some nice perks:
-
you can return different entity types depending on what happens in your method (a
Legume
or anError
for instance); -
you can set the attributes of the
Response
(the status comes to mind in the case of an error).
您的 REST 方法看起来像这样:
Your REST method then looks like this:
@GET
public Response list() {
// ...
}
Quarkus 无法在构建时确定包含在 Response
中的类型,因为该信息不可用。在这种情况下,Quarkus 无法自动注册必要的反射类。
It is not possible for Quarkus to determine at build time the type included in the Response
as the information is not available.
In this case, Quarkus won’t be able to automatically register for reflection the required classes.
这将我们带到下一部分。
This leads us to our next section.
Using Response
让我们创建一个 Legume
类,它将序列化为 JSON,遵循与我们的 Fruit
类相同的模型:
Let’s create the Legume
class which will be serialized as JSON, following the same model as for our Fruit
class:
package org.acme.rest.json;
public class Legume {
public String name;
public String description;
public Legume() {
}
public Legume(String name, String description) {
this.name = name;
this.description = description;
}
}
现在,让我们创建一个只有返回豆类列表的 LegumeResource
REST 服务。
Now let’s create a LegumeResource
REST service with only one method which returns the list of legumes.
此方法返回 Response
,而不是 Legume
的列表。
This method returns a Response
and not a list of Legume
.
package org.acme.rest.json;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;
@Path("/legumes")
public class LegumeResource {
private Set<Legume> legumes = Collections.synchronizedSet(new LinkedHashSet<>());
public LegumeResource() {
legumes.add(new Legume("Carrot", "Root vegetable, usually orange"));
legumes.add(new Legume("Zucchini", "Summer squash"));
}
@GET
public Response list() {
return Response.ok(legumes).build();
}
}
现在,让我们添加一个简单的网页来显示我们的豆类列表。在 src/main/resources/META-INF/resources
目录中,添加一个 legumes.html
文件,其中包含此{quicks-blob-url}/rest-json-quicks/src/main/resources/META-INF/resources/legumes.html[legumes.html] 文件中的内容。
Now let’s add a simple web page to display our list of legumes.
In the src/main/resources/META-INF/resources
directory, add a legumes.html
file with the content from this
{quickstarts-blob-url}/rest-json-quickstart/src/main/resources/META-INF/resources/legumes.html[legumes.html] file in it.
打开一个浏览器到 [role="bare"][role="bare"]http://localhost:8080/legumes.html,您将看到我们的豆类列表。
Open a browser to [role="bare"]http://localhost:8080/legumes.html, and you will see our list of legumes.
当以本地可执行文件方式运行应用程序时,就会开始有趣的部分:
The interesting part starts when running the application as a native executable:
-
create the native executable with:include::{includes}/devtools/build-native.adoc[]
-
execute it with
./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner
-
open a browser and go to [role="bare"]http://localhost:8080/legumes.html
那里没有豆类。
No legumes there.
如上所述,问题在于 Quarkus 无法通过分析 REST 端点来确定 Legume
类将需要一些 Reflection。JSON 序列化库试图获取 Legume
的字段列表,并获取一个空列表,因此它不会序列化字段的数据。
As mentioned above, the issue is that Quarkus was not able to determine the Legume
class will require some reflection by analyzing the REST endpoints.
The JSON serialization library tries to get the list of fields of Legume
and gets an empty list, so it does not serialize the fields' data.
现在,当 JSON-B 或 Jackson 尝试获取类的字段列表时,如果该类尚未注册用于反射,则不会抛出异常。GraalVM 将简单地返回一个空字段列表。 At the moment, when JSON-B or Jackson tries to get the list of fields of a class, if the class is not registered for reflection, no exception will be thrown. GraalVM will simply return an empty list of fields. 希望将来会改变这种情况,并使错误更明显。 Hopefully, this will change in the future and make the error more obvious. |
我们可以通过在 Legume
类中添加 @RegisterForReflection
注解手动注册 Legume
以进行反射:
We can register Legume
for reflection manually by adding the @RegisterForReflection
annotation on our Legume
class:
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection
public class Legume {
// ...
}
|
The |
这样做并按照以前相同的步骤操作:
Let’s do that and follow the same steps as before:
-
hit
Ctrl+C
to stop the application -
create the native executable with:include::{includes}/devtools/build-native.adoc[]
-
execute it with
./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner
-
open a browser and go to [role="bare"]http://localhost:8080/legumes.html
这一次,你可以看到我们的豆类列表。
This time, you can see our list of legumes.
Being reactive
你可以返回 reactive types 以处理异步处理。Quarkus 建议使用 Mutiny 来编写反应式和异步代码。
You can return reactive types to handle asynchronous processing. Quarkus recommends the usage of Mutiny to write reactive and asynchronous code.
Quarkus REST 与 Mutiny 天然集成。
Quarkus REST is naturally integrated with Mutiny.
你的端点可以返回 Uni
或 Multi
实例:
Your endpoints can return Uni
or Multi
instances:
@GET
@Path("/{name}")
public Uni<Fruit> getOne(String name) {
return findByName(name);
}
@GET
public Multi<Fruit> getAll() {
return findAll();
}
当你有单个结果时,使用 Uni
。当你有可能异步发出的多个项时,使用 Multi
。
Use Uni
when you have a single result.
Use Multi
when you have multiple items that may be emitted asynchronously.
你可以使用 Uni
和 Response
返回异步 HTTP 响应:Uni<Response>
。
You can use Uni
and Response
to return asynchronous HTTP responses: Uni<Response>
.
可在 Mutiny - an intuitive reactive programming library 中找到有关 Mutiny 的更多详细信息。
More details about Mutiny can be found in Mutiny - an intuitive reactive programming library.
Conclusion
利用久经考验且广为人知的技术,创建 JSON REST 服务时会如同 Quarkus 一样简单。
Creating JSON REST services with Quarkus is easy as it relies on proven and well known technologies.
像往常一样,当作为本机可执行文件运行你的应用程序时,Quarkus 会进一步简化底层工作。
As usual, Quarkus further simplifies things under the hood when running your application as a native executable.
只有一件事需要记住:如果你使用 Response
并且 Quarkus 无法确定要序列化的 Bean,则需要使用 @RegisterForReflection
为它们添加注释。
There is only one thing to remember: if you use Response
and Quarkus can’t determine the beans that are serialized, you need to annotate them with @RegisterForReflection
.