Java中的序列化问题及解决

写作背景
最近同事在一个VO对象中增加了一个字段,然后老大说需要评估一下是否有序列化问题,我们是将对象序列化到redis缓存中,所以查看其是否会有序列化问题,只要知道这个是对象会不会存储在redis中,并且在redis中取出数据的时候需要反序列化。

一、序列化、反序列化
对象序列化:在Java中我们经常希望将对象存储到缓存中,或者将其写入硬盘资源中。其实这个过程就是将对象进行流化,不管是放入缓存中还是硬盘中,中间的传输过程都是以流的形式传输的。

反序列化:通过上面的对序列化的解释,我们可以轻易的推测出反序列化就是将流对象转化成对象的过程。

二、实现方式
1.实现Serializable接口(下面的论证都是基于这种方式)

2.实现Parcelable接口,是Android特有的序列化方式,可以借助工具Android Parcelable Code Generator

三、序列化字段
1.实现java.io.Serializable接口代表这个对象中的所有字段都可以序列化吗?

不是,实现java.io.Serializable接口只能说明这个对象可以别序列化,并不是代表所有字段都可以序列化。

2.如果想要序列化的对象没有实现java.io.Serializable接口会报什么错误?

将得到一个 RuntimeException 异常:主线程中出现异常 java.io.NotSerializableException。

3.如何将对象中的一个字段不进行序列化?

在这个字段上加transient关键字

4.对象中的静态变量可以进行序列化吗?

静态变量不是对象状态的一部分,因此它不参与序列化。所以将静态变量声明为transient变量是没有用处的。

5.对象的final变量可以设置为不能序列化吗?

不能,final变量将直接通过值参与序列化,所以将final变量声明为transient变量不会产生任何影响。

四、serialVersionUID
SerialVersionUID是一个标识符,当它通常使用对象的哈希码序列化时会标记在对象上。这个标识符可以加可以不加,但是会有不同的影响。

如果你不添加serialVersionUID,也不会影响使用,但是存在即合理,既然存在这个字段,那么他一定是有用的,当你序列化的时候这个UID会被写入文件,当反序列话的时候会去读取这个ID,并与反序列化的类中的UID对比,如果相同,那么反序列化就成功,如果不同,反序列化就会失败

当你不指定UID的时候,系统会根据类的结构生成相应的hash值赋值给UID,但是当你的类的结构发生变化,比如增加一个字段或者减少一个字段的时候,UID就会发生变化,那么反序列话的时候两个类的UID就不一样了,就会反序列化失败

所以手动指定UID,主要就是在类结构发生变化时,减少反序列化失败的几率(如果类发生了非常规的结构变化,比如类名变化,成员变量的类型变化,就算是指定了UID,反序列化也会失败)

使用IntelliJ IDEA自动生成serialVersionUID,这个功能IDEA是默认关闭的,开启需要进行以下步骤:

1.settings–>Editor–>Inspections,然后在右侧输入UID进行搜索

2.勾选Serializable class without 'serialVersionUID'后面的复选框,右侧Severity默认Warning即可

五、代码准备(基于redis缓存)
1.MytestVo:添加serialVersionUID

@Data
public class MytestVo implements Serializable {
 
    private static final long serialVersionUID = 1587422156784638657L;
//    private static final long serialVersionUID = 158742215678463865L;
 
    private String id;
    private String name;
    private Integer age;
}
2.MytestWithOutUID:不添加serialVersionUID

@Data
public class MytestWithOutUID implements Serializable {
    private String id;
    private String name;
    private Integer age;
 
}
3.MytsetClient

@Slf4j
public class MytsetClient extends BaseControllerUT {
    @Autowired
    private CustomizationCacheService cacheService;
 
    @Test
    public void test(){
        MytestVo vo = new MytestVo();
        vo.setId("123");
        vo.setName("huchengong");
        vo.setAge(18);
        log.info("MytestVo:{}",vo);
        cacheService.setObjectAsynEx("ccccccccccccc",30*60,vo);
    }
 
    @Test
    public void test1(){
        try {
            MytestVo mytestVo = (MytestVo) cacheService.getObject("ccccccccccccc");
            log.info("MytestVo:{}",mytestVo);
        }catch (Exception e){
            log.error("MytestVo is error");
        }
 
    }
 
    @Test
    public void test2(){
        MytestWithOutUID mytestWithOutUID = new MytestWithOutUID();
        mytestWithOutUID.setId("123");
        mytestWithOutUID.setName("huchengong");
        mytestWithOutUID.setAge(18);
        log.info("MytestVo:{}",mytestWithOutUID);
        cacheService.setObjectAsynEx("dddddddddd",30*60,mytestWithOutUID);
    }
 
    @Test
    public void test3(){
        try {
            MytestWithOutUID mytestWithOutUID = (MytestWithOutUID) cacheService.getObject("dddddddddd");
            log.info("MytestWithOutUID:{}",mytestWithOutUID);
        }catch (Exception e){
            log.error("MytestWithOutUID is error");
        }
    }
}
4.缓存工具类自己编写

六、验证
1.添加serialVersionUID

1.1.保存缓存值

1.2.增加字段——无影响

1.3.减少字段——无影响

1.4.改变字段类型(该字段有值)——异常

 1.5.改变字段类型(该字段无值)——无影响

1.6.改变类名——异常

1.7. 修改serialVersionUID——异常

2.不添加serialVersionUID

2.1.保存缓存值

 2.2.增加字段——异常

2.3.减少字段——异常

七、解决方案
通过上面的验证我们基本可以知道哪些情况下可能会发生异常,正常情况下我们建议使用serialVersionUID,因为我们日常的修改中涉及比较多是增加字段(减少字段的很少),使用了serialVersionUID,增加字段就不会抛出异常。正常的解决方案是通过catch来截获异常,因为异常返回值必定为null,需要查询数据库,然后更新缓存中的值,这个时候我们的serialVersionUID也就更新了,下次就不会出现异常了,代码如下:

 public void test1(){
        // TODO: 2019/5/24 前面的业务逻辑 
        MytestVo mytestVo = null;
        try {
            mytestVo = (MytestVo) cacheService.getObject("gggggggggg");
            log.info("MytestVo:{}",mytestVo);
        }catch (Exception e){
            log.error("MytestVo is error");
        }
        
        if (null==mytestVo){
            // TODO: 2019/5/24 正常的业务应该是查询数据库 
            mytestVo = new MytestVo();
            mytestVo.setId("456");
            mytestVo.setName("libai");
            mytestVo.setAge(60);
            cacheService.setObjectAsynEx("gggggggggg",30*60,mytestVo);
        }
 
        // TODO: 2019/5/24 后面的业务逻辑 
 
    }
上面的方式可以避免反序列化异常情况的发生,不管serialVersionUID有没有固定都是可以解决的,但是如果我们添加了serialVersionUID,只是涉及字段的增加或者减少,我们的代码就可以简化成下面这样:

public void test1(){
        // TODO: 2019/5/24 前面的业务逻辑 
        MytestVo mytestVo = (MytestVo) cacheService.getObject("gggggggggg");
        log.info("MytestVo:{}", mytestVo);
        
        if (null==mytestVo){
            // TODO: 2019/5/24 正常的业务应该是查询数据库 
            mytestVo = new MytestVo();
            mytestVo.setId("456");
            mytestVo.setName("libai");
            mytestVo.setAge(60);
            cacheService.setObjectAsynEx("gggggggggg",30*60,mytestVo);
        }
 
        // TODO: 2019/5/24 后面的业务逻辑 
 
    }
参考文档:https://www.jianshu.com/p/0034244a7115

https://baijiahao.baidu.com/s?id=1622011683975285944&wfr=spider&for=pc
————————————————
版权声明:本文为CSDN博主「hy_coming」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/hy_coming/article/details/90301468

发布了15 篇原创文章 · 获赞 15 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/a568418299/article/details/103603010
今日推荐