DeepClone with Jackson

最近因為有人code沒寫好的原因,造成了Optional之亂;而始作俑者聲稱這問題很難解,促成我寫這篇文章的原因。原始問題發生在透過Gson對有Guava Optional做為member的物件做deepClone時,會拋出例外:

public class ParentObject {
    private Optional<ChildObject> guavaOptionalChild = Optional.absent();
    // ..
}


這個問題發生在Gson無法將Json內容轉回Guava Optional的物件。首先網上已有許多針對Optional使用的討論,這裡不涉及使用的議題;而以Gson來說,可以透過TypeAdapter去解決物件轉換的問題,請參考此link

Jackson會有相同問題,本篇文章主要分享Jackson的解法。詳細程式碼可以參考link

解決Jackson Json轉換問題的方法之一,是在欄位掛載@JsonSerialize與@JsonDeserialize,可以參考link。今天分享給大家的,是直接使用別人的做好的模組,來減少維護的負擔。

Target Object

我想要做DeepClone的物件,會使用Guava與JDK8的Optional,而Optional中的ChildObject結構與ParentObject相同:

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;
    }
}

Gradle

Jackson使用版本為2.12.0,會使用到以下兩個模組,用來分別處理Guava與JDK8 Optional的json轉換:

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'

Test - ObjectToJson

首先是ObjectMapper的設定,會註冊GuavaModule與Jdk8Module:

private ObjectMapper sut = new ObjectMapper();
 
private void givenGuavaAndJdk8Module() {
    sut.registerModule(new GuavaModule());
    sut.registerModule(new Jdk8Module());
}
在做DeepClone之前,我先針對將物件轉為json做了測試。可以發現在沒使用特製模組前,Optional欄位會被轉成present的內容;使用特製模組後,在present為false時,會轉為null:
    @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);
    }

Test - DeepClone

接著開始測試DeepClone。Gson或Jackson的DeepClone原理,其實就是先將物件轉為json,再把json轉為新的物件。我這裡建了一個三層的物件,透過Jackson轉換複製物件後,再將複製物件轉為json作內容驗證:

    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);
    }

Test - Performance

由於是物件轉json,再由json轉物件,這點可能會讓人對效能有疑慮。因此我針對Jackson的valueToTree與writeValueAsString,還有Gson的方式做了一萬次的平均時間測量,詳細程式碼可以參考: link。以下為結果,單位為ms:

AvgOfTreeToValue: 0.0284
AvgOfReadValue: 0.0372
AvgOfGsonCopy: 0.0234
如果不是要求回應即時的系統,這三個值應都可以被接受。對Jackson熟悉的人,必定知道還有TokenBuffer的做法,可以提升效率;這部分之後有機會我會再提供教學。另外提供2021年針對Jackson VS Gson的實驗參考給大家: link,Jackson勝利。


友藏內心的獨白: 解一個問題不需要5分鐘,但寫篇教學花費我5個小時。