Writing JSON REST Services
JSON 现已成为微服务之间的 lingua franca. 本指南介绍了如何让您的 REST 服务使用和生成 JSON 有效载荷。
如果您需要 REST client (包括对 JSON 的支持),则还有另一指南。 |
这是使用 Quarkus 编写 JSON REST 服务的简介。有关 Quarkus REST(以前称为 RESTEasy Reactive)的更详细指南,请访问 here. |
Solution
我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。
克隆 Git 存储库: git clone {quickstarts-clone-url}
,或下载 {quickstarts-archive-url}[存档]。
解决方案位于 rest-json-quickstart
directory.
Creating the Maven project
首先,我们需要一个新项目。使用以下命令创建一个新项目:
Unresolved directive in rest-json.adoc - include::{includes}/devtools/create-app.adoc[]
此命令生成一个新项目,导入 Quarkus REST/Jakarta REST 和 Jackson扩展,尤其会添加以下依赖项:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
implementation("io.quarkus:quarkus-rest-jackson")
为了改善用户体验,Quarkus 注册了三个 Jackson Java 8 modules,因此您无需手动执行此操作。 |
Quarkus 还支持 JSON-B,因此,如果您更喜欢 JSON-B 而不是 Jackson,则可以选择创建依赖于 Quarkus REST JSON-B 扩展的新项目:
Unresolved directive in rest-json.adoc - include::{includes}/devtools/create-app.adoc[]
此命令生成一个新项目,导入 Quarkus REST/Jakarta REST 和 JSON-B扩展,尤其会添加以下依赖项:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jsonb</artifactId>
</dependency>
implementation("io.quarkus:quarkus-rest-jsonb")
尽管名称为“reactive”,但 Quarkus REST 同样支持传统的阻塞模式和响应模式。 有关 Quarkus REST 的更多信息,请参阅 dedicated guide. |
Creating your first JSON REST service
在此示例中,我们将创建一个应用程序来管理水果列表。
首先,我们按如下方式创建 `Fruit`bean:
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 序列化层要求有缺省构造函数。
现在,通过以下方式创建 org.acme.rest.json.FruitResource
类:
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 注解定义您的端点。
当安装诸如 |
Configuring JSON support
Jackson
在 Quarkus 中,通过 CDI(并被 Quarkus 扩展名使用)获取的默认 Jackson ObjectMapper
被配置为忽略未知属性(通过禁用 DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
特性)。
您可以通过在 application.properties
中或通过 @JsonIgnoreProperties(ignoreUnknown = false)
为每个类设置 quarkus.jackson.fail-on-unknown-properties=true
来还原 Jackson 的默认行为。
此外,ObjectMapper
被配置为使用 ISO-8601 格式化日期和时间(通过禁用 SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
特性)。
可以通过在 application.properties
中设置 quarkus.jackson.write-dates-as-timestamps=true
来还原 Jackson 的默认行为。如果您想要更改单个字段的格式,可以使用 @JsonFormat
注解。
此外,Quarkus 使得通过 CDI Bean 配置各种 Jackson 设置变得非常容易。最简单(也是建议的)方法是在 io.quarkus.jackson.ObjectMapperCustomizer
类型内定义一个 CDI Bean,在该类型内可以应用任何 Jackson 配置。
需要注册自定义模块的示例如下:
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 特定自定义。
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;
}
}
JSON-B
如上所述,Quarkus 提供了通过使用 quarkus-resteasy-jsonb
扩展名来使用 JSON-B 而不是 Jackson 的选项。
按照上一部分中描述的相同方法,可以使用 io.quarkus.jsonb.JsonbConfigCustomizer
bean 配置 JSON-B。
例如,如果需要用 com.example.Foo
注册 FooSerializer
类型为自定义序列化器的 JSON-B,添加以下类似 bean 就足够了:
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 特定自定义。
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 文件的内容。
你现在可以与你的 REST 服务进行交互:
-
start Quarkus with:include::{includes}/devtools/dev.adoc[]
-
通过表单向列表中添加新水果
Building a native executable
您可以使用以下常用命令构建一个本机可执行文件:
Unresolved directive in rest-json.adoc - include::{includes}/devtools/build-native.adoc[]
运行它与执行 ./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner
一样简单。
然后您可以将浏览器指向 http://localhost:8080/fruits.html
,并使用您的应用程序。
About serialization
JSON 序列化库使用 Java 反射来获取对象的属性并对其进行序列化。
在将本地可执行文件与 GraalVM 一起使用时,所有将与 Reflection 一起使用的类都需要进行注册。好消息是 Quarkus 在大多数情况下会为您完成这项工作。到目前为止,我们甚至没有为 Fruit
注册任何类,用于 Reflection 用途,而且一切工作正常。
当 Quarkus 能够从 REST 方法推断出序列化类型时,它会执行一些神奇操作。当您有以下 REST 方法时,Quarkus 确定 Fruit
将被序列化:
@GET
public List<Fruit> list() {
// ...
}
Quarkus 会在构建时自动分析 REST 方法,自动执行上述操作,这就是为什么我们在本指南的第一部分不需要任何 Reflection 注册的原因。
Jakarta REST 世界中的另一个常见模式是使用 Response
对象。Response
带有一些优点:
-
您可以在方法中发生的事情的基础上返回不同的实体类型(例如
Legume
或Error
); -
您可以设置
Response
的属性(例如 在错误发生时会想到状态)。
您的 REST 方法看起来像这样:
@GET
public Response list() {
// ...
}
Quarkus 无法在构建时确定包含在 Response
中的类型,因为该信息不可用。在这种情况下,Quarkus 无法自动注册必要的反射类。
这将我们带到下一部分。
Using Response
让我们创建一个 Legume
类,它将序列化为 JSON,遵循与我们的 Fruit
类相同的模型:
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 服务。
此方法返回 Response
,而不是 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] 文件中的内容。
打开一个浏览器到 [role="bare"][role="bare"]http://localhost:8080/legumes.html,您将看到我们的豆类列表。
当以本地可执行文件方式运行应用程序时,就会开始有趣的部分:
-
使用以下命令创建本地可执行文件:include::{includes}/devtools/build-native.adoc[]
-
execute it with
./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner
-
打开浏览器并转到 [role="bare"][role="bare"]http://localhost:8080/legumes.html
那里没有豆类。
如上所述,问题在于 Quarkus 无法通过分析 REST 端点来确定 Legume
类将需要一些 Reflection。JSON 序列化库试图获取 Legume
的字段列表,并获取一个空列表,因此它不会序列化字段的数据。
现在,当 JSON-B 或 Jackson 尝试获取类的字段列表时,如果该类尚未注册用于反射,则不会抛出异常。GraalVM 将简单地返回一个空字段列表。 希望将来会改变这种情况,并使错误更明显。 |
我们可以通过在 Legume
类中添加 @RegisterForReflection
注解手动注册 Legume
以进行反射:
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection
public class Legume {
// ...
}
|
这样做并按照以前相同的步骤操作:
-
点击
Ctrl+C
以停止应用程序 -
使用以下命令创建本地可执行文件:include::{includes}/devtools/build-native.adoc[]
-
execute it with
./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner
-
打开浏览器并转到 [role="bare"][role="bare"]http://localhost:8080/legumes.html
这一次,你可以看到我们的豆类列表。
Being reactive
你可以返回 reactive types 以处理异步处理。Quarkus 建议使用 Mutiny 来编写反应式和异步代码。
Quarkus REST 与 Mutiny 天然集成。
你的端点可以返回 Uni
或 Multi
实例:
@GET
@Path("/{name}")
public Uni<Fruit> getOne(String name) {
return findByName(name);
}
@GET
public Multi<Fruit> getAll() {
return findAll();
}
当你有单个结果时,使用 Uni
。当你有可能异步发出的多个项时,使用 Multi
。
你可以使用 Uni
和 Response
返回异步 HTTP 响应:Uni<Response>
。
可在 Mutiny - an intuitive reactive programming library 中找到有关 Mutiny 的更多详细信息。