Jackson 在 Spring Boot 中的使用小结 2

上一篇介绍了三个用于 jackson 自定义序列化的场景。这一篇继续介绍其他一些实践。同样,所有的代码都可以在GitHub找到。

清理输入数据的外层包装(unwrap)

json api 不单单是数据的输出格式为 json 通常数据的输入(POSt 或者 PUT 的 request body)也是 json 格式。很多情况下会需要默认将输入的 json 数据以一个父级对象包裹。例如在 realworld 项目的 api 规范中在创建一个 article 时,其输入的数据格式为:

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

其真正的数据被 article 这个属性包裹起来了。而在实际使用的时候,如果每次都要去自行解包这个层次实在是不够优雅。好在 jackson 可以通过配置自动帮我们 unwrap 这里的对象,只需要在 application.(yml|properties) 增加一个配置:

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

比如我有一个这样的输入格式:

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

为了对其自动解包,我们对要解析的对象提供相应的 @JsonRootName 即可:

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

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

    public String getName() {
        return name;
    }

    private WrapJson() {
    }

    ...
}

但是,要注意这个配置是全局有效的,意味着一旦设置了之后所有的解析都会尝试将数据解包,即使没有提供 @JsonRootName 的注解,其依然会尝试使用类名称等方式去解包。因此,除了这个测试使用 spring.jackson.deserialization.unwrap-root-value 外默认关闭它

处理枚举类型

在 java 为了展示的方便,我们通常是需要将枚举按照字符串来处理的,jackson 默认也是这么做的。

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

对于 order 来说,其默认的序列化为:

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

当然对其进行反序列化也是会成功的,这是处理枚举最简单的情况了,不过 jackson 还支持自定义的序列化与反序列化,比如如果我们需要将原有的枚举变成小写:

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

我们可以写自定义的 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());
    }
}

处理使用自定义的反序列化外或者我们也可以提供一个包含 @JsonCreator 标注的构造函数进行自定义的反序列化,并用上一篇提到的 @JsonValue 进行序列化:

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 有点像是 MyBatis 做映射的时候那个 <constructor> 它可以让你直接使用一个构造函数或者是静态工厂方法来构建这个对象,可以在这里做一些额外的初始化或者是默认值选定的工作,有了它在反序列化的时候就不需要那个很讨厌的默认的无参数构造函数了。

当然枚举的处理还有一些更诡异的方式,这里有讲解,我就不再赘述了。

对多态的支持

在 DDD 中有领域事件(domain event)的概念,有时候我们需要将这些事件保存下来。由于每一个事件的结构是千差万别的,不论是存储在关系型数据库还是 nosql 数据库,在将其序列化保存的时候我们需要保留其原有的类型信息以便在反序列化的时候将其解析为之前的类型。jackson 对这种多态有很好的支持。

@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 数据中,这里就是采用了 type 的属性。@JsonSubTypes 定义了一系列子类与类型的映射关系。最后 @JsonTypeName 为每一个子类型定义了其名称,与 @JsonSubTypes 相对应。

那么对于 UserCreatedEventArticleCreatedEvent 类型,其解析的 json 如下:

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

使用 mixin

这是我非常喜欢的一个功能,第一次见到是在 spring restbucks 的例子里。它有点像是 ruby 里面的 mixin 的概念,就是在不修改已知类代码、甚至是不添加任何注释的前提下为其提供 jackson 序列化的一些设定。在两种场景下比较适用 mixin:

  1. 你需要对一个外部库的类进行自定义的序列化和反序列化
  2. 你希望自己的业务代码不包含一丝丝技术细节:写代码的时候很希望自己创建的业务类是 POJO,一个不需要继承自特定对象甚至是不需要特定技术注解的类,它强调的是一个业务信息而不是一个技术信息

这里就提供一个解析 joda time 的 mixin 的示例,它提供了一个 DateTimeSerializerjoda.DateTime 解析为 ISO 的格式。代码见这里

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

展示空数据的策略

有这么一个对象:

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

我们希望其任何为空的数据都不再显示,即其序列化结果为:

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

而不是

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

当然,遇到 null 的时候也不希望出现这样的结果:

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

为了达到这个效果我们可以为 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 还有很多其他的配置可以使用。

如果希望这个策略在我们的整个应用中都起效(而不是单个类)我们可以在 application.properties | application.yml 做配置:

spring.jackson.default-property-inclusion=non_empty

自定义标注

如果一个注解的组合频繁出现在我们的项目中,我们可以通过 @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);

对于对象 bean 来说,其解析结果为

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

相关资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,745评论 6 342
  • Jackson使用规范以及代码示例 依赖包 Maven依赖: org.codehaus.jackson jacks...
    山石水寿阅读 4,647评论 0 3
  • Json 数据格式由于其和 js 的亲密性等原因,目前算是比较主流的数据传输格式。Spring Boot 也默认集...
    eisenxu阅读 1,407评论 0 2
  • 恍惚间一下子到了五月底,此时的浏阳,并不是很热,下雨的天气,甚至带着点凉意。今天办公室里的老师说,去年的这个时候,...
    嘿那个哈密瓜是我的阅读 236评论 0 2