08-01
08

Java Serialization UID 问题的完美解决方案

1.问题描述:
一个老版本的object "hello.HelloWorld" 序列化然后保存到数据库中了。
我们定义数据库中保存的这个对象为1.0版本。(1.0版本参考hello.HelloWorld1)

此时对老版本的object "hello.HelloWorld"进行了修改,形成了新版本的class(定义为2.0版本)
(2.0版本参考hello.HelloWorld2)
此时从数据库中去取数据,会报告如下错误:
hello.HelloWorld; local class incompatible: stream classdesc serialVersionUID = -5863503448069391657, local class serialVersionUID = 5362978033127103447

现在要求想办法将数据库中的老版本的object转化为新版的object,转化后还要求写
回数据库。

---------------------------
hello.HelloWorld1
---------------------------
package hello;

import java.io.Serializable;

public class HelloWorld1 implements Serializable
{
    private String m_sName="";
    public String getName()
    {
        return m_sName;
    }
    
    public void setName(String name){
      m_sName=name;  
    }
}

---------------------------
hello.HelloWorld2
---------------------------
package hello;

import java.io.Serializable;

public class HelloWorld2 implements Serializable
{
    private static final long serialVersionUID = 5362978033127103447L;
    private String m_sName="";
    private String property="";
    
    public String getProperty()
    {
        return property;
    }

    public void setProperty(String property)
    {
        this.property = property;
    }

    public String getName()
    {
        return m_sName;
    }
    
    public void setName(String name){
      m_sName=name;  
    }    
}

2.分析问题:
hello.HelloWorld1.java  定义了1个field,没有指定serialVersionUID,由JVM根据hash算法自动生成。
hello.HelloWorld2.java  定义了2个field,指定serialVersionUID。
实际上1.0版本的hello.HelloWorld就是hello.HelloWorld1,只是更名而已。
实际上2.0版本的hello.HelloWorld就是hello.HelloWorld2,只是更名而已。

大家可以看到1.0版本的hello.HelloWorld只有一个字段,而且它的SUID是由JVM根据class中的信息使用
hash算法自动生成的。2.0版本的hello.HelloWorld有两个字段,但是它的SUID是开发人员指定的。SUID
就是这个类的唯一标识,做反序列化操作时,首先要比较SUID是否相同,如果不相同就会报告
上面的错误。

这样一分析,大家就知道为什么会有上面Exception的抛出了。

3.提出解决问题的思路:
从文件中读取老版本(1.0版本)byte流,然后在byte流中将包含1.0版本的serialVersionUID的byte信息更改
为2.0版本的serialVersionUID的byte信息,然后deserialization,可以获得新版本的object。
只需要写一个工具类完成将bytes stream中的1.0的SUID替换为2.0的SUID。

4.代码实现:
-----------------------------------------------
SerialUIDConvertUtil.java SUID的替换:
------------------------------------------------
说明:
helloWorld1_normal.data 为1.0版本的helloWorld类所产生的序列化数据。

使用方法:(注意当前运行程序的classpath中必须包含2.0版本的helloWorld类)
FileInputStream in = new FileInputStream("./lib/helloWorld1_normal.data");
byte[] FileBytes=SerialUIDConvertUtil.getBytesFromStream(in);
HelloWorld hello = (HelloWorld) SerialUIDConvertUtil.convertNormallySerializedObject(FileBytes);
System.out.println("name:" + hello.getName());
System.out.println("property:" + hello.getProperty());

经过SerialUIDConvertUtil.convertNormallySerializedObject(FileBytes) 后所产生的helloWorld对象已经
拥有2.0版本的类信息,多了一个property字段。

package hello.serializeUtil;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;


public class SerialUIDConvertUtil
{

    /**
     *
     * @param compressedBytes
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public static Object convertCompressedSerializedObject(byte[] compressedBytes)
        throws IOException, ClassNotFoundException
    {
        if ((compressedBytes == null) || (compressedBytes.length == 0)) {
            return null;
        }

        ByteArrayInputStream inbytes = new ByteArrayInputStream(compressedBytes);
        InflaterInputStream inflating = new InflaterInputStream(inbytes);

        ObjectInputStream in = new ObjectInputStream(inflating);

        Object return_object = null;
        try {
            return_object = in.readObject();
        } catch (InvalidClassException e) {

            //先解压数据。
            ByteArrayInputStream inbytes2 = new ByteArrayInputStream(compressedBytes);
            InflaterInputStream inflating2 = new InflaterInputStream(inbytes2);

            //获得流的所有数据
            byte[] normalBytes = getBytesFromStream(inflating2);

            return_object = convertSerialID(e, normalBytes);
        }

        in.close();
        return return_object;
    }


    public static Object convertNormallySerializedObject(byte[] a_bytes)
        throws IOException, ClassNotFoundException
    {
        if ((a_bytes == null) || (a_bytes.length == 0)) {
            return null;
        }

        ByteArrayInputStream inbytes = new ByteArrayInputStream(a_bytes);
        ObjectInputStream in = new ObjectInputStream(inbytes);

        Object return_object = null;
        try {
            return_object = in.readObject();
        } catch (InvalidClassException e) {

            ByteArrayInputStream inbytes2 = new ByteArrayInputStream(a_bytes);

            //获得流的所有数据            
            byte[] normalBytes = getBytesFromStream(inbytes2);

            return_object = convertSerialID(e, normalBytes);
        }

        in.close();
        return return_object;
    }


    /**
     * compress and serialize the object
     * @param serializedObject
     * @throws IOException
     * @return byte[] serialized byte array
     */
    public static byte[] serializeObject(Object serializedObject)
        throws IOException
    {
        Deflater deflater = new Deflater();
        deflater.setStrategy(Deflater.DEFAULT_STRATEGY);
        deflater.setLevel(Deflater.BEST_COMPRESSION);

        ByteArrayOutputStream outbytes = new ByteArrayOutputStream();
        DeflaterOutputStream deflating = new DeflaterOutputStream(outbytes, deflater);
        ObjectOutputStream out = new ObjectOutputStream(deflating);

        out.writeObject(serializedObject);

        out.close();
        return outbytes.toByteArray();
    }


    /**
     * deserialize the compress bytes
     * @param bytes
     * @throws IOException
     * @throws ClassNotFoundException
     * @return Object deserialized object
     */
    public static Object deserializeObject(byte[] bytes)
        throws IOException, ClassNotFoundException
    {
        if ((bytes == null) || (bytes.length == 0)) {
            return null;
        }

        ByteArrayInputStream inbytes = new ByteArrayInputStream(bytes);
        InflaterInputStream inflating = new InflaterInputStream(inbytes);
        ObjectInputStream in = new ObjectInputStream(inflating);

        Object deserializedObject = in.readObject();

        in.close();
        return deserializedObject;
    }


    /**
     *  根据InputStream 完全读出所有的byte。算法:处理读byte流时,byte[] 动态增长的办法。
     * @param in
     * @return
     * @throws IOException
     */
    public static byte[] getBytesFromStream(InputStream in)
        throws IOException
    {

        /**
         * initSizeRepository.size()和fixedSizeReadStream.size()的数值可以随意设置
         */
        byte[] initSizeRepository = new byte[0]; //始终用于保存数据.        
        byte[] fixedSizeReadStream = new byte[1]; //始终用于读固定长度的byte流

        int index = 0;

        while (true) {
            int counter = in.read(fixedSizeReadStream);

            //如果流结束。
            if (counter == -1) {
                break;
            }

            index += counter;

            //当前读出的流的总长度〉初始存贮数据的长度时
            if (index > initSizeRepository.length) {
                int increasedSizeRepository = (initSizeRepository.length * 3) / 2 + 1; //增长因子。
                byte[] increasedBytes = new byte[increasedSizeRepository];

                //原有数据copy过来
                System.arraycopy(initSizeRepository, 0, increasedBytes, 0,
                    initSizeRepository.length);

                //append新加的数据
                System.arraycopy(fixedSizeReadStream, 0, increasedBytes, index - counter,
                    counter);
                initSizeRepository = increasedBytes;
            }
            //当前读出的流的总长度《=初始存贮数据的长度时
            else {
                //原有数据copy过来
                System.arraycopy(fixedSizeReadStream, 0, initSizeRepository, index
                        - counter, counter);
            }

        }
        in.close();

        //        System.out.println("index:"+index);

        //获得流的所有原始数据,不多包含其他多余的数据。
        byte[] realSizeRepository = new byte[index];
        System.arraycopy(initSizeRepository, 0, realSizeRepository, 0, index);

        //        System.out.println("===========");
        //        for (int i = 0; i < realSizeRepository.length; i++) {
        //            System.out.println(realSizeRepository[i]);
        //        }
        //        System.out.println("===========");

        return realSizeRepository;
    }


    /**
     *
     * @param invalidClassException
     * @param a_bytes
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private static Object convertSerialID(InvalidClassException invalidClassException,
        byte[] a_bytes)
        throws IOException, ClassNotFoundException
    {
        byte[] bytes = changeSerialID(a_bytes, invalidClassException);

        ByteArrayInputStream inputStream_bytes = new ByteArrayInputStream(bytes);
        ObjectInputStream inputStream_object = new ObjectInputStream(inputStream_bytes);

        Object return_object = null;
        try {
            return_object = inputStream_object.readObject();
        } catch (InvalidClassException e1) {
            return_object = convertSerialID(e1, bytes);
        }

        inputStream_object.close();
        return return_object;
    }


    /**
     *
     * @param a_bytes
     * @param e
     * @return
     * @throws IOException
     */
    private static byte[] changeSerialID(byte[] a_bytes,
        InvalidClassException e)
        throws IOException
    {
        String exception = e.getMessage();
        System.out.println(exception);

        int first_begin = exception.indexOf("= ");
        int first_end = exception.indexOf(",");

        String stream_class_serialVersionUID = exception.substring(first_begin + 2,
            first_end);

        int second_begin = exception.lastIndexOf("= ");
        String local_class_serialVersionUID = exception.substring(second_begin + 2);

        System.out.println("stream class UID:" + stream_class_serialVersionUID);
        System.out.println("local class UID:" + local_class_serialVersionUID);

        long stream_UID = Long.parseLong(stream_class_serialVersionUID);
        long local_UID = Long.parseLong(local_class_serialVersionUID);
        byte[] changedBytes = replaceBytes(a_bytes, stream_UID, local_UID);

        System.out.println("stream_UID:" + stream_UID);
        System.out.println("local_UID:" + local_UID);

        //       System.out.println("origin:");
        //       for (int i = 0; i < JServiceMsgBytes.length; i++) {
        //           System.out.print(JServiceMsgBytes[i]);
        //       }
        //      
        //       System.out.println();
        //       System.out.println("new:");
        //       for (int i = 0; i < changedJServiceMsgBytes.length; i++) {
        //           System.out.print(changedJServiceMsgBytes[i]);
        //       }

        return changedBytes;
    }


    /**
     *
     * @param a_value
     * @return
     */
    private static byte[] longToByte(long a_value)
    {
        byte[] bytes = new byte[8];
        for (int i = 1; i <= bytes.length; i++) {
            bytes[8 - i] = new Long(a_value).byteValue();
            a_value = a_value >> 8;
        }
        return bytes;
    }


    /**
     *
     * @param a_bytes
     * @param oldSuid
     * @param newSuid
     * @return
     */
    private static byte[] replaceBytes(byte[] a_bytes,
        long oldSuid,
        long newSuid)
    {

        byte[] bytes = new byte[a_bytes.length];
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = a_bytes[i];
        }

        byte[] oldSuidBytes = longToByte(oldSuid);

        System.out.println("oldSuid :");
        for (int i = 0; i < oldSuidBytes.length; i++) {
            System.out.print(oldSuidBytes[i]);
        }

        byte[] newSuidBytes = longToByte(newSuid);

        System.out.println();
        System.out.println("newSuid :");
        for (int i = 0; i < newSuidBytes.length; i++) {
            System.out.print(newSuidBytes[i]);
        }

        System.out.println();


        for (int i = 0; i < bytes.length - 7; i++) {
            if (bytes[i] == oldSuidBytes[0]) {
                //compare 8-bit
                boolean match = true;
                for (int j = 1; j <= 7; j++) {
                    if (bytes[i + j] != oldSuidBytes[j]) {
                        match = false;
                        break;
                    }
                }
                if (match) {
                    //convert
                    for (int j = 0; j <= 7; j++) {
                        bytes[i + j] = newSuidBytes[j];
                    }
                    i = i + 8;
                }
            }
        }

        return bytes;
    }
}


5.参考资源:
如何提前预防上述问题的产生 http://www.javaworld.com/javaworld/jw-02-2006/jw-0227-control-p2.html
查看JDK API中的关于Serializable Interface 英文文档以及Specification。

6.其他一些技巧性的东西:
--------------------------------------------------------------
如何根据指定的class 得到这个class的serialVersionUID的值:
--------------------------------------------------------------
C:\j2sdk1.4.2_01\bin>serialver -classpath c:\1.jar hello.HelloWorld
hello.HelloWorld:    static final long serialVersionUID = -5863503448069391657L;

--------------------------------------------------------------
序列化的步骤:
--------------------------------------------------------------
The sequence of items in the stream is as follows:
1.    The class name written using UTF encoding.
2.    The class modifiers written as a 32-bit integer.
3.    The name of each interface sorted by name written using UTF encoding.
4.    For each field of the class sorted by field name (except private static and private transient fields):
    a.    The name of the field in UTF encoding.
    b.    The modifiers of the field written as a 32-bit integer.
    c.    The descriptor of the field in UTF encoding
5.    If a class initializer exists, write out the following:
    a.    The name of the method, <clinit>, in UTF encoding.
    b.    The modifier of the method, java.lang.reflect.Modifier.STATIC, written as a 32-bit integer.
    c.    The descriptor of the method, ()V, in UTF encoding.
6.    For each non-private constructor sorted by method name and signature:
    a.    The name of the method, <init>, in UTF encoding.
    b.    The modifiers of the method written as a 32-bit integer.
    c.    The descriptor of the method in UTF encoding.
7.    For each non-private method sorted by method name and signature:
    a.    The name of the method in UTF encoding.
    b.    The modifiers of the method written as a 32-bit integer.
    c.    The descriptor of the method in UTF encoding.
8.    The SHA-1 algorithm is executed on the stream of bytes
    produced by DataOutputStream and produces five 32-bit values sha[0..4].
9.    The hash value is assembled from the first and second 32-bit values of the SHA-1 message digest.
    If the result of the message digest, the five 32-bit words H0 H1 H2 H3 H4, is in an array of five
    int values named sha, the hash value would be computed as follows:
    long hash = ((sha[0] >>> 24) & 0xFF) | ((sha[0] >>> 16) & 0xFF) << 8 | ((sha[0] >>> 8) & 0xFF) << 16 | ((sha[0] >>> 0) & 0xFF) << 24 | ((sha[1] >>> 24) & 0xFF) << 32 | ((sha[1] >>> 16) & 0xFF) << 40 | ((sha[1] >>> 8) & 0xFF) << 48 | ((sha[1] >>> 0) & 0xFF) << 56;
    
Class PutField provides the API for setting values of the serializable fields for a class
when the class does not use default serialization. Each method puts the specified named value into the stream.
An IllegalArgumentException is thrown if name does not match the name of a serializable field for the class
whose fields are being written, or if the type of the named field does not match the second parameter type
of the specific put method invoked.

7.其他一些可供参考的方案:

1) XMLDecode/XMLEncode :从JDK1.4开始,提供了java.beans.XMLEncoder和java.beans.XMLDecoder两个类,
专门针对JavaBean进行序列化和反序列化。如果不是javaBean风格的class,通过写一个java.beans.PersistenceDelegate
class 来重新定制一个序列化的方法,可以解决不是javaBean风格的object。
以下是一个javaBean被序列化后的内容:
<?xml version=”1.0” encoding=”UTF-8”?>
<java version=”1.5.0-beta3” class=”java.beans.XMLDecoder”>
    <object class=”book.Configuration”>
        <void property=”recentFiles”>
            <array class=”java.lang.String” length=”3”>
                <void index=”0”>
                    <string>c:\mark\file1.proj</string>
                </void>
                <void index=”1”>
                    <string>c:\mark\testproj.proj</string>
                </void>
                <void index=”2”>
                    <string>c:\mark\final.proj</string>
                </void>
            </array>
        </void>
        <void property=”userHomeDirectory”>
            <string>C:\Documents and Settings\Mark\My Documents</string>
        </void>
        <void property=”showTabs”>
            <boolean>true</boolean>
        </void>
        <void property=”foregroundColor”>
            <object class=”java.awt.Color”>
                <int>255</int>
                <int>255</int>
                <int>51</int>
                <int>255</int>
            </object>
        </void>
        <void property=”backgroundColor”>
            <object class=”java.awt.Color”>
                <int>51</int>
                <int>51</int>
                <int>255</int>
                <int>255</int>
            </object>
        </void>
    </object>
</java>

2)JAXB
由于XMLDecoder/XMLEncoder必须使用java平台,与java语言绑定在一起了,不能跨平台。于是出了JAXB,可以跨平台,
被各种语言支持。以下是JAXB的介绍:
JAXB is fundamentally different from either the Java Serialization API or the XMLEncoder/Decoder API.
It takes a completely different approach. Instead of first specifying a data structure using Java classes, one
first specifies the serialization format itself. The two are drastically different design approaches. In the
Java Serialization and XMLEncoder/Decoder API, you design Java classes and do not worry about the
serialization file format—that is taken care of by the APIs. However, it has the unfortunate disadvantage
of limiting the use of the serialized objects to only Java-based applications. JAXB generates your Java data
classes for you (at the expense of a very loose integration of your data with the JDK libraries) from the
specification of a file format in a W3C standard XML Schema Definition. JAXB adds more complexity to
an application and requires more development effort.

Its advantages are as follows:
  ---Reads and writes standard file formats that applications written in any language can read, and
     in many languages generates classes to use the data similarly to how JAXB generates classes
     based on the file
  ---Resulting serialized documents are human-readable and as friendly to edit as they are defined
  ---Fast way to read XML data based on an XML schema—uses far less memory to represent an
     XML document in memory than a DOM tree

Its disadvantages are namely the following:
  ---Requires more development effort—sometimes it is necessary to manage two data models: one
     your application can more efficiently use and the JAXB-generated data model
  ---Working with JAXB objects can be unwieldy since they are generated—things like naming and
     object creation are more tedious to develop with than custom Java classes    
    
JAXB should be used when you want a human-readable file format that can be edited by users. It should
be used when you are developing a file format you want non-Java-based applications to be able to read.
It can be used in conjunction with other XML technologies, and to read third-party XML documents
based on third-party XML schemas. It is a valuable tool that requires more development effort and more
design, but its benefits far outweigh its costs—if you need a universal file format or just simply humanreadable
XML.


文章来自: 本站原创
引用通告: 查看所有引用 | 我要引用此文章
Tags: Serialization UID 序列化
相关日志:
评论: 0 | 引用: 0 | 查看次数: 597
发表评论
昵 称:
密 码: 游客发言不需要密码.
内 容:
验证码: 验证码
选 项:
虽然发表评论不用注册,但是为了保护您的发言权,建议您注册帐号.
字数限制 1000 字 | UBB代码 开启 | [img]标签 关闭