1. 簡介
在本教程中,我們將深入瞭解 Moshi,一個為 Java 設計的現代 JSON 庫,它將幫助我們用極小的努力在代碼中實現強大的 JSON 序列化和反序列化功能。
Moshi 的 API 相比於 Jackson 或 Gson 等其他庫更小,但功能沒有妥協。這使得它更容易集成到我們的應用程序中,並允許我們編寫更易於測試的代碼。此外,Moshi 也是一個較小的依賴項,這在某些場景下可能很重要——例如,為 Android 開發。
2. 將 Moshi 添加到我們的構建中
在我們可以使用它之前,我們首先需要在我們的 pom.xml 文件中添加 Moshi JSON 依賴項:<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
<version>1.9.2</version>
</dependency>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-adapters</artifactId>
<version>1.9.2</version>
</dependency>com.squareup.moshi:moshi 依賴項是主要庫,而com.squareup.moshi:moshi-adapters 依賴項是一些標準類型適配器——我們稍後將更詳細地探討它們。
3. 使用 Moshi 和 JSON
Moshi 允許我們將任何 Java 值轉換為 JSON 並反之亦反。我們可以在需要時,出於各種原因,進行轉換——例如,用於文件存儲、編寫 REST API 等。
Moshi 使用 JsonAdapter 類這一概念。這是一個類型安全的機制,用於將特定類序列化為 JSON 字符串,以及將 JSON 字符串反序列化為正確的類型。
public class Post {
private String title;
private String author;
private String text;
// constructor, getters and setters
}
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<Post> jsonAdapter = moshi.adapter(Post.class);一旦我們構建了 JsonAdapter,我們就可以在需要時使用它,以使用 toJson() 方法將我們的值轉換為 JSON:
Post post = new Post("My Post", "Baeldung", "This is my post");
String json = jsonAdapter.toJson(post);
// {"author":"Baeldung","text":"This is my post","title":"My Post"}當然,我們還可以使用相應的fromJson()方法將 JSON 轉換回預期的 Java 類型:
Post post = jsonAdapter.fromJson(json);
// new Post("My Post", "Baeldung", "This is my post");4. 標準 Java 類型
Moshi 內置了對標準 Java 類型的支持,將它們轉換為和從 JSON 時完全符合預期。這涵蓋了:
- 所有基本數據類型 – int, float, char 等
- 所有 Java 包裝器等效對象 – Integer, Float, Character 等
- String
- 枚舉
- 這些類型的數組
- 這些類型的標準 Java 集合 – List, Set, Map
除了這些之外,Moshi 還可以自動處理任何任意 Java Bean,將其轉換為 JSON 對象,其中值使用與其他類型相同的規則進行轉換。這意味着在 Java Bean 內部的 Java Bean 能夠正確地深度序列化。
moshi-adapters 依賴項然後為我們提供了額外的轉換規則,包括:
- 枚舉的稍微更強大的適配器 – 在從 JSON 讀取未知值時支持備用值
- java.util.Date 適配器,支持 RFC-3339 格式
需要支持必須在與 Moshi 實例註冊後才能使用。 我們很快將在為自己的自定義類型添加支持時看到這種模式:
Moshi moshi = new Moshi.builder()
.add(new Rfc3339DateJsonAdapter())
.add(CurrencyCode.class, EnumJsonAdapter.create(CurrencyCode.class).withUnknownFallback(CurrencyCode.USD))
.build()5. 自定義類型在 Moshi 中
到目前為止,我們已經獲得了完全支持將任何 Java 對象序列化為 JSON 並反序列化回 JSON 的能力。但是,這並沒有給我們對 JSON 的外觀提供多少控制,它只是通過字面意義地將 Java 對象中的每個字段都寫入 JSON 來進行序列化。雖然這種方法有效,但並不總是我們想要的。
相反,我們可以為我們自己的類型編寫自定義適配器,從而對這些類型的序列化和反序列化過程擁有精確的控制。
5.1. 簡單轉換
簡單的情況是,在 Java 類型和 JSON 類型之間進行轉換——例如,字符串。這在我們需要以特定格式表示複雜數據時非常有用。
例如,假設我們有一個 Java 類型,用於表示帖子的作者:
public class Author {
private String name;
private String email;
// constructor, getters and setters
}無需任何努力,它就會被序列化為一個 JSON 對象,包含兩個字段——name 和 email。我們想要將其序列化為一個字符串,將名稱和電子郵件地址組合在一起。
我們通過編寫一個標準類,該類包含一個用 @ToJson 註解的方法來實現這一點。
public class AuthorAdapter {
@ToJson
public String toJson(Author author) {
return author.name + " <" + author.email + ">";
}
}當然,我們需要反過來進行解析。我們需要將字符串解析回我們的 Author 對象。這通過添加一個帶有 @FromJson 註解的方法來實現:
@FromJson
public Author fromJson(String author) {
Pattern pattern = Pattern.compile("^(.*) <(.*)>$");
Matcher matcher = pattern.matcher(author);
return matcher.find() ? new Author(matcher.group(1), matcher.group(2)) : null;
}一旦完成,我們需要真正利用它。我們這樣做是在創建我們的 Moshi 時,通過將適配器添加到我們的 Moshi.Builder 中:
Moshi moshi = new Moshi.Builder()
.add(new AuthorAdapter())
.build();
JsonAdapter<Post> jsonAdapter = moshi.adapter(Post.class);現在我們可以立即將這些對象轉換為和從 JSON,並獲得我們想要的結果:
Post post = new Post("My Post", new Author("Baeldung", "[email protected]"), "This is my post");
String json = jsonAdapter.toJson(post);
// {"author":"Baeldung <[email protected]>","text":"This is my post","title":"My Post"}
Post post = jsonAdapter.fromJson(json);
// new Post("My Post", new Author("Baeldung", "[email protected]"), "This is my post");5.2. 複雜轉換
這些轉換是在 Java Bean 和 JSON 原始類型之間進行的。我們還可以將其轉換為結構化 JSON – 基本上,這允許我們將 Java 類型轉換為用於在 JSON 中渲染的不同結構。
例如,我們可能需要將 Date/Time 值渲染為三個不同的值 - 日期、時間以及時區。
使用 Moshi,我們只需要編寫一個表示所需輸出的 Java 類型,然後我們的 <em @ToJson 方法可以返回此新 Java 對象,Moshi 將使用其標準規則將該對象轉換為 JSON:
public class JsonDateTime {
private String date;
private String time;
private String timezone;
// constructor, getters and setters
}
public class JsonDateTimeAdapter {
@ToJson
public JsonDateTime toJson(ZonedDateTime input) {
String date = input.toLocalDate().toString();
String time = input.toLocalTime().toString();
String timezone = input.getZone().toString();
return new JsonDateTime(date, time, timezone);
}
}當然,以下是翻譯後的內容:
正如我們所預期的,反向操作是通過編寫一個<em>@FromJson</em>方法來實現的,該方法接受我們新的 JSON 結構化類型並返回我們所需的類型:
@FromJson
public ZonedDateTime fromJson(JsonDateTime input) {
LocalDate date = LocalDate.parse(input.getDate());
LocalTime time = LocalTime.parse(input.getTime());
ZoneId timezone = ZoneId.of(input.getTimezone());
return ZonedDateTime.of(date, time, timezone);
}我們隨後可以完全按照上述方法,將我們的 ZonedDateTime 轉換為我們的結構化輸出,以及將結構化輸出轉換回 ZonedDateTime。
Moshi moshi = new Moshi.Builder()
.add(new JsonDateTimeAdapter())
.build();
JsonAdapter<ZonedDateTime> jsonAdapter = moshi.adapter(ZonedDateTime.class);
String json = jsonAdapter.toJson(ZonedDateTime.now());
// {"date":"2020-02-17","time":"07:53:27.064","timezone":"Europe/London"}
ZonedDateTime now = jsonAdapter.fromJson(json);
// 2020-02-17T07:53:27.064Z[Europe/London]5.3. 替代類型適配器
有時,我們希望針對單個字段使用替代適配器,而不是基於字段的類型。
例如,我們可能只有特定情況需要將日期和時間渲染為自紀元以來的毫秒數,而不是 ISO-8601 字符串。
Moshi 允許我們通過使用專門標註的註解來實現這一點,然後將該註解應用於我們的字段和適配器:
@Retention(RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@JsonQualifier
public @interface EpochMillis {}這項關鍵在於 @JsonQualifier 註解,它允許Moshi 將任何帶有此註解的字段與適當的適配器方法關聯起來。
接下來,我們需要編寫一個適配器。正如往常一樣,我們有 @FromJson 和 @ToJson 方法,用於在我們的類型和 JSON 之間進行轉換:
public class EpochMillisAdapter {
@ToJson
public Long toJson(@EpochMillis Instant input) {
return input.toEpochMilli();
}
@FromJson
@EpochMillis
public Instant fromJson(Long input) {
return Instant.ofEpochMilli(input);
}
}在這裏,我們對輸入參數使用了標註,應用於 @ToJson 方法,以及 @FromJson 方法的返回值。
Moshi 現在可以使用這個適配器,或者任何也標註了 @EpochMillis 的字段:
public class Post {
private String title;
private String author;
@EpochMillis Instant posted;
// constructor, getters and setters
}我們現在能夠將我們的標註類型轉換為 JSON 格式,並在需要時將其轉換回 JSON 格式。
Moshi moshi = new Moshi.Builder()
.add(new EpochMillisAdapter())
.build();
JsonAdapter<Post> jsonAdapter = moshi.adapter(Post.class);
String json = jsonAdapter.toJson(new Post("Introduction to Moshi Json", "Baeldung", Instant.now()));
// {"author":"Baeldung","posted":1582095384793,"title":"Introduction to Moshi Json"}
Post post = jsonAdapter.fromJson(json);
// new Post("Introduction to Moshi Json", "Baeldung", Instant.now())6. 高級 JSON 處理
現在我們能夠將類型轉換為 JSON 並反向轉換,並且可以控制轉換的方式。儘管如此,在某些情況下,我們可能需要對我們的處理進行一些更高級的操作,而 Moshi 使得這些操作變得容易實現。
6.1. 重命名 JSON 字段
有時,我們需要將 JSON 字段名稱映射到我們的 Java Bean 上,以適應不同的需求。這可能僅僅是希望在 Java 中使用 camelCase 格式,而在 JSON 中使用 snake_case 格式,或者完全重命名字段以匹配所需的模式。
我們可以使用 @Json 註解來為任何 Bean 中的字段指定新的名稱:
public class Post {
private String title;
@Json(name = "authored_by")
private String author;
// constructor, getters and setters
}一旦我們完成了這些,Moshi 立即就能理解這個字段在 JSON 中具有不同的名稱:
Moshi moshi = new Moshi.Builder()
.build();
JsonAdapter<Post> jsonAdapter = moshi.adapter(Post.class);
Post post = new Post("My Post", "Baeldung");
String json = jsonAdapter.toJson(post);
// {"authored_by":"Baeldung","title":"My Post"}
Post post = jsonAdapter.fromJson(json);
// new Post("My Post", "Baeldung")6.2. 瞬態字段
在某些情況下,我們可能存在不應包含在 JSON 中的字段。Moshi 使用標準的 transient 屬性標記這些字段不應進行序列化或反序列化:
public static class Post {
private String title;
private transient String author;
// constructor, getters and setters
}我們隨後會發現,在序列化和反序列化過程中,該字段會被完全忽略:
Moshi moshi = new Moshi.Builder()
.build();
JsonAdapter<Post> jsonAdapter = moshi.adapter(Post.class);
Post post = new Post("My Post", "Baeldung");
String json = jsonAdapter.toJson(post);
// {"title":"My Post"}
Post post = jsonAdapter.fromJson(json);
// new Post("My Post", null)
Post post = jsonAdapter.fromJson("{\"author\":\"Baeldung\",\"title\":\"My Post\"}");
// new Post("My Post", null)6.3. 默認值
有時我們解析的 JSON 沒有包含 Java Bean 中所有字段的值。這沒有問題,Moshi 會盡力做到正確處理。
Moshi 在反序列化 JSON 時無法使用任何構造函數參數,但如果存在無參數構造函數,它仍然可以利用它。
這將允許我們預先填充 Java Bean,為字段提供任何必需的默認值:
public class Post {
private String title;
private String author;
private String posted;
public Post() {
posted = Instant.now().toString();
}
// getters and setters
}如果我們的解析後的JSON缺少<em>title</em>或<em>author</em>字段,則這些字段的值將為<em>null</em>。如果缺少<em>posted</em>字段,則該字段將使用當前日期和時間:
Moshi moshi = new Moshi.Builder()
.build();
JsonAdapter<Post> jsonAdapter = moshi.adapter(Post.class);
String json = "{\"title\":\"My Post\"}";
Post post = jsonAdapter.fromJson(json);
// new Post("My Post", null, "2020-02-19T07:27:01.141Z");6.4. 解析 JSON 數組
我們到目前為止所做的一切都假設我們正在將單個 JSON 對象序列化和反序列化到單個 Java Bean 中。 這種情況非常常見,但並非唯一情況。 有時我們還希望處理值的集合,這些值在我們的 JSON 中表示為數組。
當數組嵌套在我們的 Bean 內部時,無需做任何操作。 Moshi 將會自動處理。 當整個 JSON 已經是數組時,我們需要做更多的工作,因為 Java 泛型的某些限制。 我們需要以一種方式構建我們的 `JsonAdapter》,使其知道它正在反序列化一個泛型集合,以及該集合的內容。
Moshi 提供了一些幫助,用於構建一個 <em >java.lang.reflect.Type</em >>,以便我們可以將其提供給JsonAdapter>,從而提供此額外的泛型信息:
Moshi moshi = new Moshi.Builder()
.build();
Type type = Types.newParameterizedType(List.class, String.class);
JsonAdapter<List<String>> jsonAdapter = moshi.adapter(type);一旦完成,我們的適配器完全按照預期工作,並尊重這些新的通用邊界:
String json = jsonAdapter.toJson(Arrays.asList("One", "Two", "Three"));
// ["One", "Two", "Three"]
List<String> result = jsonAdapter.fromJson(json);
// Arrays.asList("One", "Two", "Three");7. 概述
我們已經看到了 Moshi 庫如何使將 Java 類轉換為 JSON 以及從 JSON 轉換為 Java 類變得非常容易,並且它具有很高的靈活性。我們可以使用此庫在任何需要將 Java 和 JSON 之間進行轉換的地方——無論是在從文件、數據庫列或 REST API 加載和保存時,都可以使用它。不妨親自嘗試一下!