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。

there is another guide if you need a REST client (including support for JSON).

This is an introduction to writing JSON REST services with Quarkus. A more detailed guide about Quarkus REST (formerly RESTEasy Reactive) is available here.

先决条件

完成这个指南,你需要:

  • 大概15分钟

  • 编辑器

  • JDK 17+ installed with JAVA_HOME configured appropriately

  • Apache Maven 3.9.9

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

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

架构

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

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

解决方案

我们建议您按照下一节的说明逐步创建应用程序。然而,您可以直接转到已完成的示例。

克隆 Git 仓库。 git clone https://github.com/quarkusio/quarkus-quickstarts.git ,或者下载一个 存档

The solution is located in the rest-json-quickstart directory.

创建Maven项目

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

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

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

For more information about how to install and use the Quarkus CLI, see the Quarkus CLI guide.

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

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

For Windows users:

  • If using cmd, (don’t use backward slash \ and put everything on the same line)

  • If using Powershell, wrap -D parameters in double quotes e.g. "-DprojectArtifactId=rest-json-quickstart"

This command generates a new project importing the Quarkus REST/Jakarta REST and Jackson extensions, and in particular adds the following dependency:

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 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:

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

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

For more information about how to install and use the Quarkus CLI, see the Quarkus CLI guide.

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

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

For Windows users:

  • If using cmd, (don’t use backward slash \ and put everything on the same line)

  • If using Powershell, wrap -D parameters in double quotes e.g. "-DprojectArtifactId=rest-json-quickstart"

This command generates a new project importing the Quarkus REST/Jakarta REST and JSON-B extensions, and in particular adds the following dependency:

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

If you use JSON-B and JSON-P, make sure you don’t use the shortcut methods offered by jakarta.json.Json such as Json.createValue(…​).

At the moment, any single call to these methods will initialize a new JsonProvider, which is extremely slow. Quarkus provides a shared JsonProvider instance via the JsonProviderHolder class of the quarkus-jsonp extension.

You can import it as a static import to simplify your code:

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

[...]

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

[...]

For more information about Quarkus REST, please refer to the dedicated guide.

创建你的第一个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 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;
    }
}

The implementation is pretty straightforward, and you just need to define your endpoints using the Jakarta REST annotations.

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

When a JSON extension is installed such as quarkus-rest-jackson or quarkus-rest-jsonb, Quarkus will use the application/json media type by default for most return values, unless the media type is explicitly set via @Produces or @Consumes annotations (there are some exceptions for well known types, such as String and File, which default to text/plain and application/octet-stream respectively).

配置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 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 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 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());
    }
}

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

创建一个前台

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

  • 启动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.native.enabled=true

运行它就像执行 ./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方法自动为你执行此操,这就是为什么我们在本指南的第一部分不需要任何反射注册。

Another common pattern in the Jakarta REST world is to use the Response object. Response comes with some nice perks:

  • 你可以根据你的方法中发生的情况,返回不同的实体类型(例如, 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 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();
    }
}

现在让我们添加一个简单的网页来显示我们的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.native.enabled=true
  • 用以下方式执行它 ./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.native.enabled=true
  • 用以下方式执行它 ./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner

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

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

响应式

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

Quarkus REST is naturally integrated with 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 注解它们。

Related content