差異處

這裏顯示兩個版本的差異處。

連向這個比對檢視

下次修改
前次修改
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]]