1. 簡介
在本文中,我們將探討 Manifold JSON,用於基於 JSON Schema 的 JSON 文檔解析。我們將瞭解它的用途、我們可以用它做什麼,以及如何使用它。
Manifold 是一套編譯器插件,每個插件都提供了一些高度生產力功能,我們可以將其用於我們的應用程序中。在本文中,我們將探索如何使用它來根據提供的 JSON Schema 文件解析和構建 JSON 文檔。
2. 安裝
在開始使用 Manifold 之前,我們需要確保它已可用給我們的編譯器。 最重要的集成方式是作為插件添加到我們的 Java 編譯器中。我們通過正確配置 Maven 或 Gradle 項目來實現這一點。此外,Manifest 還提供了一些 IDE 插件,以簡化我們的開發流程。
2.1. Maven 中的編譯器插件
在 Maven 中集成 Manifold 需要我們添加一個 依賴項和一個 編譯器插件。這兩個插件必須保持同一版本,即在寫作時為 2024.1.20。
添加我們的依賴項與添加任何其他依賴項相同:
<dependency>
<groupId>systems.manifold</groupId>
<artifactId>manifold-json-rt</artifactId>
<version>2024.1.20</version>
</dependency>添加我們的編譯器插件需要我們在模塊中配置 maven-compiler-plugin:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<compilerArgs>
<!-- Configure manifold plugin-->
<arg>-Xplugin:Manifold</arg>
</compilerArgs>
<!-- Add the processor path for the plugin -->
<annotationProcessorPaths>
<path>
<groupId>systems.manifold</groupId>
<artifactId>manifold-json</artifactId>
<version>2024.1.20</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>我們在此添加了 -Xplugin:Manifold 命令行的參數,當編譯器執行時,以及添加了 Manifold JSON 註解處理器。
這個註解處理器負責從我們的 JSON 模式文件生成代碼的所有工作。
2.2. Gradle 中編譯器插件
使用 Gradle 將 Manifold 集成到我們的應用程序中需要實現相同目標,但配置略微簡單一些。 Gradle 支持以與任何其他依賴項相同的方式將註解處理器添加到 Java 編譯器中:
dependencies {
implementation 'systems.manifold:manifold-json-rt:2024.1.20'
annotationProcessor 'systems.manifold:manifold-json:2024.1.20'
testAnnotationProcessor 'systems.manifold:manifold-json:2024.1.20'
}然而,我們仍然需要確保將 -Xplugin:Manifold 參數傳遞給編譯器:
tasks.withType(JavaCompile) {
options.compilerArgs += ['-Xplugin:Manifold']
}此時,我們準備好開始使用 Manifold JSON。
2.3. IDE 插件
除了我們 Maven 或 Gradle 構建中的插件外,Manifold 還提供適用於 IntelliJ 和 Android Studio 的插件。 這些插件可以從標準插件市場點安裝:
安裝完成後,這些編譯器插件將允許我們在 IDE 中編譯代碼時使用,而不是僅依賴 Maven 或 Gradle 構建來完成正確的工作。
<h2><strong>3. 使用 JSON 模式定義類</strong></strong></h2>
<p><strong>在將 Manifold JSON 設置到我們的項目中後,我們可以使用它來定義類。我們通過在 <em>src/main/resources</em> (或 <em>src/test/resources</em>) 目錄中編寫 JSON 模式文件來實現。 文件的完整路徑將成為完全限定的類名。</strong></p>
<p>例如,我們可以通過在 <em>src/main/resources/com/baeldung/manifold/SimpleUser.json</em> 中編寫 JSON 模式來創建 <em>com.baeldung.manifold.SimpleUser</em> 類:</p>
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "http://baeldung.com/manifold/SimpleUser.json",
"type": "object",
"properties": {
"username": {
"type": "string",
"maxLength": 100
},
"name": {
"type": "string",
"maxLength": 80
},
"email": {
"type": "string",
"format": "email",
"maxLength": 260
}
},
"required": [
"username",
"name"
]
}這代表一個類,包含三個字段:username、name 和 email,所有字段的類型均為 String。
然而,Manifold 不會執行任何不必要的操作。因此,編寫此文件不會生成任何代碼,除非有其他引用它。 這種引用可以是編譯器需要的任何內容,即使只是一個變量的定義。
4. 實例化 Manifold 類
一旦我們有了類定義,我們希望能夠使用它們。然而,我們會很快發現這些類與普通類不同。Manifold 會生成所有我們預期的類作為 Java 接口,而不是直接的類。這意味着我們不能簡單地使用 new 關鍵字創建實例。
但是,生成的代碼提供了一個靜態 create() 方法,我們可以將其用作構造方法。 該方法需要按照 required 數組中指定的順序包含所有必需的值。例如,上述 JSON Schema 將會生成以下內容:
public static SimpleUser create(String username, String name);然後,我們有設置器,可以用來填充剩餘的字段。
因此,我們可以創建一個 SimpleUser 類的實例,並使用以下內容:
SimpleUser user = SimpleUser.create("testuser", "Test User");
user.setEmail("[email protected]");此外,Manifold 還提供了一個 build() 方法,我們可以用於構建器模式。 該方法使用相同的必需參數,但返回一個構建器對象,用於填充其他字段:
SimpleUser user = SimpleUser.builder("testuser", "Test User")
.withEmail("[email protected]")
.build();
如果使用既可選又只讀的字段,那麼使用構建器模式是唯一提供值的途徑。一旦創建了實例,我們就無法再設置這些字段的值。
5. 解析 JSON
在我們生成了類之後,我們可以使用它們與 JSON 來源進行交互。我們的生成類提供了從各種類型輸入中加載數據並將其直接解析到目標類中的方法。進入此流程始終使用我們生成類上的靜態 load() 方法。這為我們提供了一個 manifold.json.rt.api.Loader<> 接口,該接口提供了用於加載我們的 JSON 數據的大量方法。
最簡單的這些是能夠解析我們提供的 JSON 字符串。這通過使用 fromJson() 方法完成的,該方法接受一個字符串並返回一個完全形成的類實例:
SimpleUser user = SimpleUser.load().fromJson("""
{
"username": "testuser",
"name": "Test User",
"email": "[email protected]"
}
""");
成功時,這會給我們帶來我們想要的結果。失敗時,我們會得到一個RuntimeException,它包裹了一個manifold.rt.api.ScriptException,指示了具體出了什麼問題。
我們還可以通過多種方式提供數據:
- fromJsonFile(),它從java.io.File中讀取數據。
- fromJsonReader(),它從java.io.Reader中讀取數據。
- fromJsonUrl(),它從URL中讀取數據,此時Manifold會去獲取該URL指向的數據。
請注意,fromJsonUrl()方法最終使用java.net.URL.openStream()來讀取數據,這可能不如我們所希望的效率。
所有這些方法都以相同的方式工作:我們用適當的數據源調用它們,並返回一個完整的對象,或者如果無法解析數據則拋出RuntimeException。
InputStream is = getClass().getResourceAsStream("/com/baeldung/manifold/simpleUserData.json");
InputStreamReader reader = new InputStreamReader(is);
SimpleUser user = SimpleUser.load().fromJsonReader(reader);
6. 生成 JSON
除了能夠將 JSON 解析為我們的對象外,我們還可以從我們的對象生成 JSON。
類似於 Manifold 產生 load() 方法以將 JSON 加載到我們的對象中,我們也有 write() 方法從我們的對象中生成 JSON。 這返回一個 manifold.json.rt.api.Writer 實例,該實例為我們提供了從我們的對象中生成 JSON 的方法。
這些方法中最簡單的就是 toJson() 方法,它返回 JSON 作為 String,我們可以從中執行任何操作:
SimpleUser user = SimpleUser.builder("testuser", "Test User")
.withEmail("[email protected]")
.build();
String json = user.write().toJson();當然,以下是翻譯後的內容:
或者,我們可以將 JSON 寫入任何實現 Appendable 接口的組件。這包括但不限於 java.io.Writer 接口或 StringBuilder 和 StringBuffer 類:
SimpleUser user = SimpleUser.builder("testuser", "Test User")
.withEmail("[email protected]")
.build();
user.write().toJson(writer);這位作者可以隨後將內容寫入任何目標,包括內存緩衝區、文件或網絡連接。
<h2><strong>7. 替代格式</strong></h2>
<p><strong><em>manifold-json</em> 的主要目的是能夠解析和生成 JSON 內容。但是,我們還具備其他格式的能力——CSV、XML 和 YAML。</strong></p>
<p>對於每種格式,我們需要為我們的項目添加特定的依賴項——對於 CSV,是 <em >systems.manifold:manifold-csv-rt</em>,對於 XML 是 <em >systems.manifold:manifold-xml-rt</em>,對於 YAML 是 <em >systems.manifold:manifold-yaml-rt</em>。我們可以添加所需的任何數量的這些依賴項。</p>
<p>完成這些操作後,我們可以使用 Manifold 提供的 <em >Loader</em> 和 <em >Writer</em> 接口中的相應方法:</p>
SimpleUser user = SimpleUser.load().fromXmlReader(reader);
String yaml = user.write().toYaml();8. JSON Schema 特性
Manifold 使用 JSON Schema 文件來描述我們類(class)的結構。我們可以利用這些文件來描述要生成類以及其中包含的字段。然而,JSON Schema 可以描述的不僅僅是這些,Manifold JSON 還支持一些額外的特性。
8.1. 只讀和只寫字段
在模式中標記為只讀的字段不會生成 setter。這意味着我們可以在使用構造函數或從解析輸入文件時設置它們的值,但之後永遠不能更改這些值。
"username": {
"type": "string",
"readOnly": true
},
相反,生成器會在模式中創建標記為只讀的字段,而沒有 getter。這意味着我們可以以任何方式填充它們——在構造時、從輸入文件解析,或使用 setter,但我們永遠無法從中獲取值。
"mfaCode": {
"type": "string",
"writeOnly": true
},
請注意,系統仍然會在任何生成的輸出中渲染只讀屬性,因此我們可以通過該路由訪問它們。但是,我們無法從 Java 類中讀取這些屬性。
8.2. 格式化類型
某些 JSON Schema 類型允許我們提供額外的格式信息。例如,我們可以指定一個字段是 字符串,但格式為 日期時間。 這為使用該類型數據的任何內容提供了一個提示,即在這些字段中應該看到什麼。
Manifold JSON 將盡力理解這些格式併為它們生成適當的 Java 類型。例如,格式為 日期時間 的 字符串 將在我們的代碼中生成為 java.time.LocalDateTime。
8.3. 附加屬性
JSON Schema 允許我們通過使用 additionalProperties 和 patternProperties 定義開放式模式。
additionalProperties 標誌指示類型可以具有任意數量的任何類型的額外屬性。 基本上這意味着模式允許任何其他 JSON 匹配。 Manifold JSON 默認情況下將此項定義為 true,但如果希望在模式中明確將其設置為 false,則可以這樣做。
如果將其設置為 true,則 Manifold 將為生成的類提供兩個附加方法:get(name) 和 put(name, value)。 通過使用這些方法,我們可以處理任何任意字段:
SimpleUser user = SimpleUser.builder("testuser", "Test User")
.withEmail("[email protected]")
.build();
user.put("note", "This isn't specified in the schema");請注意,這些值不會進行任何驗證。這包括檢查名稱是否與已定義的其他字段衝突。因此,它可以用來覆蓋我們模式中定義的字段——包括忽略諸如type或readOnly之類的驗證規則。
JSON Schema 還支持一個更高級的概念“模式屬性”。我們將其定義為完整的屬性定義,但使用正則表達式來定義屬性名稱,而不是固定的字符串。例如,這個定義將允許我們指定note0到note9的所有字段均為string:
"patternProperties": {
"note[0-9]": {
"type": "string"
}
}
<p>Manifold JSON 對此有部分支持。它不會生成顯式代碼來處理這種情況,而是會將類型中 <em >patternProperties</em > 的存在與 <em >additionalProperties</em > 設置為 <em >true</em > 的情況相 equate。這包括即使我們明確地將 <em >additionalProperties</em > 設置為 <em >false</em > 的情況。</p>
8.4. 嵌套類型
JSON Schema 支持我們定義一個類型嵌套在另一個類型中,而不是需要在一級層面定義所有內容並使用引用。 這對於為我們的 JSON 提供本地化結構非常有用。例如:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "http://baeldung.com/manifold/User.json",
"type": "object",
"properties": {
"email": {
"type": "object",
"properties": {
"address": {
"type": "string",
"maxLength": 260
},
"verified": {
"type": "boolean"
}
},
"required": ["address", "verified"]
}
}
}
我們已經定義了一個對象,其中一個字段指向另一個對象。 隨後,我們可以使用 JSON 表示如下:
{
"email": {
"address": "[email protected]",
"verified": false
}
}Manifold JSON 對此完全支持,並且會自動生成具有適當命名的內聯類。我們可以像使用任何其他類一樣使用這些類:
User user = User.builder()
.withEmail(User.email.builder("[email protected]", false).build())
.build();
8.5. 組合
JSON Schema 支持將不同類型組合在一起,使用 allOf、anyOf 或 oneOf 關鍵字。每個關鍵字都接受一個或多個 schema,可以通過引用或直接指定的方式來定義,並且生成的 schema 必須同時滿足所有 schema、至少滿足其中一個 schema,或精確滿足其中一個 schema,具體取決於情況。 Manifold JSON 對這些關鍵字具有一定的支持。
當使用 allOf 時,Manifold 會生成一個包含所組合類型所有定義的類。如果將這些定義嵌入式地定義,系統將創建一個包含所有定義的單個類:
"allOf": [
{
"type": "object",
"properties": {
"username": {
"type": "string"
}
}
},
{
"type": "object",
"properties": {
"roles": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
]
如果使用引用進行組合,則生成的接口將擴展所有引用的類型。
"allOf": [
{"$ref": "#/definitions/user"},
{"$ref": "#/definitions/adminUser"}
]
在兩種情況下,我們都可以像它同時符合兩組定義一樣使用生成的類:
Composed.user.builder()
.withUsername("testuser")
.withRoles(List.of("admin"))
.build()如果相反,我們使用 anyOf 或 oneOf,則 Manifold 將會生成能夠接受替代選項的類型安全代碼。 這要求我們使用引用,以便 Manifold 可以推斷類型名稱:
"anyOf": [
{"$ref": "#/definitions/Dog"},
{"$ref": "#/definitions/Cat"}
]
當進行此操作時,我們的類型獲得對不同選項進行交互的類型安全方法:
Composed composed = Composed.builder()
.withAnimalAsCat(Composed.Cat.builder()
.withColor("ginger")
.build())
.build();
assertEquals("ginger", composed.getAnimalAsCat().getColor());
我們這裏可以看到,我們的構建器已經獲得了withAnimalAsCat方法——其中“Animal”部分是外部對象中的字段名稱,而“Cat”部分是從我們的定義中推斷出的類型名稱。 我們的實際對象也同樣獲得了getAnimalAsCat和setAnimalAsCat方法,其機制相同。
9. 結論
在本文中,我們對 Manifold JSON 進行了廣泛的介紹。這個庫的功能遠不止如此,不妨親自嘗試一下,看看它的強大之處。