“对发送的内容要保守,对接收的内容要宽松”

这种明智的建议(也称为“稳健性原则”或Postel定律)在所有应用程序之间发送消息的用例中都非常有用。 通常,这些消息具有通过HTTP发送的Json有效负载。 典型的场景包括:

  1. 客户端在Json中序列化模型,然后通过HTTP将其发送到服务器。另一方面,服务器获取消息,提取请求的主体(即我们的Json),将其反序列化回模型(可以与客户端模型不同),然后对其执行操作。

这里没有什么新鲜的。 即使在这种简单而常见的情况下,也必须做出一个重要的决定:数据格式。 在这种情况下,消息的json结构。 假设,我们正在设计一个休息端点,该端点对User执行操作。 根据我们的要求,为了建模用户,我们只需要名字和姓氏即可。 我们用户的json看起来可能像这样:



{ "firstname" : "Diego", "lastname" : "Maradona"}



基本上,我们在系统的两个部分(客户端和服务器)之间定义了一个隐式契约。 为了使他们能够整体工作,他们需要就数据格式达成一致。 即使我们的决定看起来合理,我们还是违反了另一个重要原则:

“增加未作出的决定的数量”

通过定义消息的结构,我们确定了要由客户端发送到服务器的json具有两个字段(“名字”和“姓氏”),并且只能包含这两个字段。 这恰恰是句子的第二部分,听起来像是不必要的决定。 确实,我们无缘无故地使我们的系统难以更改。 有一些缺点:

  1. 如果将来我们必须向用户添加一个新字段,则只能通过同时更改客户端和服务器并将它们一起部署来完成。客户端或服务器可能会更改其自身的模型,这将导致仅在运行时可检测到的错误(除非我们进行额外的工作来测试我们的合同)

Schema evolution

The problem is pretty common and goes under the general name of Schema evolution, definition which includes also stuff like Database schema evolution.
So it's not a big surprise that there are libraries out there which help doing that (for example Protocol buffer or Avro).
Even though I have experimented a bit with ProtoBuf, I rarely had the need to use it (mostly because it contains a super set of the features that I actually needed). Indeed, most of the java applications I have worked on already had Jackson or Gson as a dependency. It turns out that both of them can be tuned to become a tolerant reader/writer and support schema evolution.
If you are a Java programmer, you are probably familiar with Jackson or Gson, which are two popular libraries to convert Java POJO from/to Json. In case you are not, you might want to check out this tutorial. Either way, it should be pretty easy to follow the examples.

Example

The following examples show how to tune Jackson to be resilient to schema change. The examples are written in Java and you can find all the code used in the article in github.com/napicella/java-jackson-tolerant-reader.

这就是我们的用户模型的其余api的样子:



public class User {
    private String name = "";
    private String surname = "";

    public User() {
    }

    public User(String name, String surname) {
        this.name = name;
        this.surname = surname;
    }
    // setters and getters …     
}

public class User {
    private String name = "";
    private String surname = "";

    public User() {
    }

    public User(String name, String surname) {
        this.name = name;
        this.surname = surname;
    }
    // setters and getters …     
}



服务器获取json并将其反序列化为User类型。



public void handle(Request request) throws IOException {
  ObjectMapper mapper = new ObjectMapper();  
  User user = mapper.readValue(request.body(), User.class);
   // do something with User
}

public void handle(Request request) throws IOException {
  ObjectMapper mapper = new ObjectMapper();  
  User user = mapper.readValue(request.body(), User.class);
   // do something with User
}



在上面的示例中,Jackson对象映射器将request.body中的String映射到User类。 如果json与类别不符,则会引发异常。 例如,假设客户向我们发送了以下json:



{ "firstname" : "Diego", "lastname" : "Maradona", "middlename": "Armando" }



Jackson将引发异常,因为User类中未定义属性“ middlename”。

A tolerant reader - 1

我们对于它可以做些什么呢? 让我们尝试通过定义json必须至少包含“ firstname”和“ lastname”来增加未做出的决定的数量。 杰克逊允许通过两种方式进行定义:以编程方式和Java注释。 在不失一般性的前提下,我们将使用注释进行操作:



@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
//…

@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
//…



的@JsonIgnoreProperties(ignoreUnknown = true)告诉Jackson毫无例外地忽略JSON输入中的任何未知属性。



@Test
    public void test() throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        User user =
                mapper.readValue("{ \"name\" : \"Diego\", \"surname\" : \"Maradona\", \"middlename\" : \"Armando\"}",
                        User.class);

        assertThat("TOLERANT-READER. The json might contain property that are not defined in the pojo. " +
                        "Ignore them!" +
                        "How: use @JsonIgnoreProperties(ignoreUnknown = true) annotation on the POJO",
                user.getName(), is("Diego"));
    }


    @Test
    public void test() throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        User user =
                mapper.readValue("{ \"name\" : \"Diego\", \"surname\" : \"Maradona\", \"middlename\" : \"Armando\"}",
                        User.class);

        assertThat("TOLERANT-READER. The json might contain property that are not defined in the pojo. " +
                        "Ignore them!" +
                        "How: use @JsonIgnoreProperties(ignoreUnknown = true) annotation on the POJO",
                user.getName(), is("Diego"));
    }



这样,服务器将不再抱怨User类中未定义的字段。

A tolerant reader - 2

在许多情况下,服务器可以通过为属性分配默认值来处理缺少属性的事实。 为了举例说明,假设服务器可以接受一个空用户,在这种情况下,它将为名字和姓氏分配默认值。 这需要设置两件事: 1-为模型中的名字和姓氏设置默认值



@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
    private String name = "Dries";
    private String surname = "Mertens";
    // rest of the class as before


@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
    private String name = "Dries";
    private String surname = "Mertens";
    // rest of the class as before



2-告诉杰克逊不要使用@JsonInclude(JsonInclude.Include.NON_NULL)注解。



@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
    private String name = "Dries";
    private String surname = "Mertens";
    // rest of the class as before

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
    private String name = "Dries";
    private String surname = "Mertens";
    // rest of the class as before



的JsonInclude.Include.NON_NULL保证不会将null值序列化,因此服务器接收的json根本不会具有这些属性。 如果Json缺少该属性,则设置默认值会使Jackson使用该值。 结果,以下测试为绿色:



@Test
    public void test2() throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        User user = new User("Michael", null);
        String json = mapper.writeValueAsString(user);

        User deserializedUser = mapper.readValue(json, User.class);

        assertThat("TOLERANT-WRITER. Don't serialize null values. A tolerant reader prefers no value at all, " +
                        "because in that case can provide a default." +
                        "If you serialize null, there is no way for the reader to understand that actually the property" +
                        "is missing." +
                        "How: use @JsonInclude(JsonInclude.Include.NON_NULL) annotation on the POJO",
                deserializedUser.getSurname(), is("Mertens"));

        assertThat(deserializedUser.getName(), is("Michael"));
    }

    @Test
    public void test2() throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        User user = new User("Michael", null);
        String json = mapper.writeValueAsString(user);

        User deserializedUser = mapper.readValue(json, User.class);

        assertThat("TOLERANT-WRITER. Don't serialize null values. A tolerant reader prefers no value at all, " +
                        "because in that case can provide a default." +
                        "If you serialize null, there is no way for the reader to understand that actually the property" +
                        "is missing." +
                        "How: use @JsonInclude(JsonInclude.Include.NON_NULL) annotation on the POJO",
                deserializedUser.getSurname(), is("Mertens"));

        assertThat(deserializedUser.getName(), is("Michael"));
    }



Conclusions

在REST API的世界中,肯定还有很多要说的关于架构演进的事情。 我们可以用来测试此类合同的方法也很有趣,但这是另一次讨论。 当然,任何反馈将不胜感激!