The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.
返回指南目录

编写JSON REST服务

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

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

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

这是一个关于用Quarkus编写JSON REST服务的介绍。 这里 有关于RESTEasy Reactive的更详细的指南。

先决条件

完成这个指南,你需要:

  • 大概15分钟

  • 编辑器

  • 安装JDK 11以上版本并正确配置了 JAVA_HOME

  • Apache Maven 3.8.6

  • 如果你愿意的话,还可以选择使用Quarkus CLI

  • 如果你想构建原生可执行程序,可以选择安装Mandrel或者GraalVM,并正确配置(或者使用Docker在容器中进行构建)

架构

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

浏览器和服务器之间的所有信息都被格式化为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='resteasy-reactive-jackson' \
    --no-code
cd rest-json-quickstart

创建Grade项目,请添加 --gradle 或者 --gradle-kotlin-dsl 参数。

关于如何安装并使用Quarkus CLI的更多信息,请参考Quarkus CLI指南

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

创建Grade项目,请添加 -DbuildTool=gradle 或者 -DbuildTool=gradle-kotlin-dsl 参数。

该命令生成了一个导入RESTEasy Reactive/JAX-RS和 Jackson 扩展的新项目,特别是添加了以下依赖项:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-resteasy-reactive-jackson")

为了提高用户体验,Quarkus注册了三个Jackson Java 8模块 ,所以你不需要手动操作。

Quarkus也支持 JSON-B ,因此,如果你喜欢JSON-B而不是Jackson,你可以创建一个依赖RESTEasy Reactive JSON-B扩展的项目:

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

创建Grade项目,请添加 --gradle 或者 --gradle-kotlin-dsl 参数。

关于如何安装并使用Quarkus CLI的更多信息,请参考Quarkus CLI指南

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

创建Grade项目,请添加 -DbuildTool=gradle 或者 -DbuildTool=gradle-kotlin-dsl 参数。

该命令生成了一个导入RESTEasy Reactive/JAX-RS和 JSON-B 扩展的新项目,特别是添加了以下依赖:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-reactive-jsonb</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-resteasy-reactive-jsonb")

虽然名为 "响应式",但RESTEasy Reactive同样支持传统的阻塞模式和响应式模式。

有关RESTEasy Reactive的更多信息,请参考 专用指南

创建你的第一个JSON REST服务

在这个例子中,我们将创建一个应用程序来管理一个fruit列表。

首先,让我们创建 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 javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.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;
    }
}

The implementation is pretty straightforward, and you just need to define your endpoints using the JAX-RS annotations.

Fruit 对象将被 JSON-BJackson 自动序列化/反序列化,这取决于你在初始化项目时选择的扩展。

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

配置JSON支持

Jackson

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

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 功能)。

Jackson的默认行为可以通过在你的 application.properties 中设置 quarkus.jackson.write-dates-as-timestamps=true 来设置。如果你想改变单个字段的默认行为,你可以使用 @JsonFormat 注解。

另外,Quarkus使得通过CDI beans来配置各种Jackson设置变得非常容易。最简单的(也是建议的)方法是定义一个类型为 io.quarkus.jackson.ObjectMapperCustomizer 的CDI Bean,在其中可以使用任何Jackson配置。

需要注册自定义模块的示例如下所示:

import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.jackson.ObjectMapperCustomizer;
import javax.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.jackson.ObjectMapperCustomizer;

import javax.enterprise.inject.Instance;
import javax.enterprise.inject.Produces;
import javax.inject.Singleton;

public class CustomObjectMapper {

    // Replaces the CDI producer for ObjectMapper built into Quarkus
    @Singleton
    @Produces
    ObjectMapper objectMapper(Instance<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 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的选项。

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

例如,如果需要使用 JSON-B 注册类型为 com.example.Foo 的名为 FooSerializer 的自定义序列化程序,则添加如下所示的 bean 就足够了:

import io.quarkus.jsonb.JsonbConfigCustomizer;
import javax.inject.Singleton;
import javax.json.bind.JsonbConfig;
import javax.json.bind.serializer.JsonbSerializer;

@Singleton
public class FooSerializerRegistrationCustomizer implements JsonbConfigCustomizer {

    public void customize(JsonbConfig config) {
        config.withSerializers(new FooSerializer());
    }
}

一个更高级的选择是直接提供一个 javax.json.bind.JsonbConfig 的bean(具有 Dependent 范围),或者在极端情况下,提供一个 javax.json.bind.Jsonb 的bean(具有 Singleton 范围)。如果采用后一种方法,那么在产成 javax.json.bind.Jsonb 的CDI生产者中手动注入和应用所有 io.quarkus.jsonb.JsonbConfigCustomizer Bean是非常重要的。如果不这样做,就会阻止由各种扩展提供的JSON-B特定的自定义功能被应用。

import io.quarkus.jsonb.JsonbConfigCustomizer;

import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Instance;
import javax.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 文件,其中包含这个https://github.com/quarkusio/quarkus-quickstarts/blob/main/rest-json-quickstart/src/main/resources/META-INF/resources/fruits.html[fruits.html] 文件的内容。

现在你可以与你的REST服务进行交互:

  • 启动Quarkus:

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

  • 通过表格添加新的fruits到列表中

构建一个本地可执行文件

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

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.package.type=native

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

然后你可以将你的浏览器访问 http://localhost:8080/fruits.html 并使用你的应用程序。

关于序列化

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

当使用GraalVM的本地可执行文件时,需要注册所有将与反射一起使用的类。好消息是,Quarkus在大多数时候都会为你做这项工作。到目前为止,我们还没有注册任何类,甚至没有注册 Fruit ,并且一切正常。

当Quarkus能够从REST方法中推断出序列化的类型时,它会发挥一些作用。当你有以下的REST方法时,Quarkus确定 Fruit 将被序列化:

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

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

JAX-RS世界中另一个常见的模式是使用 Response 对象。 Response 有一些很好的好处:

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

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

你的 REST 方法如下所示:

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

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

这将我们引向下一节。

使用Response

让我们创建一个将被序列化为 JSON 的 Legume 类,遵循与我们的 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服务,它只有一个返回legumes类列表的方法。

该方法返回一个 Response ,而不是一个 Legume 列表。

package org.acme.rest.json;

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.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();
    }
}

现在让我们添加一个简单的网页来显示我们的legumes列表。在 src/main/resources/META-INF/resources 目录中,添加一个 legumes.html 文件,其中包含这个https://github.com/quarkusio/quarkus-quickstarts/blob/main/rest-json-quickstart/src/main/resources/META-INF/resources/legumes.html[legumes.html] 文件的内容。

Open a browser to http://localhost:8080/legumes.html, and you will see our list of legumes.

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

  • 创建本地可执行文件。

    CLI
    quarkus build --native
    Maven
    ./mvnw install -Dnative
    Gradle
    ./gradlew build -Dquarkus.package.type=native
  • 用以下方式执行它 ./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner

  • 打开浏览器,访问 http://localhost:8080/legumes.html

那里没有legumes。

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将简单地返回一个空的字段列表。

希望这在将来会有所改变,使错误更加明显。

我们可以通过在我们的 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.package.type=native
  • 用以下方式执行它 ./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner

  • 打开浏览器,访问 http://localhost:8080/legumes.html

这一次,你可以看到我们的legumes列表。

响应式

你可以返回 响应式类型 来处理异步处理。Quarkus推荐使用 Mutiny 来编写响应式和异步代码。

RESTEasy Reactive与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 注解它们。