SpringBoot序列化与反序列化


本文探讨SpringBoot应用中的序列化和反序列化过程,主要是SpringBoot默认的Jackson库的注解使用和代码演示。

基础知识

序列化

序列化是指将内存中的对象状态信息转换为可以存储或传输的形式的过程,
与之对应,反序列化就是存储或传输形式转为对象状态信息的过程

序列化有多种方式,常用的有Java原生提供的字节格式和json格式。现在REST应用主要使用json格式传输数据。

jackson序列化规则

public修饰的字段序列化时会直接读取字段值,不需要有get方法。如果有get方法的话,以get方法返回的字段值优先。

默认情况下其他访问权限的属性没有get方法的话不会参与序列化。

序列化的字段名命名规则是类中的 get方法去掉get后首字母小写,与之类似,反序列化对应的是set方法。比如getUser序列化之后就是user,所以一些不标准的命名方式会出现意料之外的问题,比如``getCtime`。

双向注解

即序列化与反序列化过程中都有作用的注解,jackson官方没有对这种注解起名字,是我自己造的词

@JsonIgnoreProperties

类级别注解,两个主要作用

  1. 在序列化过程中标识忽略一个或多个属性;

  2. 反序列化过程中,表明忽略json中多余的属性,否则会报错。

第一个作用的代码示例:

@NoArgsConstructor
@JsonIgnoreProperties(value = {"gender", "age"})
public class User {
  @Getter @Setter private String name = "亚当";
  @Getter @Setter private String gender = "male";
  @Getter @Setter private int age = 0;
}

  @GetMapping("serializeJsonIgnoreProperties")
  public User serializeJsonIgnoreProperties() {
    return new User();
  }

这里因为使用了注解忽略了属性,所以只会返回name一个属性,去掉注解的话会返回全部三个属性:

//加上@JsonIgnoreProperties(value = {"gender", "age"})
{
  "name": "亚当"
}
//去掉注解以后:
{
  "name": "亚当",
  "gender": "male",
  "age": 0
}

接下来我们看第二个作用,即忽略json中多余的属性,先不使用注解:

@NoArgsConstructor
public class Person {
  @Getter @Setter private String name = "夏娃";
  @Getter @Setter private String gender = "female";
  @Getter @Setter private int age = 0;
}

  @PostMapping("deserializeJsonIgnoreProperties")
  public Person deserializeJsonIgnoreProperties(@RequestBody Person person) {
    return person;
  }

//传入
{
  "name": "demoData",
  "gender": "demoData",
  "age": 1,
  "action":"eatApple"//多余属性
}

结果我们发现即使有多余属性,也没有报错,不符合预期,难道jackson的注解失效了?我们再试一下使用ObjectMapper手动序列化

  @GetMapping("deserializeJsonIgnorePropertiesWithOM")
  public Person deserializeJsonIgnorePropertiesWithOM() throws JsonProcessingException {
    String json =
        "{\"name\": \"demoData\",\n"
            + "\"gender\": \"demoData\",\n"
            + "\"age\": 1,\n"
            + "\"action\":\"eatApple\"\n }";
    return new ObjectMapper().readValue(json, Person.class);
  }

//报错
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "action" (class com.hans.jackson.vo.jsonignoreproperties.Person), not marked as ignorable (3 known properties: "gender", "name", "age"])
  
//加上注解,不再报错
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Person {
  @Getter @Setter private String name = "夏娃";
  @Getter @Setter private String gender = "female";
  @Getter @Setter private int age = 0;
}

至此我们得出结论,@JsonIgnoreProperties(ignoreUnknown = true)是有效的,在SpringBoot中无效是因为SpringBoot对ObjectMapper进行了定制化配置。具体参见SpringBoot官方文档

@JsonProperty,@JsonGetter,@JsonSetter

用在不标准的get或set方法上,对序列化和反序列化的字段名进行重命名。

@NoArgsConstructor
public class Student {
  @Getter @Setter private String name;
  @Getter @Setter private int age;
  @Getter @Setter private double score;
  private String grade = "三年级";

  public String getMessGrade() {
    return grade;
  }
}

  @GetMapping("serializeJsonProperty")
  public Student serializeJsonProperty() {
    return new Student();
  }

//接口返回
{
    "name": null,
    "age": 0,
    "score": 0.0,
    "messGrade": "三年级"
}

修改实体,加上@JsonProperty注解

@NoArgsConstructor
public class Student {
  @Getter @Setter private String name;
  @Getter @Setter private int age;
  @Getter @Setter private double score;
  private String grade = "三年级";

  @JsonProperty("grade")
  public String getMessGrade() {
    return grade;
  }
}

//接口返回
{
    "name": null,
    "age": 0,
    "score": 0.0,
    "grade": "三年级"
}

可以看到我们重新定义了返回json的字段名,同理在set方法上使用@JsonProperty可以自定义入参json的字段名。

与之类似的还有用在get方法上的@JsonGetter和用在set方法上的@JsonSetter,这两个注解是@JsonProperty的特例,一般使用不到,官网是这样描述他们的关系的:

The @JsonGetter annotation is an alternative to the @JsonProperty annotation

值得注意的是,@JsonProperty也可以用在属性上,会将字段名重命名的同时强制使该字段可见,在字段名符合首字母小写,第二个字母大写这种情况时,序列化会多返回一个字段,所以尽量不要在字段上直接使用@JsonProperty

这里有一段网上找到的原理说明:

jackson2对pojo类型序列化的处理。

Jackson2在初始化序列器时,对pojo类型对象会收集其属性信息,属性包括成员变量及方法,然后属性名称和处理过后的方法名称做为key保存到一个LinkedHashMap中。收集过程中会调用com.fasterxml.jackson.databind.util.BeanUtil中的legacyManglePropertyName方法用来处理方法名称,它会将get/set方法前缀,即get或set去掉,并将其后面的连续大写字符转换成小写字符返回。例如: getNEWString会转变成newstring返回。你的属性名称如果有这样的"nSmallSellCount",lombok自动生成的get方法就会是这样的"getNSmallSellCount",处理过后就是这样的"nsmallSellCount",这与属性nSmallSellCount并不冲突,可以同时存在于HashMap中。收集完属性信息后,下面的步骤中会删除掉非可见的属性,一般指的是私有成员变量,这时,名称为"nSmallSellCount"的成员变量属性会被删除掉,这样的序列化结果是不会有问题的,但,如果加了@JsonProperty注释,Jackson2会认为这个属性是可见的,不必会删除,这时这两个表示同一个值得属性就会被一同序列化。

@JsonUnwrapped

使用在嵌套类上,用来将嵌套类的属性“扁平化”,官网称其为unwrapped/flattened。同时用在序列化和反序列化过程中。

需要注意的是用了注解之后反序列时,之前的结构失效。

@NoArgsConstructor
@Getter
@Setter
public class Car {
  private String name = "奔驰";
  @JsonUnwrapped private Wheel wheel = new Wheel();

  @NoArgsConstructor
  @Getter
  @Setter
  public static class Wheel {
    private String brand = "米其林";
    private double price = 10.1;
  }
}
/**序列化**/
  @GetMapping("serializeJsonUnwrapped")
  public Car serializeJsonUnwrapped() {
    return new Car();
  }

//接口返回
{
  "name": "奔驰",
  "brand": "米其林",
  "price": 10.1
}

/**反序列化**/
  @PostMapping("deserializeJsonUnwrapped")
  public Car deserializeJsonUnwrapped(@RequestBody Car car) {
    return car;
  }
//入参
{
  "name": "法拉利",
  "brand": "黑马",
  "price": 10.222
}
//返回值
{
  "name": "法拉利",
  "brand": "黑马",
  "price": 10.222
}

//入参
{
    "name":"法拉利",
    "wheel":{
        "brand":"黑马",
        "price":10.222
    }
}
//brand和price属性反序列化失败,返回值:
{
  "name": "法拉利",
  "brand": "米其林",
  "price": 10.1
}

@JsonAnyGetter,@JsonAnySetter

用在实体中的Map类型属性的get/set方法上,来使序列化/反序列化过程中的json扁平化,与@JsonUnwrapped作用类似。

值得注意的是:这两个注解只能用在返回值为Map类型的方法上。

@AllArgsConstructor
public class AnytterBean {
  @Getter @Setter private String name;
  private Map<String, String> properties;

  @JsonAnyGetter
  public Map<String, String> getProperties() {
    return properties;
  }
}

  @GetMapping("serializeAnytterBean")
  public AnytterBean serializeAnytterBean() {
    HashMap<String, String> propertiesMap = new HashMap<>();
    propertiesMap.put("game", "合金装备");
    propertiesMap.put("gender", "女");

    return new AnytterBean("静静", propertiesMap);
  }

//返回值
{
  "name": "静静",
  "game": "合金装备",
  "gender": "女"
}

//不使用注解返回值
{
  "name": "静静",
  "properties": {
    "game": "合金装备",
    "gender": "女"
  }
}

@JsonSerialize,@JsonDeserialize

通过实现StdSerializerStdDeSerializer抽象类方法,以自定义序列化和反序列化规则。然后在字段上表明注解使用这个自定义规则。

public class CustomDateSerializer extends StdSerializer<Date> {
    private static SimpleDateFormat formatter 
      = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
 
    @Override
    public void serialize(
      Date value, JsonGenerator gen, SerializerProvider arg2) 
      throws IOException, JsonProcessingException {
        gen.writeString(formatter.format(value));
    }
}

public class EventWithSerializer {
    public String name;
 
    @JsonSerialize(using = CustomDateSerializer.class)
    public Date eventDate;
}

@JsonView

一个很有用的注解,用在序列化和反序列化过程中,可以根据不同情况定制化不同的视图,视图中选择参与序列化和反序列化的具体字段,并且支持继承。

public class ViewBean {
  //声明两个视图,子类视图会继承父类的字段
    public interface BaseView {}
    public interface DetailView extends BaseView {}

    @JsonView(BaseView.class)
    public String name;
    @JsonView(BaseView.class)
    public String color;
    @JsonView(DetailView.class)
    public String detail;
}

  @GetMapping("serializeViewBean")
  @JsonView(ViewBean.BaseView.class)//指定父类视图
  public ViewBean serializeViewBean() {
    ViewBean viewBean = new ViewBean();
    viewBean.name = "名字";
    viewBean.color = "blue";
    viewBean.detail = "view";
    return viewBean;
  }
//返回值,没有包含detail字段
{
    "name": "名字",
    "color": "blue"
}

  @GetMapping("serializeViewBean")
  @JsonView(ViewBean.DetailView.class)//指定父类视图
  public ViewBean serializeViewBean() {
    ViewBean viewBean = new ViewBean();
    viewBean.name = "名字";
    viewBean.color = "blue";
    viewBean.detail = "view";
    return viewBean;
  }
//返回值,包含了全部字段
{
    "name": "名字",
    "color": "blue",
    "detail": "view"
}

值得注意的是:

  1. 如果方法上指定了具体视图,则实体中未被@JsonView修饰的属性在任何视图中都不会生效。
  2. 如果方法上没有指定任何视图,则实体中所有属性都会返回,就像实体上没有使用@JsonView注解一样。
public class ViewBean {
    public interface BaseView {}
    public interface DetailView extends BaseView {}

		//没有@JsonView注解
    public String name;
    @JsonView(BaseView.class)
    public String color;
    @JsonView(DetailView.class)
    public String detail;
}

  @GetMapping("serializeViewBean")
  @JsonView(ViewBean.DetailView.class)//指定视图
  public ViewBean serializeViewBean() {
    ViewBean viewBean = new ViewBean();
    viewBean.name = "名字";
    viewBean.color = "blue";
    viewBean.detail = "view";
    return viewBean;
  }

//返回值,没有注解的字段不返回
{
    "color": "blue",
    "detail": "view"
}


/**方法上没有指定视图**/
  @GetMapping("serializeViewBean")
  @JsonView(ViewBean.DetailView.class)//指定视图
  public ViewBean serializeViewBean() {
    ViewBean viewBean = new ViewBean();
    viewBean.name = "名字";
    viewBean.color = "blue";
    viewBean.detail = "view";
    return viewBean;
  }

//返回值,相当于实体上的注解无效。
{
    "name": "名字",
    "color": "blue",
    "detail": "view"
}

与之对应,反序列化过程中@JsonView也是类似作用

public class ViewBean {
    public interface BaseView {}
    public interface DetailView extends BaseView {}

    @JsonView(BaseView.class)
    public String name;
    @JsonView(BaseView.class)
    public String color;
    @JsonView(DetailView.class)
    public String detail;
}

  @PostMapping("deserializeViewBean")
  public ViewBean deserializeViewBean(
      @RequestBody @JsonView(ViewBean.BaseView.class) ViewBean viewBean) {
    return viewBean;
  }

//入参json
{
    "name": "名字",
    "color": "blue",
    "detail": "view"
}
//返回值,没有序列化子类字段
{
    "name": "名字",
    "color": "blue",
    "detail": null
}

序列化注解

@JsonFormat

序列化Date/Time类型的字段时,格式化为指定格式。并且字段不需要有get方法。但不能用于反序列化。

@NoArgsConstructor
public class Calendar {
  @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
  private LocalDateTime localDateTime = LocalDateTime.now();
}

//接口返回
{
  "localDateTime": "25-03-2020 06:26:24"
}

@JsonIgnore

用在属性上,表明序列化时,忽略被注解的属性

@JsonIgnoreType

用在类上,表明序列化时,包含这个类作为属性的对象会忽略该属性。也就是说@JsonIgnore是标注在忽略的属性上,@JsonIgnoreType是标注在忽略的属性的类声明上。

@JsonInclude

用在属性上,在序列化过程中,指定哪些字段参与序列化。

JsonJsonInclude.Include.ALWAYS 默认策略,任何情况下都序列化该字段
JsonJsonInclude.Include.NON_NULL这个最常用,如果字段值为null,那么就不序列化这个字段了。

JsonJsonInclude.Include.NON_EMPTY表明字段为空时,也不参与序列化。

JsonJsonInclude.Include.NON_DEFAULT表明子段是默认值的话,就不参与序列化。

@JsonAutoDetect

指定自动识别的级别,不指定的话默认只能识别public字段和get/set方法。

@JsonPropertyOrder

用在类上,在序列化过程中指定字段顺序.

@JsonPropertyOrder({ "name", "id" })
public class MyBean {
    public int id;
    public String name;
}

@JsonRawValue

用在String类型的属性上,在序列化过程中,强制将String转换为对象(即使字符串不能转换成合法对象,也会强制转换引发错误,不推荐使用)

/**不使用@JsonRawValue修饰字段的实体类**/
public class RawBean {
  public String name;
  public String json;
}

  @GetMapping("serializeJsonRawValue")
  public RawBean serializeJsonRawValue() {
    RawBean rawBean = new RawBean();
    rawBean.name = "raw";
    rawBean.json = "{\n" + "  \"name\": \"法拉利\",\n" + "  \"brand\": \"米其林\",\n" + "  \"price\": 10.1\n" + "}";
    return rawBean;
  }
//返回值
{
  "name": "raw",
  "json": "{\n  \"name\": \"法拉利\",\n  \"brand\": \"米其林\",\n  \"price\": 10.1\n}"
}

/**使用@JsonRawValue修饰**/
public class RawBean {
  public String name;
  @JsonRawValue public String json;
}
//返回值
{
  "name": "raw",
  "json": {
    "name": "法拉利",
    "brand": "米其林",
    "price": 10.1
  }
}

/**如果字符串不能从json转化成对象**/
  @GetMapping("serializeJsonRawValue")
  public RawBean serializeJsonRawValue() {
    RawBean rawBean = new RawBean();
    rawBean.name = "raw";
    rawBean.json = "messString";
    return rawBean;
  }
//返回值是不合法的json
{"name":"raw","json":messString}

@JsonValue

用在属性或get方法上,一个类只能使用一次,作用是序列化的结果只返回这一个字段值。

值得注意的是,这个注解用在属性上时对反序列化过程也有作用,但是作用有些奇怪:不会为被注解的字段进行反序列化。所以这个注解最好是用在get方法上。

@NoArgsConstructor
public class ValueBean {
  @JsonValue public String name = "dove";
  public String color = "yellow";
}

  @GetMapping("serializeJsonValue")
  public ValueBean serializeJsonValue() {
    return new ValueBean();
  }

//返回值
"dove"

/**反序列化**/
  @PostMapping("deserializeJsonValue")
  public ValueBean deserializeJsonValue(@RequestBody ValueBean valueBean) {
    return valueBean;
  }
//入参json
{
  "name": "Robert",
  "color": "oh"
}
//返回值
"dove"

@JsonRootValue

用在最外部的类上,在序列化和反序列化过程中生效,为json添加一个根节点值,用在嵌套类上没有作用。并且要设置ObjectMapper的相关属性才能激活这个注解,而SpringBoot默认没有设置属性,所以这个注解在SpringBoot应用中默认是无效的。

@NoArgsConstructor
@Getter
@Setter
@JsonRootName("root")
public class RootBean {
  private String name = "根";
  private Wheel wheel = new Wheel();

  @NoArgsConstructor
  @Getter
  @Setter
  public static class Wheel {
    private String brand = "团藏";
    private double price = 10.1;
  }
}
/**序列化**/
  @GetMapping("serializeJsonRootNameWithOM")
  public String serializeJsonRootNameWithOM() throws JsonProcessingException {
    ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.WRAP_ROOT_VALUE);
    return objectMapper.writeValueAsString(new RootBean());
  }
//返回值
{
  "RootBean": {
    "name": "根",
    "wheel": {
      "brand": "团藏",
      "price": 10.1
    }
  }
}

/**反序列化**/
  @PostMapping("deserializeJsonRootName")
  public RootBean deserializeJsonRootName() throws JsonProcessingException {
    ObjectMapper objectMapper = new ObjectMapper().enable(DeserializationFeature.UNWRAP_ROOT_VALUE);
    String json =
        "{\n"
            + "  \"RootBean\": {\n"
            + "    \"name\": \"根\",\n"
            + "    \"wheel\": {\n"
            + "      \"brand\": \"团藏\",\n"
            + "      \"price\": 10.1\n"
            + "    }\n"
            + "  }\n"
            + "}";
    return objectMapper.readValue(json, RootBean.class);
  }

反序列化注解

@JsonAlias

用在属性上,在反序列化过程中,为属性定义一个别名,在序列化中不起作用。一个字段上可以定义多个别名,在客户端传参不统一的情况下很有用。

@NoArgsConstructor
@Getter
@Setter
public class AliasBean {
  @JsonAlias({"name", "desc"})
  private String theName;

  private double price;
}

  @PostMapping("deserializeAliasBean")
  public AliasBean deserializeAliasBean(@RequestBody AliasBean aliasBean) {
    return aliasBean;
  }

//入参json
{
  "name": "name",
  "price": 1.0
}
//返回值,返回字段名不变
{
  "theName": "desc",
  "price": 1.0
}


//入参json,尝试同时传两个别名
{
  "name": "name",
  "desc": "desc",
  "price": 1.0
}

//返回值,第二个别名的值会覆盖第一个
{
  "theName": "desc",
  "price": 1.0
}

以上就是Jackson常用注解在SpringBoot中的使用实践。

参考资料:

https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/html/spring-boot-features.html#boot-features-json

https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/html/howto.html#howto-customize-the-jackson-objectmapper

https://www.baeldung.com/jackson-annotations

http://fasterxml.github.io/jackson-annotations/javadoc/2.10/

https://blog.51cto.com/luhaiyou/2369886

posted @ 2020-03-27 18:56  孔令翰  阅读(12029)  评论(1编辑  收藏  举报