JDK序列化问题

本文探讨了Java序列化接口在实际应用中的问题,包括无法跨语言、序列化后码流过大以及效率低下,并通过实验展示了Java序列化与二进制编码在时间和空间上的显著差异。作者指出,为解决这些问题,开发者通常转向如Protobuf和Thrift等社区活跃的序列化工具。

谈到序列化我们自然想到 Java 提供的 Serializable 接口,在 Java 中我们如果需要序列化只需要继承该接口就可以通过输入输出流进行序列化和反序列化。

但是在提供很用户简单的调用的同时他也存在很多问题:

1、无法跨语言

当我们进行跨应用之间的服务调用的时候如果另外一个应用使用c语言来开发,这个时候我们发送过去的序列化对象,别人是无法进行反序列化的因为其内部实现对于别人来说完全就是黑盒。

2、序列化之后的码流太大

这个我们可以做一个实验还是上一节中的Message类,我们分别用java的序列化和使用二进制编码来做一个对比,下面我写了一个测试类:

@Test
public void testSerializable(){
    String str = "哈哈,我是一条消息";
    Message msg = new Message((byte)0xAD,35,str);
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    try {
        ObjectOutputStream os = new ObjectOutputStream(out);
        os.writeObject(msg);
        os.flush();
        byte[] b = out.toByteArray();
        System.out.println("jdk序列化后的长度: "+b.length);
        os.close();
        out.close();


        ByteBuffer buffer = ByteBuffer.allocate(1024);
        byte[] bt = msg.getMsgBody().getBytes();
        buffer.put(msg.getType());
        buffer.putInt(msg.getLength());
        buffer.put(bt);
        buffer.flip();

        byte[] result = new byte[buffer.remaining()];
        buffer.get(result);
        System.out.println("使用二进制序列化的长度:"+result.length);

    } catch (IOException e) {
        e.printStackTrace();
    }
}

 

输出结果为:

图片

 

我们可以看到差距是挺大的,目前的主流编解码框架序列化之后的码流也都比java序列化要小太多。

3、序列化效率

这个我们也可以做一个对比,还是上面写的测试代码我们循环跑100000次对比一下时间:


 
@Test
public void testSerializable(){
    String str = "哈哈,我是一条消息";
    Message msg = new Message((byte)0xAD,35,str);
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    try {
        long startTime = System.currentTimeMillis();
        for(int i = 0;i < 100000;i++){
            ObjectOutputStream os = new ObjectOutputStream(out);
            os.writeObject(msg);
            os.flush();
            byte[] b = out.toByteArray();
            /*System.out.println("jdk序列化后的长度: "+b.length);*/
            os.close();
            out.close();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("jdk序列化100000次耗时:" +(endTime - startTime));

        long startTime1 = System.currentTimeMillis();
        for(int i = 0;i < 100000;i++){
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            byte[] bt = msg.getMsgBody().getBytes();
            buffer.put(msg.getType());
            buffer.putInt(msg.getLength());
            buffer.put(bt);
            buffer.flip();

            byte[] result = new byte[buffer.remaining()];
            buffer.get(result);
            /*System.out.println("使用二进制序列化的长度:"+result.length);*/
        }
        long endTime1 = System.currentTimeMillis();
        System.out.println("使用二进制序列化100000次耗时:" +(endTime1 - startTime1));

    } catch (IOException e) {
        e.printStackTrace();
    }
}

结果为:

图片

结果为毫秒数,这个差距也是不小的。

结合以上我们看到:

目前的序列化过程中使用 Java 本身的肯定是不行,使用二进制编码的话又的我们自己去手写,所以为了让我们少搬砖前辈们早已经写好了工具让我们调用,目前社区比较活跃的有 google 的 Protobuf 和 Apache 的 Thrift。

### JDK 序列化常见问题及解决方案 #### 1. 安全性问题 JDK序列化机制存在一定的安全隐患,尤其是在处理不可信的数据源时。攻击者可以通过精心构造的恶意数据触发反序列化漏洞,从而执行任意代码或导致其他安全风险[^1]。 **解决方案**: 为了提高安全性,可以在反序列化过程中加入自定义验证逻辑。例如,在 `readObject` 方法中实现额外的安全校验,或者使用第三方库(如 Jackson、Kryo)来替代默认的 JDK 序列化方式。此外,还可以启用 Java 提供的模块化功能(如 JEP 290),它允许开发者过滤掉潜在危险的类加载行为。 #### 2. 版本兼容性问题序列化与反序列化的对象版本不一致时,可能会引发 `java.io.InvalidClassException` 异常。这是因为 JDK 默认会基于 `serialVersionUID` 来判断两个类是否属于同一版本[^2]。 **解决方案**: 为了避免此类问题的发生,建议显式声明 `serialVersionUID` 字段。即使类结构发生变化,只要不影响字段的核心语义,就可以保持该值不变;反之则需更新其数值以反映新版本的变化情况。 ```java private static final long serialVersionUID = 1L; ``` #### 3. 性能低下问题 相较于现代高效的二进制协议(比如 Protobuf 和 Avro),原生 JDK 序列化的效率较低,并且生成的结果体积较大[^3]。 **解决方案**: 对于高性能需求场景下推荐采用专用工具代替传统方法完成任务。例如 Apache Commons Lang 中提供的 SerializationUtils 类提供了更简洁易用 API 接口的同时也具备较好的运行表现;另外还有像 Google 开发出来的 Protocol Buffers (Protobuf),Facebook 发布的消息压缩算法 Thrift 等都是不错的选择。 #### 4. 进程间通信中的应用 在分布式系统架构设计当中经常涉及到不同 JVM 实例之间传递复杂业务实体的需求。此时利用 java 对象流技术能够轻松达成目标——即先将内存里的实例转换成字节数组形式发送出去后再于接收端还原回来形成新的副本[^4]。 然而需要注意的是这种方式仅适用于双方都支持相同类型的环境之中而且传输成本相对较高因此实际开发过程里应权衡利弊合理选用方案。 #### 5. 缓存框架配置冲突问题 有时候即便已经调整好了 Redis 数据存储引擎的相关参数设定仍然无法改变 Spring Cache 扩展插件所依赖的基础编码策略依旧沿用了原始模式而非预期中的 JSON 表达样式[^5]。 **解决方案**: 针对上述现象可通过如下手段加以修正:确保全局范围内唯一指定统一风格的 Converter 组件实例并将其绑定至对应的 Template 上面去覆盖原有的默认选项设置即可解决问题。 --- ### 示例代码片段展示如何正确设置 RedisTemplate 配置项: ```java @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); // 设置 key serializer StringRedisSerializer stringSerializer = new StringRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); // 设置 value serializer 使用 JacksonJson Jackson2JsonRedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jsonSerializer.setObjectMapper(objectMapper); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值