美文网首页Spring-BootSpring Boot
Jackson 在 Spring Boot 中的使用小结 1

Jackson 在 Spring Boot 中的使用小结 1

作者: eisenxu | 来源:发表于2017-09-18 22:57 被阅读0次

    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

    相关资料

    相关文章

      网友评论

        本文标题:Jackson 在 Spring Boot 中的使用小结 1

        本文链接:https://www.haomeiwen.com/subject/josisxtx.html