上一篇介紹了三個(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ì)于 UserCreatedEvent
和 ArticleCreatedEvent
類型,其解析的 json 如下:
{
"type": "user_created"
}
{
"type": "article_created"
}
使用 mixin
這是我非常喜歡的一個(gè)功能忘伞,第一次見(jiàn)到是在 spring restbucks 的例子里薄翅。它有點(diǎn)像是 ruby 里面的 mixin 的概念,就是在不修改已知類代碼氓奈、甚至是不添加任何注釋的前提下為其提供 jackson 序列化的一些設(shè)定翘魄。在兩種場(chǎng)景下比較適用 mixin:
- 你需要對(duì)一個(gè)外部庫(kù)的類進(jìn)行自定義的序列化和反序列化
- 你希望自己的業(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è) DateTimeSerializer
將 joda.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
}