Jackson 在 Spring Boot 中的使用小结 1

Json 数据格式由于其和 js 的亲密性等原因,目前算是比较主流的数据传输格式。Spring Boot 也默认集成了 jackson 用于 application/json 格式的序列化与反序列化,在写这种 restful json api 的时候势必会需要使用其 api(尤其是它所支持的注解)对输入输出的数据进行各种各样的修改和加工。这里总结了一些常见到的场景、应对的方法并提供了一些额外的学习资料,写着写着发现内容还有点点多,先完成了序列化中的三个情况,后面的博客再补充。

所有的代码可以在GitHub找到。

样例的形式

和之前的博客类似,这次也是单独创建了一个用于演示的项目,然后通过测试的形式展示项目。在演示 jackson 时,为了方便测试,我们都将提供一个期望的 json 文件进行比对:

public class NormalJavaClass {
    private String name;
    private int number;

    public NormalJavaClass(String name, int number) {
        this.name = name;
        this.number = number;
    }

    public String getName() {
        return name;
    }

    public int getNumber() {
        return number;
    }
}

@RunWith(SpringRunner.class)
@JsonTest
public class NormalJavaClassTest {
    @Autowired
    private JacksonTester<NormalJavaClass> json;
    private NormalJavaClass obj;

    @Before
    public void setUp() throws Exception {
        obj = new NormalJavaClass("aisensiy", 18);
    }

    @Test
    public void should_serialize_success() throws Exception {
        assertThat(this.json.write(obj)).isEqualToJson("normaljavaclass.json");
    }
}

其中 normaljavaclass.json 为:

{
  "number": 18,
  "name": "aisensiy"
}

后面为了简洁,就不再展示这种不必要的测试了,仅仅展示最终的 json 文件。

驼峰格式变下划线格式

User.java:

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

为了将其属性 firstNamelastName 在展示时从驼峰式转换为下划线分割,可以对需要转换的属性上配置 @JsonProperty("<the_underscoe_version_name>"),其序列化结果为:

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

当然,spring boot 中还可以通过对 jackson 对配置实现驼峰到下划线格式的转换,需要在 application.properties 中增加一个 jackson 的配置:

spring.jackson.property-naming-strategy=CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES

对于 CamelExample 这样的类

public class CamelExample {
    private String aVeryLongCamelCaseName;

    public CamelExample(String aVeryLongCamelCaseName) {
        this.aVeryLongCamelCaseName = aVeryLongCamelCaseName;
    }

    public String getaVeryLongCamelCaseName() {
        return aVeryLongCamelCaseName;
    }
}

就会匹配如下的 json 文件:

{
  "a_very_long_camel_case_name": "test"
}

用单一值作为值对象的序列化结果

DDD 中提倡使用 Value Object,那么项目中可能会出现以下这样的类:

public class UserWithEmail {
    private String username;
    private Email email;

    public UserWithEmail(String username, Email email) {
        this.username = username;
        this.email = email;
    }

    public String getUsername() {
        return username;
    }

    public Email getEmail() {
        return email;
    }
}

其中 Email 是一个 Value Object 它有 usernamedomain 两个属性。

在序列化的时候,我们其实不需要 username + domain 的组合,我们想要的就是 username@domain 的样子。这个时候,我们可以用 @JsonValue 注解:

public class Email {
    private String username;
    private String domain;

    public Email(String value) {
        if (value == null) {
            throw new IllegalArgumentException();
        }

        String[] splits = value.split("@");
        if (splits.length != 2) {
            throw new IllegalArgumentException();
        }
        username = splits[0];
        domain = splits[1];
    }

    @JsonValue
    @Override
    public String toString() {
        return String.format("%s@%s", username, domain);
    }

    public String getUsername() {
        return username;
    }

    public String getDomain() {
        return domain;
    }
}

它的意思就是在序列化这个对象的时候采用标有 JsonValue 的方法的值作为其序列化的结果。

当然,另外一个比较通用的办法就是采用自定义的序列化器:

Address:

@JsonSerialize(using = AddressSerializer.class)
public class Address {
    private String City;
    private String Country;

    public Address(String city, String country) {
        City = city;
        Country = country;
    }

    public String getCity() {
        return City;
    }

    public String getCountry() {
        return Country;
    }
}

AddressSerializer:

public class AddressSerializer extends StdSerializer<Address> {

    public AddressSerializer() {
        this(Address.class);
    }

    public AddressSerializer(Class<Address> t) {
        super(t);
    }

    @Override
    public void serialize(Address value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeString(String.format("%s, %s", value.getCity(), value.getCountry()));
    }
}

要序列化的类:

public class UserWithAddress {
    private String username;
    private Address address;

    public UserWithAddress(String username, Address address) {
        this.username = username;
        this.address = address;
    }

    public String getUsername() {
        return username;
    }

    public Address getAddress() {
        return address;
    }
}

其序列化的结果为:

{
  "username": "test",
  "address": "Beijing, China"
}

提升内嵌属性的层级

如果所提供的 rest json api 需要支持 hypermedia 那么就需要将通过 GET 获取的暴露给外部的资源的状态迁移以链接的形式提供出来。这个时候在我们 API 的内部实现时需要将从数据库中查询的 Data Object 再次用一个修饰类包装(如在 spring-hateoas 中就提供了一个 Resource 的修饰器用于处理这种情况)并为其提供额外的状态迁移链接:

public class Resource<T> {
    private T content;

    private Map<String, URI> links = new TreeMap<>();

    ...

    public T getContent() {
        return content;
    }

    public Map<String, URI> getLinks() {
        return Collections.unmodifiableMap(links);
    }
}

但是这样会有一个问题:原有的 Data Object 成为了 Resource 类中的一个属性。其默认的序列化会成为这个样子:

{
  "content": {
    "property1": "xxx",
    "property2": "xxx",
    ...
  },
  "links": {
    "self": "xxx",
    ...
  }
}

我们不希望这样的代理模式导致原有的对象数据层级下降到 content 中,这里可以采用 @JsonUnwrapped 注解:

public class Resource<T> {
    private T content;

    private Map<String, URL> links = new TreeMap<>();

    ...

    @JsonUnwrapped
    public T getContent() {
        return content;
    }

    public Map<String, URL> getLinks() {
        return Collections.unmodifiableMap(links);
    }
}

对之前的 User 进行包裹:

User user = new User("123", "eisen", "xu");
Link link = new Link(
    "self",
    UriComponentsBuilder.newInstance()
        .scheme("http").host("localhost")
        .path("/users/{id}")
        .buildAndExpand(user.getId()).toUri());
userResource = new Resource<>(user, link);

其序列化的结果为:

{
  "id": "123",
  "first_name": "eisen",
  "last_name": "xu",
  "links": {
    "self": "http://localhost/users/123"
  }
}

更多信息请见 aisensiy.github.io

相关资料

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

推荐阅读更多精彩内容