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;
    }
}为了将其属性 firstName 和 lastName 在展示时从驼峰式转换为下划线分割,可以对需要转换的属性上配置 @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 它有 username 和 domain 两个属性。
在序列化的时候,我们其实不需要 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"
  }
}