3

Jackson学习笔记(一)

 2 years ago
source link: http://fancyerii.github.io/2022/03/17/jackson1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Jackson学习笔记(一) - 李理的博客

本文是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

其实使用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。



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK