使用QUARKUS编写 JSON REST 服务

原标题:QUARKUS - WRITING JSON REST SERVICES
来源:https://quarkus.io/guides/rest-json
版权:本作品采用「署名 3.0 未本地化版本 (CC BY 3.0)」许可协议进行许可
这是原作者的中文翻译版本

当前文档版本:1.13

[TOC]

QUARKUS - 编写 JSON REST 服务

在本指南中,我们将学会如何让我们的REST服务来消费和生产JSON。

阅读本文需要

  • 不超过15分钟
  • 一个 IDE
  • JDK 1.8+
  • Apache Maven 3.6.2+

体系结构

本指南中构建的应用程序非常简单:用户使用表单,在列表中添加元素。

浏览器和服务器之间的所有信息传送都是JSON格式。

解决方案

我们建议您按照接下来的说明,一步步创建应用程序。不过,您可以直接进入已完成的示例。

克隆这个Git仓库: git clone https://github.com/quarkusio/quarkus-quickstarts.git, 或者在 这里下载

解决方案在 rest-json-quickstart 目录下。

创建 Maven 工程

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

mvn io.quarkus:quarkus-maven-plugin:1.13.1.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=rest-json-quickstart \
    -DclassName="org.acme.rest.json.FruitResource" \
    -Dpath="/fruits" \
    -Dextensions="resteasy,resteasy-jackson"
cd rest-json-quickstart

该命令会生成一个Maven结构,并会 导入 RESTEasy/JAX-RS 和 Jackson ,会添加以下依赖关系:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>

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

mvn io.quarkus:quarkus-maven-plugin:1.13.1.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=rest-json-quickstart \
    -DclassName="org.acme.rest.json.FruitResource" \
    -Dpath="/fruits" \
    -Dextensions="resteasy-jsonb"
cd rest-json-quickstart

该命令会生成一个导入 RESTEasy/JAX-RS 和 JSON-B 扩展的Maven结构,会添加以下依赖关系。

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-jsonb</artifactId>
</dependency>

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

实现起来非常简单,只需要使用 JAX-RS 注解。

Fruit 对象将通过 JSON-B 或 Jackson 自动地序列化/反序列化。

  • 当安装了 JSON 扩展,比如安装了quarkus-resteasy-jacksonquarkus-resteasy-jsonb,Quarkus将默认使用application/json类型来处理返回结果,除非通过@Produces@Consumes注解明确设置了媒体类型(对于众所周知的类型有一些例外,比如String类型和File类型,它们的媒体类型分别为text/plainapplication/octet-stream)。

如果你不想要使用默认的JSON设置,你可以设置quarkus.resteasy-json.default-json=false,如果你这样设置了,则还需要添加@Produces(MediaType.APPLICATION_JSON)@Consumes(MediaType.APPLICATION_JSON)这两个注解。

如果你不使用 JSON 的默认值,即设置quarkus.resteasy-json.default-json=false,那么强烈建议使用@Produces@Consumes注解,以精确定义内容类型。这会缩小本地可执行文件中的 JAX-RS providers的数量。

配置JSON

Jackon

在 Quarkus 中,通过 CDI 获取的 Jackson ObjectMapper 对象被配置为忽略未知的 properties(是通过禁用 DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES 来实现的)。

可以通过在你的application.properties中设置quarkus.jackson.fail-on-unknown-properties=true或通过@JsonIgnoreProperties(ignoreUnknown = false)来恢复。

此外,ObjectMapper 被配置为以 ISO-8601 格式化日期和时间(是通过禁用 SerializationFeature.WRITE_DATES_AS_TIMESTAMPS 来实现的)。

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

Quarkus通过CDI beans可以很容易地配置各种Jackson设置。建议的方法是定义一个类型为io.quarkus.jackson.ObjectMapperCustomizer的CDI bean,在这个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。

import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.jackson.ObjectMapperCustomizer;

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

public class CustomObjectMapper {

    // Replaces the CDI producer for ObjectMapper built into Quarkus
    @Singleton
    ObjectMapper objectMapper(Instance<ObjectMapperCustomizer> customizers) {
        ObjectMapper mapper = myObjectMapper(); // Custom `ObjectMapper`

        // Apply all ObjectMapperCustomerizer beans (incl. Quarkus)
        for (ObjectMapperCustomizer customizer : customizers) {
            customizer.customize(mapper);
        }

        return mapper;
    }
}

JSON-B

Quarkus 通过 quarkus-resteasy-jsonb 扩展提供了 JSON-B。

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

例如,在 JSON-B 中注册一个名为FooSerializercom.example.Foo类型的自定义序列化器,只需添加一个下面的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.JsonbConfigCustomizer 的 CDI 生产者中,需要手动注入并应用到所有的 io.quarkus.jsonb.JsonbConfigCustomizer beans 。

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目录下,添加一个fruit.html文件,html文件的内容见这里

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

  • 用 ./mvnw 启动 Quarkus 编译 quarkus:dev。
  • 通过form表单将新水果添加到列表中

生成本地可执行文件(native executable)

你可以通过./mvnw package -Pnative来生成一个本地可执行文件

执行./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner来运行

访问 http://localhost:8080/fruits.html来使用

关于序列化

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

当用GraalVM虚拟机来使用本地可执行文件时,会注册所有将使用反射的类。好消息是,Quarkus在大多数时候都会为你做这些工作。到目前为止,我们还没有注册任何类,甚至连Fruit都没有注册,用于反射的使用,一切都很正常。

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

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

Quarkus在build过程时会自动分析REST方法,这就是为什么我们在本指南的第一部分不需要任何反射注册

JAX-RS 中另一种常见的模式是使用Response对象。Response有一些不错的能力:

  • 你可以返回不同的实体类型(例如一个Legume或一个Error)。
  • 你可以设置Response的属性。

你的REST方法写起来像这样:

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

如果 Quarkus 无法在build时确定响应中的类型,Quarkus 将无法自动注册反射所需的类。

这就引出了我们的下一节。

使用 Response

让我们创建Legume类,它将被序列化为JSON。

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的list。

这个方法返回的是Response

package org.acme.rest.json;

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

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/legumes")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
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文件,文件内容在这里

打开浏览器访问http://localhost:8080/legumes.html,会看到legumes列表。

当构建本地可执行文件并运行时,有趣的事情发生了:

  • ./mvnw package -Pnative创建本地可执行文件。
  • ./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner执行

你会发现网页上没有legumes列表。

问题的原因是Quarkus无法通过分析REST来判断出来Legume类需要反射。JSON序列化库尝试获取Legume的字段,得到的是一个空列表,所以它没有序列化字段的数据。

目前,当JSON-B或 Jackson 试图获取一个类的字段列表时,如果该类没有注册反射,则不会抛出异常。GraalVM会返回一个空的字段列表。

未来开发团队会对代码进行进一步修改,来让错误更加明显。

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

import io.quarkus.runtime.annotations.RegisterForReflection;

@RegisterForReflection
public class Legume {
    // ...
}

加入注解后按照相同的步骤来运行:

  • Ctrl+C 停止程序
  • ./mvnw package -Pnative创建本地可执行文件。
  • ./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner执行
  • 打开浏览器,进入http://localhost:8080/legumes.html

这次,你可以看到一列 legumes.

变成响应式

你可以返回响应式类型(reactive types)来进行异步处理。Quarkus推荐使用Mutiny来编写响应式和异步的代码。

若要整合 Mutiny 和 RESTEasy ,你需要在你的项目中添加quarkus-resteasy-mutiny依赖。

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-mutiny</artifactId>
</dependency>

然后,你可以返回 Uni 或 Multi 实例。

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

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

当你返回一个单一的结果时,用Uni。当你有多个可能异步返回的结果时,使用Multi

你可以使用UniResponse来返回异步的HTTP响应

更多关于Mutiny的内容,可以查看 这里.

HTTP 过滤器和拦截器

HTTP请求和响应都可以通过ContainerRequestFilterContainerResponseFilter来拦截。这些过滤器适用于处理与消息相关的元数据:HTTP头、查询参数、媒体类型和其他元数据。它们还可以中止请求处理,例如当用户没有访问权限时。

让我们使用ContainerRequestFilter来为我们的服务添加日志功能。我们可以通过实现ContainerRequestFilter接口并使用@Provider注。

package org.acme.rest.json;

import io.vertx.core.http.HttpServerRequest;
import org.jboss.logging.Logger;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Provider;

@Provider
public class LoggingFilter implements ContainerRequestFilter {

    private static final Logger LOG = Logger.getLogger(LoggingFilter.class);

    @Context
    UriInfo info;

    @Context
    HttpServerRequest request;

    @Override
    public void filter(ContainerRequestContext context) {

        final String method = context.getMethod();
        final String path = info.getPath();
        final String address = request.remoteAddress().toString();

        LOG.infof("Request %s %s from IP %s", method, path, address);
    }
}

现在,每当一个REST方法被调用时,该请求将被记录。

2019-06-05 12:44:26,526 INFO  [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request GET /legumes from IP 127.0.0.1
2019-06-05 12:49:19,623 INFO  [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request GET /fruits from IP 0:0:0:0:0:0:0:1
2019-06-05 12:50:44,019 INFO  [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request POST /fruits from IP 0:0:0:0:0:0:0:1
2019-06-05 12:51:04,485 INFO  [org.acm.res.jso.LoggingFilter] (executor-thread-1) Request GET /fruits from IP 127.0.0.1

CORS 过滤

Quarkus 带有 CORS 过滤. 请访问 HTTP 参考文档

GZip 支持

Quarkus 自带 GZip 的支持(默认情况下没有启用)。使用下面的配置来开启GZip支持。

quarkus.resteasy.gzip.enabled=true 
quarkus.resteasy.gzip.max-input=10M 

第二个配置项决定了压缩后请求体的大小上限。默认值为10M。

一旦启用了GZip支持,你就可以通过添加@org.jboss.resteasy.annotations.GZIP注解来使用。

如果你想压缩所有的东西,那么我们建议你使用quarkus.http.enable-compression=true

Multipart

RESTEasy 通过 RESTEasy Multipart Provider来支持Multipart

Quarkus提供了一个名为quarkus-resteasy-multipart的扩展。

这个扩展与RESTEasy的默认行为略有不同,因为默认的字符集(如果你的请求中没有指定)是UTF-8而不是US-ASCII。

Servlet 兼容性

在Quarkus中,RESTEasy可以直接运行在Vert.x HTTP服务器之上,如果你有任何servlet依赖,也可以运行在Undertow之上。

因此,某些类,如HttpServletRequest,并不总是可用于注入。这个类的大部分使用场景都被 JAX-RS 所覆盖。RESTEasy自带了一个可以用来注入的API:HttpRequest,它有两个方法, getRemoteAddress()getRemoteHost()

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    余生动听阅读 13,586评论 0 11
  • 彩排完,天已黑
    刘凯书法阅读 9,757评论 1 3
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 126,977评论 2 7