String之Protostuff序列化踩坑

起因

在使用redis时,为了方便操作,于是写了以下帮助类,其中有个帮助函数,用来进行hmset,序列化方式是使用的Protostuff序列化.

/**
     * mset
     */
    public  void hmset(String key, Map hash, int expireSeconds) {
        ShardedJedis jedis = null;
        try {
            jedis = pool.getResource();
            byte[] keybytes = ProtostuffUtil.serialize(key);
            Map<byte[], byte[]> bytesMaps = Maps.newHashMap();
            hash.forEach((k, v) -> {
                bytesMaps.put(ProtostuffUtil.serialize(k), ProtostuffUtil.serialize(v));
            });
            jedis.hmset(keybytes, bytesMaps);
            if (expireSeconds > 0) {
                jedis.expire(keybytes, expireSeconds);
            }
        } finally {
            closeJedis(jedis);
        }
    }

这段代码大概的功能就是把参数hash中的值,每个kv序列化之后存进redis。 然而发现在hget时发现拿不到值,hgetall却显示有值;经过排查发现,是用于String的序列化问题导致的坑。。。。。。

String Protostuff序列化之hashCode

我们先来看String的equals方法,在该方法中,对比了两个string的value长度以及值,但是没有对比两个string对象的hashCode是否相等。而通常我们判断两个字符串是否相等时调用的是equals方法。
再来看hashCode方法,string重写了hashCode方法,有个hash字段来缓存已经计算的hash值,问题就出在这里;考虑以下代码:

       String abc="abc";  //此时hash字段值为0

        byte[] serialize = ProtostuffUtil.serialize(abc);

        String bbb=new String(abc.getBytes()); // 此时hash为0,abc与bbb是两个不同对象。

        byte[] serialize2=ProtostuffUtil.serialize(bbb);

        Arrays.equals(serialize,serialize2); // true

第二段

        String abc="abc";  //此时hash字段值为0

        byte[] serialize = ProtostuffUtil.serialize(abc);

        String bbb=new String(abc.getBytes()); // 此时hash为0,abc与bbb是两个不同对象。

        bbb.hashCode() ;  //修改了hash值

        abc.equals(bbb); // true

        byte[] serialize2=ProtostuffUtil.serialize(bbb);

        Arrays.equals(serialize,serialize2); // false

原因是protostuff序列化时将String的hash值也进行了序列化,直接看源码:
com.dyuproject.protostuff.runtime.MappedSchema#writeTo这个方法遍历了对象所有的字段,

public final void writeTo(Output output, T message) throws IOException
    {
        for(Field f : fields)
            f.writeTo(output, message);
    }

String之Protostuff序列化踩坑_第1张图片
而 fields又是在其构造方法中进行赋值,传入的参数最初是在这里获取的:
com.dyuproject.protostuff.runtime.RuntimeSchema#createFrom(java.lang.Class, java.util.Set

  final Map.lang.reflect.Field> fieldMap = findInstanceFields(typeClass);

由此我们知道,为啥string计算了hashCode之后,protostuff的序列化结果会不一样。那什么时候会修改字符串的hashCode呢? HashMap进行put时。String的hashCode方法在计算hash时,会把结果保存到hash字段,因此会改变该字段的值;

  static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

文章开头的代码,传入的参数一个一个HashMap,而在序列化各个字段时,是通过直接遍历进行的,因此如果key是一个string,其hashCode必定是有值的。

hash.forEach((k, v) -> {
                bytesMaps.put(ProtostuffUtil.serialize(k), ProtostuffUtil.serialize(v));
            });

如果此时一个hash值为0的字符串,即便内容相同,但是通过ProtostuffUtil序列化出来的byte数组也是和set进去的不一致,因此导致了一开始的问题。

String的Java序列化

知道了这个坑的前因后果,我们再来看看Java自带的序列化方式。

代码三

       String abc="abc";

        byte[] serialize = SerializationUtils.serialize(abc);

        String bbb=new String(abc.getBytes());

        bbb.hashCode();

        boolean equals1 = abc.equals(bbb);

        byte[] serialize2=SerializationUtils.serialize(bbb);

        boolean equals = Arrays.equals(serialize, serialize2); //true
        System.out.println(equals); 

可以看到,即便修改了hashCode之后,两个对象序列化的结果任然是一样的。原因在于:java自带的序列化对字符串String做了特殊处理。
在java.io.ObjectOutputStream#writeObject0中,如果对象实例是String,会调用java.io.ObjectOutputStream#writeString进行处理。

            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());
                }
            }

而writeString中,最终只处理了String的value值。因此Java自带的序列化方式对String的hashCode值不敏感。

总结

  • 任何序列化方式,都要小心;
  • 源码很重要,实现很重要;

你可能感兴趣的:(Java)