如何在Java中测试类是否正确实现了Serializable(不仅是Serializable的实例)


77

我正在实现一个可序列化的类(因此,它是一个使用RMI的值对象)。但是我需要测试一下。有没有一种方法可以轻松做到这一点?

澄清:我正在实现该类,因此将Serializable保留在类定义中很简单。我需要手动对其进行序列化/反序列化以查看其是否有效。

我发现了这个C#问题,对于Java是否有类似的答案?


您是否在尝试测试对象是否已正确序列化和反序列化?
Vivin Paliath

Answers:


128

简单的方法是检查对象是否为java.io.Serializable或的实例java.io.Externalizable,但这并不能真正证明该对象确实可序列化。

唯一可以确定的方法是实际尝试。最简单的测试是这样的:

new ObjectOutputStream(new ByteArrayOutputStream()).writeObject(myObject);

并检查它是否不会引发异常。

Apache Commons Lang提供了一个更简短的版本:

SerializationUtils.serialize(myObject);

再次检查异常。

您仍然可以更加严格,并检查它反序列化为与原始序列相同的东西:

Serializable original = ...
Serializable copy = SerializationUtils.clone(original);
assertEquals(original, copy);

等等。


万一有人想了解或不希望包括Apache的共享库,春天还与方法SerializationUtilsserializedeserialize。参见SerializationUtils
Peter Kirby 2014年

你的回答帮我,但deserialize()clone()回报Object,而不是Serializable
NeplatnyUdaj 2015年

在旧公琅V2这是事实,但不是因为V3 - commons.apache.org/proper/commons-lang/javadocs/api-3.4/org/...
skaffman

4
即使这也不是万无一失的。如果对象具有不可序列化的字段,但是它们是null,则它将可序列化;初始化它们,事实并非如此。
wu-lee

28

基于skaffman答案的实用方法:

private static <T extends Serializable> byte[] pickle(T obj) 
       throws IOException 
{
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(obj);
    oos.close();
    return baos.toByteArray();
}

private static <T extends Serializable> T unpickle(byte[] b, Class<T> cl)
       throws IOException, ClassNotFoundException 
{
    ByteArrayInputStream bais = new ByteArrayInputStream(b);
    ObjectInputStream ois = new ObjectInputStream(bais);
    Object o = ois.readObject();
    return cl.cast(o);
}

1
++感谢您和skaffman!我一直在寻找类似的东西添加到测试工具中,以阻止开发人员编写不可序列化的代码,并阻止我们的应用程序到达集群服务器后终止它们。救生员!谢谢你们!
克里斯·奥尔德里奇

有关“腌制”一词的更多信息,请访问:docs.python.org/2/library/pickle.html
时间范围为

简单。优雅。谢谢。。。我想复制和粘贴是可以的。
斯科特

3

简短的答案是,您可以提出一些候选对象,并实际上尝试使用您选择的机制来序列化它们。此处的测试是在编组/解组期间不会遇到任何错误,并且生成的“重新水化”对象与原始对象相同。

另外,如果您没有任何候选对象,则可以实施基于反射的测试,该测试将对类的(非静态,非瞬态)字段进行内部检查,以确保它们也可序列化。从经验上讲,这令人惊讶地很快变得异常复杂,但是可以在合理范围内完成。

后一种方法的不利之处在于,如果某个字段例如为List<String>,则您可能由于没有严格可序列化的字段而使该类失败,或者只是假设将使用List的可序列化实现。都不是完美的。(请记住,后面的问题也存在于示例中;如果测试中使用的每个示例都使用可序列化的列表,则没有任何办法可以防止实践中的其他一些代码使用不可序列化的版本)。


3

这仅适用于完全填充的对象,如果您要求在顶级对象中组成的任何对象也都可以序列化,则它们不能为null才能使此测试有效,因为序列化/反序列化会跳过空对象


3

该代码应该做到这一点...

import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;

public class Main
{
    public static void main(String[] args)
    {
        System.out.println(isSerializable("Hello"));
        System.out.println(isSerializable(new Main()));
    }

    public static boolean isSerializable(final Object o)
    {
        final boolean retVal;

        if(implementsInterface(o))
        {
            retVal = attemptToSerialize(o);
        }
        else
        {
            retVal = false;
        }

        return (retVal);
    }

    private static boolean implementsInterface(final Object o)
    {
        final boolean retVal;

        retVal = ((o instanceof Serializable) || (o instanceof Externalizable));

        return (retVal);
    }

    private static boolean attemptToSerialize(final Object o)
    {
        final OutputStream sink;
        ObjectOutputStream stream;

        stream = null;

        try
        {
            sink   = new ByteArrayOutputStream();
            stream = new ObjectOutputStream(sink);
            stream.writeObject(o);
            // could also re-serilalize at this point too
        }
        catch(final IOException ex)
        {
            return (false);
        }
        finally
        {
            if(stream != null)
            {
                try
                {
                    stream.close();
                }
                catch(final IOException ex)
                {
                    // should not be able to happen
                }
            }
        }

        return (true);
    }
}

instanceof Externalizable暗示instanceof Serializable。您无需同时测试两者。
罗恩侯爵


0

我试图编写一个单元测试(使用Spock在Groovy中),它可以检查用于RMI的给定接口实际上是完全可序列化的-所有参数,异常以及方法中定义的类型的可能实现。

到目前为止,它似乎对我仍然有效,但是,这样做有点奇怪,并且可能无法解决某些情况,因此,使用风险自负!

您将需要Notification用自己的示例接口等替换。该示例包括一个不可序列化的字段作为说明。

package example

import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import spock.lang.Specification

import java.lang.reflect.*
import java.rmi.Remote
import java.rmi.RemoteException

/** This checks that the a remoting API NotifierServer is safe
 *
 * It attempts to flush out any parameter classes which are
 * not Serializable. This isn't checked at compile time!
 *
 */
@CompileStatic
class RemotableInterfaceTest extends Specification {
    static class NotificationException extends RuntimeException {
        Object unserializable
    }

    static interface Notification {
        String getMessage()

        Date getDate()
    }

    static interface Notifier extends Remote {
        void accept(Notification notification) throws RemoteException, NotificationException
    }


    static interface NotifierServer extends Remote {
        void subscribe(Notification notifier) throws RemoteException
        void notify(Notification message) throws RemoteException
    }

    // From https://www.javaworld.com/article/2077477/learn-java/java-tip-113--identify-subclasses-at-runtime.html
    /**
     * Scans all classes accessible from the context class loader which belong to the given package and subpackages.
     *
     * @param packageName The base package
     * @return The classes
     * @throws ClassNotFoundException
     * @throws IOException
     */
    static Class[] getClasses(String packageName)
            throws ClassNotFoundException, IOException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader()
        assert classLoader != null
        String path = packageName.replace('.', '/')
        Enumeration resources = classLoader.getResources(path)
        List<File> dirs = new ArrayList()
        while (resources.hasMoreElements()) {
            URL resource = resources.nextElement()
            dirs.add(new File(resource.getFile()))
        }
        ArrayList classes = new ArrayList()
        for (File directory : dirs) {
            classes.addAll(findClasses(directory, packageName))
        }
        return classes.toArray(new Class[classes.size()])
    }

    /**
     * Recursive method used to find all classes in a given directory and subdirs.
     *
     * @param directory   The base directory
     * @param packageName The package name for classes found inside the base directory
     * @return The classes
     * @throws ClassNotFoundException
     */
    static List<Class> findClasses(File directory, String packageName) throws ClassNotFoundException {
        List<Class> classes = new ArrayList()
        if (!directory.exists()) {
            return classes
        }
        File[] files = directory.listFiles()
        for (File file : files) {
            if (file.isDirectory()) {
                //assert !file.getName().contains(".");
                classes.addAll(findClasses(file, packageName + "." + file.getName()))
            } else if (file.getName().endsWith(".class")) {
                classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)))
            }
        }
        return classes
    }

    /** Finds all known subclasses of a class */
    @CompileDynamic
    static List<Class> getSubclasses(Class type) {
        allClasses
            .findAll { Class it ->
                !Modifier.isAbstract(it.modifiers) &&
                it != type &&
                type.isAssignableFrom(it)
            }
    }

    /** Checks if a type is nominally serializable or remotable.
     *
     * Notes:
     * <ul>
     * <li> primitives are implicitly serializable
     * <li> interfaces are serializable or remotable by themselves, but we
     * assume that since #getSerializedTypes checks derived types of interfaces,
     * we can safely assume that all implementations will be checked
     *</ul>
     *
     * @param it
     * @return
     */
    static boolean isSerializableOrRemotable(Class<?> it) {
        return it.primitive || it.interface || Serializable.isAssignableFrom(it) || Remote.isAssignableFrom(it)
    }

    /** Recursively finds all (new) types associated with a given type 
     * which need to be serialized because they are fields, parameterized
     * types, implementations, etc. */
    static void getSerializedTypes(final Set<Class<?>> types, Type... it) {
        for(Type type in it) {
            println "type: $type.typeName"

            if (type instanceof GenericArrayType) {
                type = ((GenericArrayType)type).genericComponentType
            }

            if (type instanceof ParameterizedType) {
                ParameterizedType ptype = (ParameterizedType)type
                getSerializedTypes(types, ptype.actualTypeArguments)
                break
            }


            if (type instanceof Class) {
                Class ctype = (Class)type

                if (ctype == Object)
                    break

                if (types.contains(type))
                    break

                types << ctype
                for (Field field : ctype.declaredFields) {
                    println "${ctype.simpleName}.${field.name}: ${field.type.simpleName}"
                    if (Modifier.isVolatile(field.modifiers) ||
                        Modifier.isTransient(field.modifiers) ||
                        Modifier.isStatic(field.modifiers))
                        continue

                    Class<?> fieldType = field.type
                    if (fieldType.array)
                        fieldType = fieldType.componentType

                    if (types.contains(fieldType))
                        continue

                    types << fieldType
                    if (!fieldType.primitive)
                        getSerializedTypes(types, fieldType)
                }

                if (ctype.genericSuperclass) {
                    getSerializedTypes(types, ctype.genericSuperclass)
                }

                getSubclasses(ctype).each { Class c -> getSerializedTypes(types, c) }

                break
            }
        }
    }

    /** Recursively checks a type's methods for related classes which
     * need to be serializable if the type is remoted */
    static Set<Class<?>> getMethodTypes(Class<?> it) {
        Set<Class<?>> types = []
        for(Method method: it.methods) {
            println "method: ${it.simpleName}.$method.name"
            getSerializedTypes(types, method.genericParameterTypes)
            getSerializedTypes(types, method.genericReturnType)
            getSerializedTypes(types, method.genericExceptionTypes)
        }
        return types
    }

    /** All the known defined classes */
    static List<Class> allClasses = Package.packages.collectMany { Package p -> getClasses(p.name) as Collection<Class> }


    @CompileDynamic
    def "NotifierServer interface should only expose serializable or remotable types"() {
        given:
        Set<Class> types = getMethodTypes(NotifierServer)

        Set<Class> nonSerializableTypes = types.findAll { !isSerializableOrRemotable(it) }

        expect:
        nonSerializableTypes.empty
    }

}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.