Writing JSON REST Services

JSON 现已成为微服务之间的 lingua franca. 本指南介绍了如何让您的 REST 服务使用和生成 JSON 有效载荷。

如果您需要 REST client (包括对 JSON 的支持),则还有另一指南。

这是使用 Quarkus 编写 JSON REST 服务的简介。有关 Quarkus REST(以前称为 RESTEasy Reactive)的更详细指南,请访问 here.

Prerequisites

Unresolved directive in rest-json.adoc - include::{includes}/prerequisites.adoc[]

Architecture

本指南中的应用程序非常简单:用户可以使用窗体在列表中添加元素,然后更新列表。

浏览器和服务器之间的所有信息都采用 JSON 格式。

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扩展,尤其会添加以下依赖项:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-jackson</artifactId>
</dependency>
build.gradle
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扩展,尤其会添加以下依赖项:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-jsonb</artifactId>
</dependency>
build.gradle
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 注解定义您的端点。

将根据在初始化项目时选择的文件扩展名由 JSON-BJackson 自动序列化/反序列化 Fruit 对象。

当安装诸如 quarkus-rest-jacksonquarkus-rest-jsonb 之类的 JSON 扩展名时,除非明确通过 @Produces@Consumes 注解设置媒体类型,Quarkus 将在大多数返回值中默认使用 application/json 媒体类型(某些众所周知的类型有例外,例如 StringFile,在默认为 text/plainapplication/octet-stream)。

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;
    }
}
Mixin support

Quarkus 通过 io.quarkus.jackson.JacksonMixin 注解自动注册 Jackson 的 Mixin 支持。此注释可以放置在旨在用作 Jackson Mixin 的类上,而它们旨在自定义的类被定义为注释的值。

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 服务进行交互:

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 带有一些优点:

  • 您可以在方法中发生的事情的基础上返回不同的实体类型(例如 LegumeError);

  • 您可以设置 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 {
    // ...
}

@RegisterForReflection 注释指示 Quarkus 在本机编译期间保留类及其成员。有关 @RegisterForReflection 注释的更多详细信息,可以在 native application tips 页面上找到。

这样做并按照以前相同的步骤操作:

  • 点击 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 天然集成。

你的端点可以返回 UniMulti 实例:

@GET
@Path("/{name}")
public Uni<Fruit> getOne(String name) {
    return findByName(name);
}

@GET
public Multi<Fruit> getAll() {
    return findAll();
}

当你有单个结果时,使用 Uni。当你有可能异步发出的多个项时,使用 Multi

你可以使用 UniResponse 返回异步 HTTP 响应:Uni<Response>

可在 Mutiny - an intuitive reactive programming library 中找到有关 Mutiny 的更多详细信息。

Conclusion

利用久经考验且广为人知的技术,创建 JSON REST 服务时会如同 Quarkus 一样简单。

像往常一样,当作为本机可执行文件运行你的应用程序时,Quarkus 会进一步简化底层工作。

只有一件事需要记住:如果你使用 Response 并且 Quarkus 无法确定要序列化的 Bean,则需要使用 @RegisterForReflection 为它们添加注释。