Jackson学习笔记(一)

Posted by lili on March 17, 2022

本文是Jackson的学习笔记,本系列笔记主要参考了baeldung Jackson JSON TutorialA哥学Jackson等文章内容。

目录

简介

JSON是最常见的数据交换格式,说简单好像也挺简单,但是也没想象中简单。尤其是要把Java对象和它进行相互转换时问题就会变得复杂,因为毕竟Java是复杂的面向对象语言。因为一直没搞懂多态对象的序列化和反序列化问题,这次特地搜索了Jackson的不少资料,学习之后发现Jackson的功能比想象中复杂的多,所以特意记录一下。本系列笔记主要是翻译baeldung Jackson JSON Tutorial的内容,不过会提供完整的代码示例。因为原文虽然写得很好(Baeldung的文章质量都很高),但是却没有提供完整的源代码,有的时候还是运行或者调试一下代码才会更加明白。

对于大部分用户来说,使用Jackson(或者其它Json库)的目的就是序列化和反序列化POJO,所以我们首先介绍Jackson ObjectMapper,本文主要参考了Intro to the Jackson ObjectMapper

依赖

我们要使用ObjectMapper,首先就得引入如下依赖:

    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.13.2</version>
    </dependency>

jackson-databind会间接的引入jackson-annotations和jackson-core,我们这里使用的是最新的版本2.13.2。用惯了Gson或者其它Json库的读者可能觉得Jackson有点麻烦,首先就会觉得不就是一个破Json库嘛,搞那么多jar干什么。另外就是这名字也别扭,为什么叫ObjectMapper,这和Json有半毛钱关系。而且要用的话还得先new一个对象才能干活,远不如某些Fast的库一个静态方法搞定,感觉就不够快!

名词解释

这里稍微解释一下为什么要ObjectMapper的含义。首先JSON本身只是一种简单的基于文本的数据格式,和编程语言无关(虽然它起源与JavaScript,名字是JavaScript Object Notation的首字母缩写,但是标准化之后就不在隶属于某种语言了,而变成了一种标准格式,类似于Xml)。因此从解析或者生成JSON字符串的角度来看和对象甚至POJO没有什么关系。当然JSON在内存中需要一种表示方法,因为JSON支撑嵌套,所以最自然的表示方式就是Tree的形式了。JSON的原子数据类型只有字符串、数字、boolean等少数类型,然后再加上Object和Array这两种复杂(嵌套)类型,因此如果用Java来表示的话,只需要对应的原子类型以及Map<String, Object>(因为JSON的key只能是字符串)和List就行了。但是如果所有的数据结构(Java社区喜欢POJO这个奇怪的名字)都用玩意的话,代码就无法维护了。

其实使用Map<String,Object>和类的区别就类似动态语言和静态语言的区别。比如在Python里,我们随时可以往某个对象里增加任意属性。这当然用起来方便(对于写代码的人来说),但是读代码的人就惨了,谁知道名字为id的属性到底是整数还是字符串?靠规范和文档?感觉都不如静态语言的编译器检查靠谱。你的函数参数类型从整数改成了字符串,用的人还傻傻不知道,也没有任何人(或者编译器)提升它,你说这能不容易出问题吗?

所以在Python里,json库很简单:

import json

# some JSON:
x =  '{ "name":"John", "age":30, "city":"New York"}'

# parse x:
y = json.loads(x)

# the result is a Python dictionary:
print(y["age"]) 

从来没有用户说json库怎么不能把Json映射成一个Person类的实例,因为Python不需要。但是如果某个Java工程师在设计代码时说我没有一个Person对象,就用一个Map<String,Object>来表示这个Person对象把,而且约定说有3个key——”name”、”age”和”city”,并且它们的类型分别是String、int和String,估计立马就得被开除吧。

我们注意,如果不是使用流式(Streaming)解析的话,一般都需要在内存中用一棵树的数据结构来表示JSON,而再要把它转换成类的对象(POJO)则会带来额外的开销。而且即使不构建中间的树表示而直接映射成POJO,我们也需要通过反射或者类似的技术才能构造POJO并且把内容塞进去,这也会有额外的开销,而且Java的类出了名的臃肿。所以像C++这种追求极致性能的语言(但是开发者太不友好)基本也不会有把Json映射成对象的需求。比如最著名的C++ Json库之一的nlohmann/json,也只能处理没有嵌套的对象。当然这也和C++语言有关,因为在一个对象里如果要嵌套另一个对象,如果不使用指针,都是值的拷贝,这个在Java里是不支持的。比如:

class B{
  string strValue;
};
class A{
  int intValue;
  B classBValue;
};

在C++里A的内存布局就是就是把B的内容都拷贝过去了,这样的好处是A的所有成员包括classBValue都是一块连续的内存,因此访问效率更高。而使用指针通常是不推荐的用法,所以C++的类之间很少有特别复杂的嵌套关系。而在Java里,可以认为所有的对象都是引用(因为是GC来回收,所以不存在野指针的问题)。而对象的指针带来的问题就是多态的问题,Java社区又特别喜欢接口和抽象类(其实大部分人的代码从来就只有一个实现,而且我个人觉得POJO通常没有什么必要搞复杂的层次结构,要加字段加就行了,除非说这个POJO的代码不属于你。

使用ObjectMapper来进行读写

对于读来说(反序列化),readValue是最简单和常用的方法;而writeValue用于写(序列化)。在这之前,我们先定义一个Car类:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Car {
    private String color;
    private String type;
}

这里为了避免冗长的get和set方法,使用了lombok,不喜欢的读者可以自己实现(或者用IDE生成)这些方法。因为默认Jackson只序列化public的字段(或者可以通过getXXX访问的字段),所以一定要实现get和set方法。另外为了代码方便,实现了全部参数的构造函数。注意:默认Jackson使用空参数的构造函数来构造对象,因此一定要提供空参数的构造函数(后续的文章会介绍没有怎么办,比如这个类不是你控制的第三方代码)。

Java对象到JSON

ObjectMapper objectMapper = new ObjectMapper();
Car car = new Car("yellow", "renault");
objectMapper.writeValue(new File("car.json"), car);

代码构造一个ObjectMapper对象,然后调用writeValue方法就可以把Java的Car对象序列化成Json。运行后在car.json文件里的内容是:

{"color":"yellow","type":"renault"}

当然调试的时候把对象转成String会更加方便:

String carAsString = objectMapper.writeValueAsString(car);

JSON到对象

JSON到对象的转换也会简单:

String json = "{ \"color\" : \"Black\", \"type\" : \"BMW\" }";
Car car = objectMapper.readValue(json, Car.class);	

如果json存在文件里,那么也可以直接读取:

Car car = objectMapper.readValue(new File("json_car.json"), Car.class);

注意:JSON到对象的时候一定要告诉ObjectMapper要把JSON字符串转换为那个类的对象,否则它就不知道怎么办。

JSON到JsonNode

有的时候我们会从某个地方拿到一个Json,但是并没有(或者不想)用一个类来对应它,那么就可以用Jackson内部的JsonNode,它可以看成一棵树。

ObjectMapper objectMapper = new ObjectMapper();
String json = "{ \"color\" : \"Black\", \"type\" : \"FIAT\" }";
JsonNode jsonNode = objectMapper.readTree(json);
String color = jsonNode.get("color").asText();
// Output: color -> Black
System.out.println(color);

从JSON数组转换成Car的List

String jsonCarArray = 
  "[{ \"color\" : \"Black\", \"type\" : \"BMW\" }, { \"color\" : \"Red\", \"type\" : \"FIAT\" }]";
List<Car> listCar = objectMapper.readValue(jsonCarArray, new TypeReference<List<Car>>(){});

TypeReference的左右是告诉ObjectMapper List里具体的类型。除此接触它的读者可能觉得这种东西很奇怪,但是它的作用是保留泛型的信息,大家只要记住如果我想转成一个List,那么就new TypeReference<List>(){}就行了。注意这里是new一个TypeReference的子类,所以需要加{}。如果去掉{}是不行的,因为TypeReference是抽象类。

注意,如果我们只告诉ObjectMapper这是一个List,则它会把里面的对象转换成Map:

List<Map> list = objectMapper.readValue(jsonCarArray, List.class);
for(Map map:list){
    System.out.println(String.format("type=%s, color=%s", map.get("type"), map.get("color")));
}

这当然也可以,但是这就回到Python的做法了,我们定义的Car没用上。有的读者可能会问,为什么不能这样呢:

List<Car> cars = objectMapper.readValue(jsonCarArray, List<Car>.class);

大家如果试一下就会发现,这个语句无法编译,它会提示Cannot select from parameterized type。因为Java的泛型会在编译时被擦除,所以运行的时候ObjectMapper不可能知道这个List里的对象类型都是Car。

JSON对象转Map

ObjectMapper objectMapper = new ObjectMapper();
String json = "{ \"color\" : \"Black\", \"type\" : \"BMW\" }";
Map<String, Object> map
        = objectMapper.readValue(json, new TypeReference<Map<String,Object>>(){});
for(Map.Entry<String, Object> entry:map.entrySet()){
    System.out.println(entry.getKey()+": "+entry.getValue().toString());
}

其实这里用Map.class也是可以的。

高级特性

Jackson可以非常方便的通过配置来改变对Json的解析和生成。

配置序列化和反序列化特性

在默认的情况下,如果Json包含的属性要比Java对象多,则会抛出UnrecognizedPropertyException异常(如果Java对象的某个属性在Json里没有,则不会出问题)。 比如:

ObjectMapper objectMapper = new ObjectMapper();
String jsonString = "{ \"color\" : \"Black\", \"type\" : \"Fiat\", \"year\" : \"1970\" }";
try {
    objectMapper.readValue(jsonString, Car.class);
}catch(Exception e){
    e.printStackTrace();
}

运行后会出现UnrecognizedPropertyException,提示Json包含的year字段无法在Car中找到:

com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "year"

为了解决这个问题,我们可以设置DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES为false,从而忽略未知的属性:

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
String jsonString = "{ \"color\" : \"Black\", \"type\" : \"Fiat\", \"year\" : \"1970\" }";
Car car = objectMapper.readValue(jsonString, Car.class);

JsonNode jsonNodeRoot = objectMapper.readTree(jsonString);
JsonNode jsonNodeYear = jsonNodeRoot.get("year");
String year = jsonNodeYear.asText();
System.out.println("year: "+year);

上面的代码就不会抛出异常,当然如果我们想要读取year这个属性,通过Car类的反序列化肯定是不行的了,如果我们不想(或者不能)修改Car类,那么就可以使用readTree的方法把Json变成一个JsonNode的树。

另外一个相关的配置是FAIL_ON_NULL_FOR_PRIMITIVES,默认为false。如果设置为true,当Java类的某个基本类型(int, boolean等等)在Json中找不到对于的属性,就会抛出异常。注意:这里是基本类型,如果是某个对象,那么即使Json里没有,也不会抛出异常。比如下面的例子:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyBean {
    private int intValue;
    private String strValue;
}


ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false);
String jsonString = "{ \"strValue\" : \"Hello world\", \"intValue\" : null}";
try {
    MyBean bean = objectMapper.readValue(jsonString, MyBean.class);
}catch(Exception e){
    e.printStackTrace();
}

比如设置FAIL_ON_NULL_FOR_PRIMITIVES,传入的intValue为null,则Java对应的intValue会设置成初始值0(基本类型无法设置成null)。如果FAIL_ON_NULL_FOR_PRIMITIVES为true,则上面的代码会抛出异常。

创建自定义的Serializer和Deserializer

这是最灵活的一种方式。比如上面的例子,我们的Car里有个type字段,但是假设Json希望把它叫做car_brand。一种方法当然是修改Java类,但是如果我们不想(或者不能)修改,则我们可以自定义Serializer来实现对象转Json时的名字修改。当然对应也需要Deserializer来反序列化。我们先看Serializer:

public class CustomCarSerializer extends StdSerializer<Car> {

    public CustomCarSerializer() {
        this(null);
    }

    public CustomCarSerializer(Class<Car> t) {
        super(t);
    }

    @Override
    public void serialize(
            Car car, JsonGenerator jsonGenerator, SerializerProvider serializer) throws IOException {
        jsonGenerator.writeStartObject();
        jsonGenerator.writeStringField("car_brand", car.getType());
        jsonGenerator.writeEndObject();
    }
}

这个类看起来有些复杂,尤其是第二个构造函数。不过我们不用管它,只需要记住我们要实现的是Car的序列化,所以继承StdSerializer时需要在<>里写上Car,而构造函数里也是改成Class,如果是序列化Plane类,那么把Car换成Plane就行了。

我们实际主要实现的是serialize方法,传入3个参数,我们这里用到的是两个。第一个是需要序列化的Car对象,第二个JsonGenerator就是负责生成Json的Generator。我们这里有3个语句:

jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("car_brand", car.getType());
jsonGenerator.writeEndObject();

第一个writeStartObject()相对于生成”{“,而writeEndObject()生成”}”,而中间的writeStringField则生成”car_brand”: “BMW”。因此最终就会生成:

{"car_brand":"BMW}

写好了Serialzier之后,我们需要通过模块化注册的方法告诉ObjectMapper,你如果碰到Car类的对象,就应该调用我这个序列化器进行序列化:

ObjectMapper mapper = new ObjectMapper();
SimpleModule module =
        new SimpleModule("CustomCarSerializer", new Version(1, 0, 0, null, null, null));
module.addSerializer(Car.class, new CustomCarSerializer());
mapper.registerModule(module);
Car car = new Car("yellow", "BMW");
String carJson = mapper.writeValueAsString(car);
System.out.println(carJson);

当然,除了通过这种方式,我们也可以通过注解的方式告诉ObjectMapper使用什么序列化器来序列化某个类,后面注解部分会介绍。

有自定义的序列化器,当然也就需要对应的反序列化器:

public class CustomCarDeserializer extends StdDeserializer<Car> {

    public CustomCarDeserializer() {
        this(null);
    }

    public CustomCarDeserializer(Class<?> vc) {
        super(vc);
    }

    @Override
    public Car deserialize(JsonParser parser, DeserializationContext deserializer) throws IOException {
        Car car = new Car();
        ObjectCodec codec = parser.getCodec();
        JsonNode node = codec.readTree(parser);

        // try catch block
        JsonNode brand = node.get("car_brand");
        String type = brand.asText();
        car.setType(type);
        
        car.setColor(node.get("color").asText());

        return car;
    }
}

Deserializer和Serializer很像,哪些奇怪的构造函数我们不用管它,只需要实现deserialize方法就行。为了解析JSON,我们首先使用JsonParser得到ObjectCodec。然后就可以使用ObjectCodec的readTree方法把parser解析成JsonNode这个树结构。ObjectCodec看起来有点复杂,我们暂时不用管它,其实我们常用的ObjectMapper就是ObjectCodec的子类。总之,我们有了JsonNode之后,就可以轻松的通过get方法得到需要的具体属性了。然后构造一个Car对象,把我们需要的属性塞进去。下面是测试代码:

        String json = "{ \"color\" : \"Black\", \"car_brand\" : \"BMW\" }";
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module =
                new SimpleModule("CustomCarDeserializer", new Version(1, 0, 0, null, null, null));
        module.addDeserializer(Car.class, new CustomCarDeserializer());
        mapper.registerModule(module);
        Car car = mapper.readValue(json, Car.class);
        System.out.println(String.format("car type=%s, color=%s", car.getType(), car.getColor()));

我们可以看到,虽然输入的JSON属性叫car_brand,我们还是把它成功的放到Car的type字段里了。

处理Date

由于Json没有日期类型,Jackson默认会把Date转换成到1970年的毫秒数。但是我们通常希望转成人类可读的字符串,这个时候可以通过ObjectMapper的setDateFormat方法设置DateFormat,从而根据我们的设定转换日期为可读的字符串。为了测试,我们首先定义一个POJO:

        ObjectMapper objectMapper = new ObjectMapper();
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        objectMapper.setDateFormat(df);
        Request request=new Request(new Car("red", "BMW"), new Date());
        String carAsString = objectMapper.writeValueAsString(request);
        System.out.println(carAsString);

这个时候的输出为:

{"car":{"color":"red","type":"BMW"},"datePurchased":"2022-03-18 16:37:36"}

我们可以看到日期变成了我们设定的格式。

处理集合

我们可以把JSON的数组转换成Java的数组:

        String jsonCarArray =
                "[{ \"color\" : \"Black\", \"type\" : \"BMW\" }, { \"color\" : \"Red\", \"type\" : \"FIAT\" }]";
        ObjectMapper objectMapper = new ObjectMapper();
        //objectMapper.configure(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY, true);
        Car[] cars = objectMapper.readValue(jsonCarArray, Car[].class);
        for(Car car:cars){
            System.out.println(String.format("car type=%s, color=%s", car.getType(), car.getColor()));
        }

也可以把JSON数组转换成List:

List<Car> listCar = objectMapper.readValue(jsonCarArray, new TypeReference<List<Car>>(){});

这个我们上面也介绍过了,为了转成List这种,我们需要使用TypeReference。