Jackson ObjectMapper 是 Java 中應用非常廣泛的序列化、反序列化的工具,它可以幫助我們簡單、快速將 Java 物件與 json 之間作轉換,就連 Spring Framework 將它作為預設轉換器。不過,一旦使用的人多,錯誤的寫法也就層出不窮,如果沒有按照正確做法,將很容易導致問題,本文將描述如何避免與改善。

問題描述

你能看出這段程式碼有什麼問題嗎 ?

public String toJson(Something something) throws JsonProcessingException {
    ObjectMapper objectMapper = new ObjectMapper();
    return objectMapper.writeValueAsString(something);
}

問題在於 new ObjectMapper()

這段程式碼是很經典的錯誤,而且這種錯誤隨處可見,很多人卻沒注意到。事實上,執行 new ObjectMapper() 是非常昂貴的,在系統遭遇高併發(High Concurrency)情況下,這種寫法很容易出現效能瓶頸。根據這篇文章的實驗,若每次序列化/反序列化都使用 new ObjectMapper,比起共用一個 ObjectMapper,執行時間至少相差五倍,因此要盡快修正。

解法

解法很簡單,根據官方文件指出,ObjectMapper 是 thread-safe,因此只要共用同一個 instance,而不要每次都 new 即可,否則代價很高。我常用的作法有:

解法1. 宣告成員變數

若你的 ObjectMapper 不需要任何 configure,其實 Spring 已經幫我們建好一個預設的,直接注入即可,當然這裡還是建議使用 Constructor Based Dependency Injection,可以看我寫的這篇文章

@Service
public class MyService {

    private final ObjectMapper objectMapper;
    
    @Autowired
    public MyService(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    public String toJson(Something something) throws JsonProcessingException {
        return objectMapper.writeValueAsString(something);
    }
}

ObjectMapper 強大的地方在於,它有很多參數可視需求設定。這種寫法可以在 new 的同時一起做 configure:

@Service
public class MyService {

    private static final ObjectMapper objectMapper =
         new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    public String toJson(Something something) throws JsonProcessingException {
        return objectMapper.writeValueAsString(something);
    }
}

解法2. @Configuration

如果你需要全域設定,或是有多個不同設定的 ObjectMapper,建議使用此方法,並且注入時要用 @Qualifier,否則將會注入預設的 ObjectMapper Bean。

@Configuration
public class JacksonConfiguration {

    @Bean("customObjectMapper")
    public ObjectMapper customObjectMapper() {
        return new ObjectMapper()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false);
    }

}

解法3. 包裝成 Util (推薦)

這是我最常用的作法,我在專案中通常都只有一個 ObjectMapper,因此全部的 class 都共用它就夠了,這時可包裝成 Util 方便全域使用。例外處理的部分,就依各專案需求而定,沒有最佳的設計,只有最適合自己的設計。

public class JsonUtil {

    private final static ObjectMapper objectMapper = 
                new ObjectMapper()
                    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    private JsonUtil() {

    }

    public static String toJson(Object obj) {
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            log.error("Occur error during parsing data to json: {}", obj, e);
            return "";
        }
    }

    public static <T> T toObject(String json, Class<T> objectClass) {
        try {
            return objectMapper.readValue(json, objectClass);
        } catch (IOException e) {
            log.error("Occur error during mapping json to an object: {}", json, e);
            return null;
        }
    }
}

使用起來非常方便簡單。

String json = JsonUtil.toJson(something);
Something something = JsonUtil.toObject(json, Something.class);

結論

盡量不要在每次序列化/反序列化使用時都 new ObjectMapper();,這樣的代價是昂貴的,比起共用同一個實例,兩者的效能可以相差很多倍。ObjectMapper 是 thread-safe 的物件,所以本文介紹的解法概念上是一樣的,就是請放心的共用同一個 ObjectMapper 實例。這是很重要的,雖然簡單但別小看它,也許一個小動作可以拯救你的一天。

References

更多你可能會感興趣的文章

Java

view: