原標題: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。
- 你也許需要一個 REST 客戶端
閱讀本文需要
- 不超過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-jackson或quarkus-resteasy-jsonb,Quarkus將默認使用application/json類型來處理返回結果,除非通過@Produces或@Consumes注解明確設置了媒體類型(對于眾所周知的類型有一些例外,比如String類型和File類型,它們的媒體類型分別為text/plain和application/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。
你可以使用Uni和Response來返回異步的HTTP響應
更多關于Mutiny的內(nèi)容,可以查看 這里.
HTTP 過濾器和攔截器
HTTP請求和響應都可以通過ContainerRequestFilter或ContainerResponseFilter來攔截。這些過濾器適用于處理與消息相關的元數(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()