参考文献

什么是序列化

  • 序列化: Java中的序列化机制能够将一个实例化对象信息写入到一个字节流中(只序列号对象的属性值,而不会去序列化方法),序列化后的对象可用于网络传输,或者持久化到数据库,磁盘中.
  • 反序列化: 需要对象的时候,在通过字节流中的信息来重构一个相同的对象.

Java中具体实现

  • Java中要使一个类可以序列化,实现java.io.Serializable接口是最简单的.

    1
    2
    3
    4
    public class User implements Serializable {

    private static final long serialVersionUID = 1L;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Data
    public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    private String name;

    private String age;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    @Slf4j
    public class SerializeTest {

    public static void main(String[] args) throws Exception {
    User user = new User();
    user.setName("HoleLin");
    user.setAge("18");

    serialize(user);
    log.info("Java序列化前的结果:{} ", user);

    User deserializeUser = deserialize();
    log.info("Java反序列化的结果:{} ", deserializeUser);
    }

    /**
    * 序列化
    */
    private static void serialize(User user) throws Exception {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:\\UserSerialize.txt")));
    oos.writeObject(user);
    oos.close();
    }

    /**
    * 反序列化
    */
    private static User deserialize() throws Exception {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:\\UserSerialize.txt")));
    return (User) ois.readObject();
    }
    }

为什么要实现Serializable接口

  • 打开writeObject方法的源码看一下,发现方法中有这么一个逻辑,当要写入的对象是StringArrayEnumSerializable类型的对象则可以正常序列化,否则会抛出NotSerializableException异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* Underlying writeObject/writeUnshared implementation.
*/
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
boolean oldMode = bout.setBlockDataMode(false);
depth++;
try {
// handle previously written and non-replaceable objects
int h;
if ((obj = subs.lookup(obj)) == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}

// check for replacement object
Object orig = obj;
Class<?> cl = obj.getClass();
ObjectStreamClass desc;
for (;;) {
// REMIND: skip this check for strings/arrays?
Class<?> repCl;
desc = ObjectStreamClass.lookup(cl, true);
if (!desc.hasWriteReplaceMethod() ||
(obj = desc.invokeWriteReplace(obj)) == null ||
(repCl = obj.getClass()) == cl)
{
break;
}
cl = repCl;
}
if (enableReplace) {
Object rep = replaceObject(obj);
if (rep != obj && rep != null) {
cl = rep.getClass();
desc = ObjectStreamClass.lookup(cl, true);
}
obj = rep;
}

// if object replaced, run through original checks a second time
if (obj != orig) {
subs.assign(orig, obj);
if (obj == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
}

// remaining cases
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
} finally {
depth--;
bout.setBlockDataMode(oldMode);
}
}

为什么要显示指定serialVersionUID

  • 因为序列化对象时,如果不显示的设置serialVersionUID,Java在序列化会根据对象属性自动的生成一个serialVersionUID,在进行存储或用作网络传输.
  • 在反序列化时,会根据对象属性自动再生成一个新的serialVersionUID,和序列化是生成的serialVersionUID进行对比,两个serialVersionUID相同则反序列化成功,否则就会抛出异常.
  • 故当显示的设置serialVersionUID后,Java序列化和反序列化对象时,生成的serialVersionUID都是我们设定的serialVersionUID,这样就保证了反序列化的成功.

实际场景中遇到的问题

POJO 中的成员变量名不符合Java成员变量命名规范(驼峰式),作为接口参数无法读取的问题

环境: SpringBoot,JDK1.8

  • POJO

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Data
    public class IllegalBean {
    /**
    * 不符合驼峰命名规范: 全大写
    */
    private String NAME;
    /**
    * 不符合驼峰命名规范: 首字母大写
    */
    private String DeviceId;

    private List<POJO> data;

    public static class POJO{
    private String ADDRESS;
    private String AGE;
    }
    }
  • Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @RestController
    @RequestMapping("illegal")
    public class IllegalController {

    @PostMapping("illegal-bean")
    public IllegalBean illegal(@RequestBody IllegalBean bean) {
    return bean;
    }
    }
  • 测试: 发现传值无法被映射到对应的字段上去

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 请求体
    {
    "NAME": "NAME",
    "DeviceId": "DeviceId",
    "data": [
    {
    "ADDRESS": "address",
    "AGE": "age"
    }
    ]
    }
    // 返回值
    {
    "data": [
    {
    "age": null,
    "address": null
    }
    ],
    "deviceId": null,
    "name": null
    }
  • 问题所在: 使用@Data生成Getter/Setterget/set属性名称SpringBoot默认Jackson序列化转换获取属性不一致

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
       // ...省略
    public String getNAME() {
    return this.NAME;
    }

    public String getDeviceId() {
    return this.DeviceId;
    }

    public List<IllegalBean.POJO> getData() {
    return this.data;
    }

    public void setNAME(final String NAME) {
    this.NAME = NAME;
    }

    public void setDeviceId(final String DeviceId) {
    this.DeviceId = DeviceId;
    }
    // ...省略
  • 源码分析

    • 默认采用MappingJackson2HttpMessageConverter转换

    img

    • NAME改为name则可以映射到对应的位置

      img

      img

    • Jackson获取的属性名称是驼峰式的,故而当传NAME这种是无法映射到对应的位置上

    img

  • 解决办法

    • 方法一: 使用@JsonProperty注解

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      @Data
      public class IllegalBean {

      /**
      * 不符合驼峰命名规范: 全大写
      */
      @JsonProperty("NAME")
      private String NAME;
      /**
      * 不符合驼峰命名规范: 首字母大写
      */
      @JsonProperty("DeviceId")
      private String DeviceId;

      private List<POJO> data;
      @Data
      public static class POJO{
      @JsonProperty("ADDRESS")
      private String ADDRESS;

      @JsonProperty("AGE")
      private String AGE;
      }

      }

      img

    • 方法二: 添加阿里巴巴的FastJson

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      @Data
      public class IllegalBean {

      /**
      * 不符合驼峰命名规范: 全大写
      */
      private String NAME;
      /**
      * 不符合驼峰命名规范: 首字母大写
      */
      private String DeviceId;

      private List<POJO> data;

      @Data
      public static class POJO {
      private String ADDRESS;
      private String AGE;
      }
      }

      img

      • 但是会带来返回值会变成小写的情况,对于这种情况可以加@JsonField注解来解决
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      @Data
      public class IllegalBean {

      /**
      * 不符合驼峰命名规范: 全大写
      */
      @JSONField(name = "NAME")
      private String NAME;
      /**
      * 不符合驼峰命名规范: 首字母大写
      */
      @JSONField(name = "DeviceId")
      private String DeviceId;

      private List<POJO> data;

      @Data
      public static class POJO {

      @JSONField(name = "ADDRESS")
      private String ADDRESS;
      @JSONField(name = "AGE")
      private String AGE;

      }
      }

      img