Как проверить в Java, что класс правильно реализует Serializable (не только экземпляр Serializable)

Я реализую класс, чтобы быть Serializable (так что это объект значения для использования с RMI). Но мне нужно это проверить. Есть ли способ сделать это легко?

пояснение: я реализую класс, поэтому тривиально добавить Serializable в определение класса. Мне нужно вручную сериализовать / десериализовать его, чтобы увидеть, работает ли он.

Я нашел этот вопрос C#, есть ли подобный ответ для Java?

8 ответов

Решение

Самый простой способ - проверить, что объект является экземпляром 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);

и так далее.

Служебные методы, основанные на ответе Скаффмана:

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

Это работает только для полностью заполненных объектов, если вам требуется, чтобы любые объекты, созданные в вашем объекте верхнего уровня, были также сериализуемыми, тогда они не могут иметь значение null, чтобы этот тест был действительным, так как сериализация / десериализация пропускает нулевые объекты

Короткий ответ: вы можете придумать несколько объектов-кандидатов и попытаться их сериализовать, используя выбранный вами механизм. Тест здесь заключается в том, что во время маршаллинга / демаршаллинга ошибок не обнаружено, и что полученный "регидратированный" объект равен оригиналу.

В качестве альтернативы, если у вас нет объектов-кандидатов, вы можете реализовать тест на основе отражений, который анализирует (не статические, не переходные) поля вашего класса, чтобы убедиться, что они тоже являются сериализуемыми. Судя по опыту, это удивительно сложно на удивление быстро, но это можно сделать в разумных пределах.

Недостатком этого последнего подхода является то, что если поле, например, List<String>тогда вы можете либо потерпеть неудачу в классе из-за отсутствия строго сериализуемого поля, либо просто предположить, что будет использоваться сериализуемая реализация List. Ни один не идеален. (Имейте в виду, что последняя проблема существует и для примеров; если в каждом примере, используемом в тесте, используются сериализуемые списки, нет ничего, что могло бы предотвратить использование не сериализуемой версии каким-либо другим кодом на практике).

Этот код должен сделать это...

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

Вы можете сделать следующий тест:

  • Сериализуйте объект в файл и убедитесь, что исключение не выдается.
  • Кроме того, десериализовать объект обратно и сравнить с исходным объектом.

Вот пример для сериализации и десериализации объекта в файл:

http://www.rgagnon.com/javadetails/java-0075.html

http://www.javapractices.com/topic/TopicAction.do?Id=57

Создайте класс POJO:

      package com.example.simpleproject.dto;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
@Getter
@Setter
@EqualsAndHashCode
@ToString
public class ExtraInfo implements Serializable {

    private String phoneNumber;
    private String other;
}

Создайте тестовый класс и напишите следующий метод модульного теста:

      package com.example.simpleproject.dto;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.util.SerializationUtils;
import java.io.Serializable;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ExtraInfoTest {

    private ExtraInfo objectUnderTest;

    @BeforeEach
    void setUp() {
        objectUnderTest = new ExtraInfo();
    }

    @Test
    void testSerializable() {
        Serializable serialized = SerializationUtils.serialize(objectUnderTest);
        Object deserialized = SerializationUtils.deserialize((byte[]) serialized);
        assertEquals(objectUnderTest, deserialized);
    }
}

Я попытался написать модульный тест (в Groovy с использованием Spock), который может проверить, что данный интерфейс для использования с 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
    }

}
Другие вопросы по тегам