编辑此页面

编写 JSON REST 服务

JSON 现在是微服务之间的通用语

在本指南中,我们将了解如何让您的 REST 服务使用和生成 JSON 有效负载。

如果您需要 REST 客户端(包括对 JSON 的支持),则有另一份指南。

这是使用 Quarkus 编写 JSON REST 服务的介绍。有关 Quarkus REST(以前称为 RESTEasy Reactive)的更详细指南,请参见此处

先决条件

要完成本指南,您需要

  • 大约 15 分钟

  • 一个 IDE

  • 已安装 JDK 17+ 并正确配置了 JAVA_HOME

  • Apache Maven 3.9.9

  • 如果您想使用它,可以选择 Quarkus CLI

  • 如果您想构建本机可执行文件(或者如果您使用本机容器构建,则为 Docker),可以选择安装 Mandrel 或 GraalVM 并进行适当的配置

架构

本指南中构建的应用程序非常简单:用户可以使用表单在列表中添加元素,并且列表会更新。

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

解决方案

我们建议您按照以下章节中的说明,逐步创建应用程序。但是,您可以直接转到完整的示例。

克隆 Git 存储库:git clone https://github.com/quarkusio/quarkus-quickstarts.git,或下载存档

解决方案位于 rest-json-quickstart 目录中。

创建 Maven 项目

首先,我们需要一个新项目。使用以下命令创建一个新项目

CLI
quarkus create app org.acme:rest-json-quickstart \
    --extension='rest-jackson' \
    --no-code
cd rest-json-quickstart

要创建 Gradle 项目,请添加 --gradle--gradle-kotlin-dsl 选项。

有关如何安装和使用 Quarkus CLI 的更多信息,请参阅 Quarkus CLI 指南。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.24.4:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=rest-json-quickstart \
    -Dextensions='rest-jackson' \
    -DnoCode
cd rest-json-quickstart

要创建 Gradle 项目,请添加 -DbuildTool=gradle-DbuildTool=gradle-kotlin-dsl 选项。

对于 Windows 用户

  • 如果使用 cmd,(不要使用反斜杠 \ 并将所有内容放在同一行上)

  • 如果使用 Powershell,请将 -D 参数用双引号括起来,例如 "-DprojectArtifactId=rest-json-quickstart"

此命令生成一个新项目,导入 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 模块,因此您无需手动执行此操作。

Quarkus 还支持 JSON-B,因此,如果您喜欢 JSON-B 而不是 Jackson,您可以创建一个依赖于 Quarkus REST JSON-B 扩展的项目

CLI
quarkus create app org.acme:rest-json-quickstart \
    --extension='rest-jsonb' \
    --no-code
cd rest-json-quickstart

要创建 Gradle 项目,请添加 --gradle--gradle-kotlin-dsl 选项。

有关如何安装和使用 Quarkus CLI 的更多信息,请参阅 Quarkus CLI 指南。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.24.4:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=rest-json-quickstart \
    -Dextensions='rest-jsonb' \
    -DnoCode
cd rest-json-quickstart

要创建 Gradle 项目,请添加 -DbuildTool=gradle-DbuildTool=gradle-kotlin-dsl 选项。

对于 Windows 用户

  • 如果使用 cmd,(不要使用反斜杠 \ 并将所有内容放在同一行上)

  • 如果使用 Powershell,请将 -D 参数用双引号括起来,例如 "-DprojectArtifactId=rest-json-quickstart"

此命令生成一个新项目,导入 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")

如果您使用 JSON-B 和 JSON-P,请确保不要使用 jakarta.json.Json 提供的快捷方式方法,例如 Json.createValue(…​)

目前,对这些方法的任何单个调用都将初始化一个新的 JsonProvider,这非常慢。Quarkus 通过 quarkus-jsonp 扩展的 JsonProviderHolder 类提供了一个共享的 JsonProvider 实例。

您可以将其作为静态导入来简化您的代码

import static io.quarkus.jsonp.JsonProviderHolder.jsonProvider;

[...]

    public void method() {
        jsonProvider().createValue("value");
    }

[...]

有关 Quarkus REST 的更多信息,请参阅专用指南

创建您的第一个 JSON REST 服务

在此示例中,我们将创建一个应用程序来管理水果列表。

首先,让我们创建 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 注释定义您的端点。

Fruit 对象将由 JSON-BJackson 自动序列化/反序列化,具体取决于您初始化项目时选择的扩展。

当安装 JSON 扩展时,例如 quarkus-rest-jacksonquarkus-rest-jsonb,Quarkus 默认将 application/json 媒体类型用于大多数返回值,除非通过 @Produces@Consumes 注释显式设置媒体类型(对于众所周知的类型,例如 StringFile,有一些例外,它们分别默认为 text/plainapplication/octet-stream)。

配置 JSON 支持

Jackson

在 Quarkus 中,通过 CDI 获取的默认 Jackson ObjectMapper(并由 Quarkus 扩展使用)被配置为忽略未知属性(通过禁用 DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES 功能)。

您可以通过在 application.properties 中设置 quarkus.jackson.fail-on-unknown-properties=true 或通过 @JsonIgnoreProperties(ignoreUnknown = false) 在每个类的基础上恢复 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。如果这样做,手动注入并应用所有 io.quarkus.jackson.ObjectMapperCustomizer bean 在产生 ObjectMapper 的 CDI 生产者中非常重要。否则将阻止应用各种扩展提供的 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 支持

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

JSON-B

如上所述,Quarkus 提供了使用 JSON-B 而不是 Jackson 的选项,通过使用 quarkus-resteasy-jsonb 扩展。

按照上一节中描述的相同方法,可以使用 io.quarkus.jsonb.JsonbConfigCustomizer bean 配置 JSON-B。

例如,如果需要使用 JSON-B 注册类型为 com.example.Foo 的自定义序列化程序 FooSerializer,则添加如下 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 范围)。如果利用了后一种方法,则手动注入并应用所有 io.quarkus.jsonb.JsonbConfigCustomizer bean 在产生 jakarta.json.bind.Jsonb 的 CDI 生产者中非常重要。否则将阻止应用各种扩展提供的 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;
    }
}

创建一个前端

现在让我们添加一个简单的网页来与我们的 FruitResource 交互。Quarkus 会自动提供位于 META-INF/resources 目录下的静态资源。在 src/main/resources/META-INF/resources 目录中,添加一个 fruits.html 文件,其中包含此 fruits.html 文件中的内容。

您现在可以与您的 REST 服务交互

  • 使用以下命令启动 Quarkus

    CLI
    quarkus dev
    Maven
    ./mvnw quarkus:dev
    Gradle
    ./gradlew --console=plain quarkusDev
  • 打开浏览器访问 https://:8080/fruits.html

  • 通过表单将新水果添加到列表中

构建本机可执行文件

您可以使用常用命令构建本机可执行文件

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.native.enabled=true

运行它就像执行 ./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner 一样简单。

然后,您可以将您的浏览器指向 https://:8080/fruits.html 并使用您的应用程序。

关于序列化

JSON 序列化库使用 Java 反射来获取对象的属性并对其进行序列化。

在使用带有 GraalVM 的本机可执行文件时,需要注册所有将与反射一起使用的类。好消息是 Quarkus 大部分时间为您完成这项工作。到目前为止,我们还没有注册任何类,甚至没有注册 Fruit 以供反射使用,并且一切正常。

当 Quarkus 能够从 REST 方法推断序列化类型时,Quarkus 会执行一些魔术。当您有以下 REST 方法时,Quarkus 确定 Fruit 将被序列化

@GET
public List<Fruit> list() {
    // ...
}

Quarkus 通过在构建时分析 REST 方法自动执行此操作,这就是为什么在本指南的第一部分中我们不需要任何反射注册。

在 Jakarta REST 世界中,另一种常见的模式是使用 Response 对象。Response 带来了一些好处

  • 您可以根据方法中发生的情况返回不同的实体类型(例如 LegumeError);

  • 您可以设置 Response 的属性(在出现错误的情况下,首先想到的是状态)。

您的 REST 方法如下所示

@GET
public Response list() {
    // ...
}

Quarkus 无法在构建时确定 Response 中包含的类型,因为该信息不可用。在这种情况下,Quarkus 将无法自动注册所需的类以进行反射。

这将我们带到下一节。

使用 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 而不是豆类列表。

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 文件,其中包含此 legumes.html 文件中的内容。

打开浏览器访问 https://:8080/legumes.html,您将看到我们的豆类列表。

有趣的部分从将应用程序作为本机可执行文件运行时开始

  • 使用以下命令创建本机可执行文件

    CLI
    quarkus build --native
    Maven
    ./mvnw install -Dnative
    Gradle
    ./gradlew build -Dquarkus.native.enabled=true
  • 使用 ./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner 执行它

  • 打开浏览器并访问 https://:8080/legumes.html

那里没有豆类。

如上所述,问题在于 Quarkus 无法通过分析 REST 端点来确定 Legume 类是否需要一些反射。JSON 序列化库尝试获取 Legume 的字段列表,并获得一个空列表,因此它不会序列化字段的数据。

目前,当 JSON-B 或 Jackson 尝试获取类的字段列表时,如果该类未注册以进行反射,则不会引发任何异常。GraalVM 将简单地返回一个空字段列表。

希望这种情况将来会改变,并使错误更加明显。

我们可以通过在我们的 Legume 类上添加 @RegisterForReflection 注释来手动注册 Legume 以进行反射

import io.quarkus.runtime.annotations.RegisterForReflection;

@RegisterForReflection
public class Legume {
    // ...
}
@RegisterForReflection 注释指示 Quarkus 在本机编译期间保留该类及其成员。有关 @RegisterForReflection 注释的更多详细信息,请参见本机应用程序提示页面。

让我们这样做并按照与之前相同的步骤操作

  • Ctrl+C 停止应用程序

  • 使用以下命令创建本机可执行文件

    CLI
    quarkus build --native
    Maven
    ./mvnw install -Dnative
    Gradle
    ./gradlew build -Dquarkus.native.enabled=true
  • 使用 ./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner 执行它

  • 打开浏览器并访问 https://:8080/legumes.html

这次,您可以看到我们的豆类列表。

变为响应式

您可以返回响应式类型来处理异步处理。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 的更多详细信息,请参见 Mutiny - 一个直观的响应式编程库

结论

使用 Quarkus 创建 JSON REST 服务很容易,因为它依赖于经过验证且众所周知的技术。

与往常一样,当您将应用程序作为本机可执行文件运行时,Quarkus 会进一步简化底层操作。

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

相关内容