差異處
這裏顯示兩個版本的差異處。
下次修改 | 前次修改 | ||
java:jackson:deepclone [2021/07/03 16:58] tony 建立 |
java:jackson:deepclone [2023/06/25 09:48] (目前版本) |
||
---|---|---|---|
行 2: | 行 2: | ||
====== DeepClone with Jackson ====== | ====== DeepClone with Jackson ====== | ||
===== Problem ===== | ===== Problem ===== | ||
- | 最近因為有人code沒寫好的原因,造成了Optional之亂。原始問題發生在要透過Gson對使用Guava Optional做為member的物件做deepClone時,會拋出例外: | + | <del>最近因為有人code沒寫好的原因,造成了Optional之亂;而始作俑者聲稱這問題很難解,促成我寫這篇文章的原因。</del>原始問題發生在透過Gson對有Guava Optional做為member的物件做deepClone時,會拋出例外: |
<code java> | <code java> | ||
public class ParentObject { | public class ParentObject { | ||
行 11: | 行 11: | ||
{{:java:jackson:gson_failed_with_guava_optional.png?800|}}\\ | {{:java:jackson:gson_failed_with_guava_optional.png?800|}}\\ | ||
\\ | \\ | ||
- | - 網上已有許多針對Optional使用的討論,這裡不涉及使用的議題。 | + | 這個問題發生在Gson無法將Json內容轉回Guava Optional的物件。首先網上已有許多針對Optional使用的討論,這裡不涉及使用的議題;而以Gson來說,可以透過TypeAdapter去解決物件轉換的問題,請參考此[[https://stackoverflow.com/questions/12161366/how-to-serialize-optionalt-classes-with-gson|link]]。\\ |
- | - 以Gson來說,可以透過TypeAdapter去解決物件轉換的問題,可以參考此[[[[https://stackoverflow.com/questions/12161366/how-to-serialize-optionalt-classes-with-gson|link]]。 | + | \\ |
- | - 本篇文章主要分享Jackson的解法。詳細程式碼可以參考[[https://github.com/frank007love/JacksonPractice/tree/master/src/test/java/org/tonylin/practice/jackson/optional|link]]。 | + | Jackson會有相同問題,本篇文章主要分享Jackson的解法。詳細程式碼可以參考[[https://github.com/frank007love/JacksonPractice/tree/master/src/test/java/org/tonylin/practice/jackson/optional|link]]。 |
===== How to? ===== | ===== How to? ===== | ||
+ | 解決Jackson Json轉換問題的方法之一,是在欄位掛載@JsonSerialize與@JsonDeserialize,可以參考[[java:jackson:annotation:jsonserialize:convertdate|link]]。今天分享給大家的,是直接使用別人的做好的模組,來減少維護的負擔。 | ||
+ | ==== Target Object ==== | ||
+ | 我想要做DeepClone的物件,會使用Guava與JDK8的Optional,而Optional中的ChildObject結構與ParentObject相同: | ||
+ | <code java> | ||
+ | public class ParentObject { | ||
+ | private Optional<ChildObject> guavaOptionalChild = Optional.absent(); | ||
+ | private java.util.Optional<ChildObject> jdkOptionalChild = java.util.Optional.empty(); | ||
+ | private String value; | ||
+ | | ||
+ | public java.util.Optional<String> getValue(){ | ||
+ | return java.util.Optional.ofNullable(value); | ||
+ | } | ||
+ | | ||
+ | public void setValue(String value) { | ||
+ | this.value = value; | ||
+ | } | ||
+ | | ||
+ | public void setGuavaOptionalChild(ChildObject childObject) { | ||
+ | guavaOptionalChild = Optional.fromNullable(childObject); | ||
+ | } | ||
+ | | ||
+ | public Optional<ChildObject> getGuavaOptionalChild(){ | ||
+ | return guavaOptionalChild; | ||
+ | } | ||
- | + | public void setJdkOptionalChild(ChildObject childObject) { | |
+ | jdkOptionalChild = java.util.Optional.ofNullable(childObject); | ||
+ | } | ||
+ | |||
+ | public java.util.Optional<ChildObject> getJdkOptionalChild(){ | ||
+ | return jdkOptionalChild; | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | ==== Gradle ==== | ||
+ | Jackson使用版本為2.12.0,會使用到以下兩個模組,用來分別處理Guava與JDK8 Optional的json轉換: | ||
+ | <code> | ||
+ | compile group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-guava', version: '2.12.0' | ||
+ | compile group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: '2.12.0' | ||
+ | </code> | ||
+ | ==== Test - ObjectToJson ==== | ||
+ | 首先是ObjectMapper的設定,會註冊GuavaModule與Jdk8Module: | ||
+ | <code java> | ||
+ | private ObjectMapper sut = new ObjectMapper(); | ||
+ | |||
+ | private void givenGuavaAndJdk8Module() { | ||
+ | sut.registerModule(new GuavaModule()); | ||
+ | sut.registerModule(new Jdk8Module()); | ||
+ | } | ||
+ | </code> | ||
+ | 在做DeepClone之前,我先針對將物件轉為json做了測試。可以發現在沒使用特製模組前,Optional欄位會被轉成present的內容;使用特製模組後,在present為false時,會轉為null: | ||
+ | <code java> | ||
+ | @Test | ||
+ | public void Should_GetJsonStringWithNullValue_When_GivenEmptyOptionalsWithCustomModule() throws JsonProcessingException { | ||
+ | givenGuavaAndJdk8Module(); | ||
+ | ParentObject parentObject = new ParentObject(); | ||
+ | parentObject.setValue("testValue"); | ||
+ | |||
+ | String result = sut.writeValueAsString(parentObject); | ||
+ | |||
+ | assertEquals("{\"guavaOptionalChild\":null,\"jdkOptionalChild\":null,\"value\":\"testValue\"}", result); | ||
+ | } | ||
+ | |||
+ | @Test | ||
+ | public void Should_GetJsonStringWithPresentValue_When_GivenEmptyOptionalsWithoutCustomModule() throws JsonProcessingException { | ||
+ | ParentObject parentObject = new ParentObject(); | ||
+ | parentObject.setValue("testValue"); | ||
+ | |||
+ | String result = sut.writeValueAsString(parentObject); | ||
+ | |||
+ | assertEquals("{\"guavaOptionalChild\":{\"present\":false},\"jdkOptionalChild\":{\"present\":false},\"value\":{\"present\":true}}", result); | ||
+ | } | ||
+ | </code> | ||
+ | ==== Test - DeepClone ==== | ||
+ | 接著開始測試DeepClone。Gson或Jackson的DeepClone原理,其實就是先將物件轉為json,再把json轉為新的物件。我這裡建了一個三層的物件,透過Jackson轉換複製物件後,再將複製物件轉為json作內容驗證: | ||
+ | <code java> | ||
+ | private ParentObject givenThreeLevelObject() { | ||
+ | ChildObject leefObject = new ChildObject(); | ||
+ | leefObject.setValue("leefTestValue"); | ||
+ | |||
+ | ChildObject childObject = new ChildObject(); | ||
+ | childObject.setValue("childTestValue"); | ||
+ | childObject.setGuavaOptionalChild(leefObject); | ||
+ | childObject.setJdkOptionalChild(leefObject); | ||
+ | |||
+ | ParentObject parentObject = new ParentObject(); | ||
+ | parentObject.setValue("testValue"); | ||
+ | parentObject.setGuavaOptionalChild(childObject); | ||
+ | parentObject.setJdkOptionalChild(childObject); | ||
+ | |||
+ | return parentObject; | ||
+ | } | ||
+ | |||
+ | @Test | ||
+ | public void Should_GetSameCopy_When_DeepCopyThreeLevelObject() throws JsonProcessingException, IllegalArgumentException { | ||
+ | givenGuavaAndJdk8Module(); | ||
+ | ParentObject parentObject = givenThreeLevelObject(); | ||
+ | |||
+ | ParentObject copy = sut.treeToValue(sut.valueToTree(parentObject), ParentObject.class); | ||
+ | |||
+ | String result = sut.writeValueAsString(copy); | ||
+ | assertEquals("{\"guavaOptionalChild\":{\"guavaOptionalChild\":{\"guavaOptionalChild\":null,\"jdkOptionalChild\":null,\"value\":\"leefTestValue\"}," | ||
+ | + "\"jdkOptionalChild\":{\"guavaOptionalChild\":null,\"jdkOptionalChild\":null,\"value\":\"leefTestValue\"},\"value\":\"childTestValue\"}," | ||
+ | + "\"jdkOptionalChild\":{\"guavaOptionalChild\":{\"guavaOptionalChild\":null,\"jdkOptionalChild\":null,\"value\":\"leefTestValue\"}," | ||
+ | + "\"jdkOptionalChild\":{\"guavaOptionalChild\":null,\"jdkOptionalChild\":null,\"value\":\"leefTestValue\"}," | ||
+ | + "\"value\":\"childTestValue\"},\"value\":\"testValue\"}", result); | ||
+ | } | ||
+ | </code> | ||
+ | ==== Test - Performance ==== | ||
+ | 由於是物件轉json,再由json轉物件,這點可能會讓人對效能有疑慮。因此我針對Jackson的valueToTree與writeValueAsString,還有Gson的方式做了一萬次的平均時間測量,詳細程式碼可以參考: [[https://github.com/frank007love/JacksonPractice/blob/master/src/test/java/org/tonylin/practice/jackson/optional/OptionalPerformanceTest.java|link]]。以下為結果,單位為ms: | ||
+ | <code java> | ||
+ | AvgOfTreeToValue: 0.0284 | ||
+ | AvgOfReadValue: 0.0372 | ||
+ | AvgOfGsonCopy: 0.0234 | ||
+ | </code> | ||
+ | 如果不是要求回應即時的系統,這三個值應都可以被接受。對Jackson熟悉的人,必定知道還有TokenBuffer的做法,可以提升效率;這部分之後有機會我會再提供教學。另外提供2021年針對Jackson VS Gson的實驗參考給大家: [[https://www.ericthecoder.com/2020/10/13/benchmarking-gson-vs-jackson-vs-moshi-2020/|link]],Jackson勝利。 | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | 友藏內心的獨白: 解一個問題不需要5分鐘,但寫篇教學花費我5個小時。 | ||
===== Reference ===== | ===== Reference ===== | ||
* [[https://stackoverflow.com/questions/49903859/deep-copy-using-jackson-string-or-jsonnode|Deep Copy using Jackson: String or JsonNode]] | * [[https://stackoverflow.com/questions/49903859/deep-copy-using-jackson-string-or-jsonnode|Deep Copy using Jackson: String or JsonNode]] |