使用QUARKUS編寫 JSON REST 服務

原標題:QUARKUS - WRITING JSON REST SERVICES
來源:https://quarkus.io/guides/rest-json
版權:本作品采用「署名 3.0 未本地化版本 (CC BY 3.0)」許可協(xié)議進行許可
這是原作者的中文翻譯版本

當前文檔版本:1.13

[TOC]

QUARKUS - 編寫 JSON REST 服務

在本指南中,我們將學會如何讓我們的REST服務來消費和生產(chǎn)JSON。

閱讀本文需要

  • 不超過15分鐘
  • 一個 IDE
  • JDK 1.8+
  • Apache Maven 3.6.2+

體系結構

本指南中構建的應用程序非常簡單:用戶使用表單,在列表中添加元素。

瀏覽器和服務器之間的所有信息傳送都是JSON格式。

解決方案

我們建議您按照接下來的說明,一步步創(chuàng)建應用程序。不過,您可以直接進入已完成的示例。

克隆這個Git倉庫: git clone https://github.com/quarkusio/quarkus-quickstarts.git, 或者在 這里下載

解決方案在 rest-json-quickstart 目錄下。

創(chuàng)建 Maven 工程

首先,我們需要一個新的項目。用以下命令創(chuàng)建一個新的項目。

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,你可以重新創(chuàng)建一個依靠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>

創(chuàng)建你的第一個 JSON REST 服務

在這個例子中,我們將創(chuàng)建一個應用程序來管理一個水果列表。

首先,讓我們創(chuàng)建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 序列化層要求要有一個默認的構造函數(shù)。

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

實現(xiàn)起來非常簡單,只需要使用 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注解,以精確定義內(nèi)容類型。這會縮小本地可執(zhí)行文件中的 JAX-RS providers的數(shù)量。

配置JSON

Jackon

在 Quarkus 中,通過 CDI 獲取的 Jackson ObjectMapper 對象被配置為忽略未知的 properties(是通過禁用 DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES 來實現(xiàn)的)。

可以通過在你的application.properties中設置quarkus.jackson.fail-on-unknown-properties=true或通過@JsonIgnoreProperties(ignoreUnknown = false)來恢復。

此外,ObjectMapper 被配置為以 ISO-8601 格式化日期和時間(是通過禁用 SerializationFeature.WRITE_DATES_AS_TIMESTAMPS 來實現(xiàn)的)。

可以通過在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。如果這樣做,應該在生產(chǎn) ObjectMapper 的 CDI 生產(chǎn)者中手動注入并應用到所有的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。

和上一節(jié)的方法相同,JSON-B可以使用io.quarkus.jsonb.JsonbConfigCustomizer bean進行配置。

例如,在 JSON-B 中注冊一個名為FooSerializer,com.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 生產(chǎn)者中,需要手動注入并應用到所有的 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;
    }
}

寫一個前端頁面

現(xiàn)在讓我們添加一個簡單的網(wǎng)頁,來與FruitResource交互。Quarkus會自動為位于META-INF/resources目錄下的靜態(tài)資源提供服務。在src/main/resources/META-INF/resources目錄下,添加一個fruit.html文件,html文件的內(nèi)容見這里

現(xiàn)在可以與你的REST服務進行交互。

  • 用 ./mvnw 啟動 Quarkus 編譯 quarkus:dev。
  • 通過form表單將新水果添加到列表中

生成本地可執(zhí)行文件(native executable)

你可以通過./mvnw package -Pnative來生成一個本地可執(zhí)行文件

執(zhí)行./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner來運行

訪問 http://localhost:8080/fruits.html來使用

關于序列化

JSON 序列化庫使用Java反射來獲取對象的屬性并將其序列化。

當用GraalVM虛擬機來使用本地可執(zhí)行文件時,會注冊所有將使用反射的類。好消息是,Quarkus在大多數(shù)時候都會為你做這些工作。到目前為止,我們還沒有注冊任何類,甚至連Fruit都沒有注冊,用于反射的使用,一切都很正常。

當Quarkus能夠從REST方法中推斷出序列化的類型時,Quarkus就會執(zhí)行一些魔法。當你有以下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 將無法自動注冊反射所需的類。

這就引出了我們的下一節(jié)。

使用 Response

讓我們創(chuàng)建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;
    }
}

現(xiàn)在創(chuàng)建一個只有一個方法的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();
    }
}

讓我們添加一個網(wǎng)頁來顯示legumes。在src/main/resources/META-INF/resources目錄下,添加一個legumes.html文件,文件內(nèi)容在這里

打開瀏覽器訪問http://localhost:8080/legumes.html,會看到legumes列表。

當構建本地可執(zhí)行文件并運行時,有趣的事情發(fā)生了:

  • ./mvnw package -Pnative創(chuàng)建本地可執(zhí)行文件。
  • ./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner執(zhí)行

你會發(fā)現(xiàn)網(wǎng)頁上沒有l(wèi)egumes列表。

問題的原因是Quarkus無法通過分析REST來判斷出來Legume類需要反射。JSON序列化庫嘗試獲取Legume的字段,得到的是一個空列表,所以它沒有序列化字段的數(shù)據(jù)。

目前,當JSON-B或 Jackson 試圖獲取一個類的字段列表時,如果該類沒有注冊反射,則不會拋出異常。GraalVM會返回一個空的字段列表。

未來開發(fā)團隊會對代碼進行進一步修改,來讓錯誤更加明顯。

我們可以通過在Legume類上添加@RegisterForReflection注解來手動注冊Legume類的反射。

import io.quarkus.runtime.annotations.RegisterForReflection;

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

加入注解后按照相同的步驟來運行:

  • Ctrl+C 停止程序
  • ./mvnw package -Pnative創(chuàng)建本地可執(zhí)行文件。
  • ./target/rest-json-quickstart-1.0.0-SNAPSHOT-runner執(zhí)行
  • 打開瀏覽器,進入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的內(nèi)容,可以查看 這里.

HTTP 過濾器和攔截器

HTTP請求和響應都可以通過ContainerRequestFilterContainerResponseFilter來攔截。這些過濾器適用于處理與消息相關的元數(shù)據(jù):HTTP頭、查詢參數(shù)、媒體類型和其他元數(shù)據(jù)。它們還可以中止請求處理,例如當用戶沒有訪問權限時。

讓我們使用ContainerRequestFilter來為我們的服務添加日志功能。我們可以通過實現(xiàn)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);
    }
}

現(xiàn)在,每當一個REST方法被調(diào)用時,該請求將被記錄。

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()

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 今天感恩節(jié)哎,感謝一直在我身邊的親朋好友。感恩相遇!感恩不離不棄。 中午開了第一次的黨會,身份的轉變要...
    余生動聽閱讀 10,826評論 0 11
  • 彩排完,天已黑
    劉凱書法閱讀 4,469評論 1 3
  • 表情是什么,我認為表情就是表現(xiàn)出來的情緒。表情可以傳達很多信息。高興了當然就笑了,難過就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 129,582評論 2 7

友情鏈接更多精彩內(nèi)容