Jackson 在 Spring Boot 中的使用小結(jié) 2

上一篇介紹了三個(gè)用于 jackson 自定義序列化的場(chǎng)景扎唾。這一篇繼續(xù)介紹其他一些實(shí)踐。同樣,所有的代碼都可以在GitHub找到裸违。

清理輸入數(shù)據(jù)的外層包裝(unwrap)

json api 不單單是數(shù)據(jù)的輸出格式為 json 通常數(shù)據(jù)的輸入(POSt 或者 PUT 的 request body)也是 json 格式。很多情況下會(huì)需要默認(rèn)將輸入的 json 數(shù)據(jù)以一個(gè)父級(jí)對(duì)象包裹本昏。例如在 realworld 項(xiàng)目的 api 規(guī)范中在創(chuàng)建一個(gè) article 時(shí)供汛,其輸入的數(shù)據(jù)格式為:

{
  "article": {
    "title": "How to train your dragon",
    "description": "Ever wonder how?",
    "body": "You have to believe",
    "tagList": ["reactjs", "angularjs", "dragons"]
  }
}

其真正的數(shù)據(jù)被 article 這個(gè)屬性包裹起來(lái)了。而在實(shí)際使用的時(shí)候涌穆,如果每次都要去自行解包這個(gè)層次實(shí)在是不夠優(yōu)雅怔昨。好在 jackson 可以通過(guò)配置自動(dòng)幫我們 unwrap 這里的對(duì)象,只需要在 application.(yml|properties) 增加一個(gè)配置:

spring.jackson.deserialization.unwrap-root-value=true

比如我有一個(gè)這樣的輸入格式:

{
  "wrap": {
    "name": "name"
  }
}

為了對(duì)其自動(dòng)解包宿稀,我們對(duì)要解析的對(duì)象提供相應(yīng)的 @JsonRootName 即可:

@JsonRootName("wrap")
public class WrapJson {
    private String name;

    public WrapJson(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    private WrapJson() {
    }

    ...
}

但是趁舀,要注意這個(gè)配置是全局有效的,意味著一旦設(shè)置了之后所有的解析都會(huì)嘗試將數(shù)據(jù)解包祝沸,即使沒(méi)有提供 @JsonRootName 的注解矮烹,其依然會(huì)嘗試使用類名稱等方式去解包越庇。因此,除了這個(gè)測(cè)試使用 spring.jackson.deserialization.unwrap-root-value 外默認(rèn)關(guān)閉它奉狈。

處理枚舉類型

在 java 為了展示的方便卤唉,我們通常是需要將枚舉按照字符串來(lái)處理的,jackson 默認(rèn)也是這么做的嘹吨。

OrderStatus:

public enum OrderStatus {
    UNPAID, PREPARING, COMPLETED, CANCELED
}

OrderWithStatus:

class OrderWithStatus {
    private OrderStatus status;
    private String id;

    public OrderWithStatus(OrderStatus status, String id) {
        this.status = status;
        this.id = id;
    }

    private OrderWithStatus() {

    }

    public OrderStatus getStatus() {
        return status;
    }

    public String getId() {
        return id;
    }

    ...
}

OrderWithStatus order = new OrderWithStatus(OrderStatus.UNPAID, "123");

對(duì)于 order 來(lái)說(shuō)搬味,其默認(rèn)的序列化為:

{
  "id": "123",
  "status": "UNPAID"
}

當(dāng)然對(duì)其進(jìn)行反序列化也是會(huì)成功的,這是處理枚舉最簡(jiǎn)單的情況了蟀拷,不過(guò) jackson 還支持自定義的序列化與反序列化碰纬,比如如果我們需要將原有的枚舉變成小寫(xiě):

{
  "id": "123",
  "status": "unpaid"
}

我們可以寫(xiě)自定義的 serializer 和 deserializer:

@JsonSerialize(using = OrderStatusSerializer.class)
@JsonDeserialize(using = OrderStatusDeserializer.class)
public enum OrderStatus {
    UNPAID, PREPARING, COMPLETED, CANCELED;
}

public class OrderStatusSerializer extends StdSerializer<OrderStatus> {
    public OrderStatusSerializer(Class<OrderStatus> t) {
        super(t);
    }

    public OrderStatusSerializer() {
        super(OrderStatus.class);
    }

    @Override
    public void serialize(OrderStatus value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeString(value.toString().toLowerCase());
    }
}

public class OrderStatusDeserializer extends StdDeserializer<OrderStatus> {
    public OrderStatusDeserializer(Class<?> vc) {
        super(vc);
    }

    public OrderStatusDeserializer() {
        super(OrderStatus.class);
    }

    @Override
    public OrderStatus deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        return OrderStatus.valueOf(p.getText().toUpperCase());
    }
}

處理使用自定義的反序列化外或者我們也可以提供一個(gè)包含 @JsonCreator 標(biāo)注的構(gòu)造函數(shù)進(jìn)行自定義的反序列化,并用上一篇提到的 @JsonValue 進(jìn)行序列化:

public enum OrderStatus {
    UNPAID, PREPARING, COMPLETED, CANCELED;

    @JsonCreator
    public static OrderStatus fromValue(@JsonProperty("status") String value) {
        return valueOf(value.toUpperCase());
    }

    @JsonValue
    public String ofValue() {
        return this.toString().toLowerCase();
    }
}

@JsonCreator 有點(diǎn)像是 MyBatis 做映射的時(shí)候那個(gè) <constructor> 它可以讓你直接使用一個(gè)構(gòu)造函數(shù)或者是靜態(tài)工廠方法來(lái)構(gòu)建這個(gè)對(duì)象问芬,可以在這里做一些額外的初始化或者是默認(rèn)值選定的工作悦析,有了它在反序列化的時(shí)候就不需要那個(gè)很討厭的默認(rèn)的無(wú)參數(shù)構(gòu)造函數(shù)了。

當(dāng)然枚舉的處理還有一些更詭異的方式此衅,這里有講解强戴,我就不再贅述了。

對(duì)多態(tài)的支持

在 DDD 中有領(lǐng)域事件(domain event)的概念挡鞍,有時(shí)候我們需要將這些事件保存下來(lái)骑歹。由于每一個(gè)事件的結(jié)構(gòu)是千差萬(wàn)別的,不論是存儲(chǔ)在關(guān)系型數(shù)據(jù)庫(kù)還是 nosql 數(shù)據(jù)庫(kù)墨微,在將其序列化保存的時(shí)候我們需要保留其原有的類型信息以便在反序列化的時(shí)候?qū)⑵浣馕鰹橹暗念愋偷烂摹ackson 對(duì)這種多態(tài)有很好的支持。

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "type"
)
@JsonSubTypes({
    @JsonSubTypes.Type(value = UserCreatedEvent.class, name = "user_created"),
    @JsonSubTypes.Type(value = ArticleCreatedEvent.class, name = "article_created")
})
public abstract class Event {
}

@JsonTypeName("user_created")
class UserCreatedEvent extends Event {
}

@JsonTypeName("article_created")
class ArticleCreatedEvent extends Event {
}

其中 @JsonTypeInfo 定義類型信息以什么方式保留在 json 數(shù)據(jù)中翘县,這里就是采用了 type 的屬性最域。@JsonSubTypes 定義了一系列子類與類型的映射關(guān)系。最后 @JsonTypeName 為每一個(gè)子類型定義了其名稱锈麸,與 @JsonSubTypes 相對(duì)應(yīng)镀脂。

那么對(duì)于 UserCreatedEventArticleCreatedEvent 類型,其解析的 json 如下:

{
  "type": "user_created"
}
{
  "type": "article_created"
}

使用 mixin

這是我非常喜歡的一個(gè)功能忘伞,第一次見(jiàn)到是在 spring restbucks 的例子里薄翅。它有點(diǎn)像是 ruby 里面的 mixin 的概念,就是在不修改已知類代碼氓奈、甚至是不添加任何注釋的前提下為其提供 jackson 序列化的一些設(shè)定翘魄。在兩種場(chǎng)景下比較適用 mixin:

  1. 你需要對(duì)一個(gè)外部庫(kù)的類進(jìn)行自定義的序列化和反序列化
  2. 你希望自己的業(yè)務(wù)代碼不包含一絲絲技術(shù)細(xì)節(jié):寫(xiě)代碼的時(shí)候很希望自己創(chuàng)建的業(yè)務(wù)類是 POJO,一個(gè)不需要繼承自特定對(duì)象甚至是不需要特定技術(shù)注解的類探颈,它強(qiáng)調(diào)的是一個(gè)業(yè)務(wù)信息而不是一個(gè)技術(shù)信息

這里就提供一個(gè)解析 joda time 的 mixin 的示例熟丸,它提供了一個(gè) DateTimeSerializerjoda.DateTime 解析為 ISO 的格式。代碼見(jiàn)這里伪节。

@Configuration
public class JacksonCustomizations {

    @Bean
    public Module realWorldModules() {
        return new RealWorldModules();
    }

    public static class RealWorldModules extends SimpleModule {
        public RealWorldModules() {
            addSerializer(DateTime.class, new DateTimeSerializer());
        }
    }

    public static class DateTimeSerializer extends StdSerializer<DateTime> {

        protected DateTimeSerializer() {
            super(DateTime.class);
        }

        @Override
        public void serialize(DateTime value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            if (value == null) {
                gen.writeNull();
            } else {
                gen.writeString(ISODateTimeFormat.dateTime().withZoneUTC().print(value));
            }
        }
    }

}

其他

@JsonPropertyOrder

@JsonPropertyOrder({ "name", "id" })
public class MyBean {
    public int id;
    public String name;
}

按照其指定的順序解析為:

{
    "name":"My bean",
    "id":1
}

展示空數(shù)據(jù)的策略

有這么一個(gè)對(duì)象:

User user = new User("123", "", "xu");

我們希望其任何為空的數(shù)據(jù)都不再顯示光羞,即其序列化結(jié)果為:

{
  "id": "123",
  "last_name": "xu"
}

而不是

{
  "id": "123",
  "first_name": "",
  "last_name": "xu"
}

當(dāng)然绩鸣,遇到 null 的時(shí)候也不希望出現(xiàn)這樣的結(jié)果:

{
  "id": "123",
  "first_name": null,
  "last_name": "xu"
}

為了達(dá)到這個(gè)效果我們可以為 User.java 提供 @JsonInclude(JsonInclude.Include.NON_EMPTY) 注解:

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class User {
    private String id;
    @JsonProperty("first_name")
    private String firstName;
    @JsonProperty("last_name")
    private String lastName;

    public User(String id, String firstName, String lastName) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getId() {
        return id;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}

除了 NON_EMPTY 還有很多其他的配置可以使用。

如果希望這個(gè)策略在我們的整個(gè)應(yīng)用中都起效(而不是單個(gè)類)我們可以在 application.properties | application.yml 做配置:

spring.jackson.default-property-inclusion=non_empty

自定義標(biāo)注

如果一個(gè)注解的組合頻繁出現(xiàn)在我們的項(xiàng)目中纱兑,我們可以通過(guò) @JacksonAnnotationsInside 將其打包使用:

@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonInclude(Include.NON_NULL)
@JsonPropertyOrder({ "name", "id", "dateCreated" })
public @interface CustomAnnotation {}

@CustomAnnotation
public class BeanWithCustomAnnotation {
    public int id;
    public String name;
    public Date dateCreated;
}

BeanWithCustomAnnotation bean 
      = new BeanWithCustomAnnotation(1, "My bean", null);

對(duì)于對(duì)象 bean 來(lái)說(shuō)呀闻,其解析結(jié)果為

{
    "name":"My bean",
    "id":1
}

相關(guān)資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市潜慎,隨后出現(xiàn)的幾起案子捡多,更是在濱河造成了極大的恐慌,老刑警劉巖铐炫,帶你破解...
    沈念sama閱讀 222,378評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件垒手,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡倒信,警方通過(guò)查閱死者的電腦和手機(jī)科贬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)鳖悠,“玉大人榜掌,你說(shuō)我怎么就攤上這事〕俗郏” “怎么了憎账?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,983評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)卡辰。 經(jīng)常有香客問(wèn)我胞皱,道長(zhǎng),這世上最難降的妖魔是什么看政? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,938評(píng)論 1 299
  • 正文 為了忘掉前任朴恳,我火速辦了婚禮抄罕,結(jié)果婚禮上允蚣,老公的妹妹穿的比我還像新娘。我一直安慰自己呆贿,他們只是感情好嚷兔,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著做入,像睡著了一般冒晰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上竟块,一...
    開(kāi)封第一講書(shū)人閱讀 52,549評(píng)論 1 312
  • 那天壶运,我揣著相機(jī)與錄音,去河邊找鬼浪秘。 笑死蒋情,一個(gè)胖子當(dāng)著我的面吹牛埠况,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播棵癣,決...
    沈念sama閱讀 41,063評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼辕翰,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了狈谊?” 一聲冷哼從身側(cè)響起喜命,我...
    開(kāi)封第一講書(shū)人閱讀 39,991評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎河劝,沒(méi)想到半個(gè)月后壁榕,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,522評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赎瞎,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評(píng)論 3 342
  • 正文 我和宋清朗相戀三年护桦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片煎娇。...
    茶點(diǎn)故事閱讀 40,742評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡二庵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缓呛,到底是詐尸還是另有隱情催享,我是刑警寧澤,帶...
    沈念sama閱讀 36,413評(píng)論 5 351
  • 正文 年R本政府宣布哟绊,位于F島的核電站因妙,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏票髓。R本人自食惡果不足惜攀涵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望洽沟。 院中可真熱鬧以故,春花似錦、人聲如沸裆操。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,572評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)踪区。三九已至昆烁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間缎岗,已是汗流浹背静尼。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,671評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鼠渺。 一個(gè)月前我還...
    沈念sama閱讀 49,159評(píng)論 3 378
  • 正文 我出身青樓蜗元,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親系冗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子奕扣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)掌敬,斷路器惯豆,智...
    卡卡羅2017閱讀 134,714評(píng)論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,859評(píng)論 6 342
  • Jackson使用規(guī)范以及代碼示例 依賴包 Maven依賴: org.codehaus.jackson jacks...
    山石水壽閱讀 4,692評(píng)論 0 3
  • Json 數(shù)據(jù)格式由于其和 js 的親密性等原因,目前算是比較主流的數(shù)據(jù)傳輸格式奔害。Spring Boot 也默認(rèn)集...
    eisenxu閱讀 1,418評(píng)論 0 2
  • 恍惚間一下子到了五月底楷兽,此時(shí)的瀏陽(yáng),并不是很熱华临,下雨的天氣芯杀,甚至帶著點(diǎn)涼意。今天辦公室里的老師說(shuō)雅潭,去年的這個(gè)時(shí)候揭厚,...
    嘿那個(gè)哈密瓜是我的閱讀 240評(píng)論 0 2