Common serialization methods for redis

Posted by linkin on Fri, 07 Feb 2020 18:16:29 +0100

Generalization

The main serialization methods of redis are string serialization, json serialization, xml serialization, jdk serialization. The implementation classes of org.springframework.data.redis.serializer.RedisSerializer can be consulted. For json serialization, jackson serialization is the official implementation, and fastjson has corresponding implementation.

Source Code Analysis

A little bit more analysis of the source code for spring-data-redis,

The serialization of key and hash key is generally done as follows:


...
/**
 * Simple String to byte[] (and back) serializer. Converts Strings into bytes and vice-versa using the specified charset
 * (by default UTF-8).
 * <p>
 * Useful when the interaction with the Redis happens mainly through Strings.
 * <p>
 * Does not perform any null conversion since empty strings are valid keys/values.
 * 
 * @author Costin Leau
 * @author Christoph Strobl
 */
public class StringRedisSerializer implements RedisSerializer<String> {

	private final Charset charset;

	public StringRedisSerializer() {
		this(Charset.forName("UTF8"));
	}

	public StringRedisSerializer(Charset charset) {
		Assert.notNull(charset, "Charset must not be null!");
		this.charset = charset;
	}

	public String deserialize(byte[] bytes) {
		return (bytes == null ? null : new String(bytes, charset));
	}

	public byte[] serialize(String string) {
		return (string == null ? null : string.getBytes(charset));
	}
}

//You can see that string serialization is common

The serialization of value and hash value is typically done in the following ways:

part I
...
/**
 * Java Serialization Redis serializer. Delegates to the default (Java based) {@link DefaultSerializer serializer} and
 * {@link DefaultDeserializer}. This {@link RedisSerializer serializer} can be constructed with either custom
 * {@link ClassLoader} or own {@link Converter converters}.
 */
public class JdkSerializationRedisSerializer implements RedisSerializer<Object> {

// Three constructors
// Default constructor
public JdkSerializationRedisSerializer(){
this(new SerializingConverter(), new DeserializingConverter());}

// Specify the constructor for ClassLoader (which may be required for hot deployment to avoid confusion between different class loaders)
public JdkSerializationRedisSerializer(ClassLoader classLoader){...}

// Specify the constructor for org.springframework.core.convert.converter.Converter (heavily dependent on the spring framework or self-implemented)
public JdkSerializationRedisSerializer(Converter<Object, byte[]> serializer, Converter<byte[], Object> deserializer){...};
...

part II
...
org.springframework.core.serializer.support.SerializingConverter#convert
   /**
     * Serializes the source object and returns the byte array result.
	 */
	@Override
	public byte[] convert(Object source) {
		ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024);
		try  {
			this.serializer.serialize(source, byteStream);// According to the constructor, the default serializer is used here
			return byteStream.toByteArray();
		}
		catch (Throwable ex) {
			throw new SerializationFailedException("Failed to serialize object using " +
					this.serializer.getClass().getSimpleName(), ex);
		}
	}
...

part III
...
org.springframework.core.serializer.DefaultSerializer#serialize
    /**
	 * Writes the source object to an output stream using Java serialization.
	 * The source object must implement {@link Serializable}.
	 * @see ObjectOutputStream#writeObject(Object)
	 */
	@Override
	public void serialize(Object object, OutputStream outputStream) throws IOException {
		if (!(object instanceof Serializable)) {
			throw new IllegalArgumentException(getClass().getSimpleName() + " requires a Serializable payload " +
					"but received an object of type [" + object.getClass().getName() + "]");
		}
		ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
		objectOutputStream.writeObject(object);
		objectOutputStream.flush();
	}
...

//The general serialization method is known above.
//Next, look at how to deserialize:

part IV
...
org.springframework.core.serializer.support.DeserializingConverter#convert
    public Object convert(byte[] source) {
		ByteArrayInputStream byteStream = new ByteArrayInputStream(source);
		try {
			return this.deserializer.deserialize(byteStream);// From the constructor, the default deserializer is used here
		}
		catch (Throwable ex) {
			throw new SerializationFailedException("Failed to deserialize payload. " +
					"Is the byte array a result of corresponding serialization for " +
					this.deserializer.getClass().getSimpleName() + "?", ex);
		}
	}
...

part V
...

org.springframework.core.serializer.DefaultDeserializer#deserialize
	/**
	 * Read from the supplied {@code InputStream} and deserialize the contents
	 * into an object.
	 * @see ObjectInputStream#readObject()
	 */
	@Override
	@SuppressWarnings("resource")
	public Object deserialize(InputStream inputStream) throws IOException {
		ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, this.classLoader);
		try {
			return objectInputStream.readObject();
		}
		catch (ClassNotFoundException ex) {
			throw new NestedIOException("Failed to deserialize object type", ex);
		}
	}
...

part VI
...
/**
 * Special ObjectInputStream subclass that resolves class names
 * against a specific ClassLoader. Serves as base class for
 * {@link org.springframework.remoting.rmi.CodebaseAwareObjectInputStream}.
 *
 * @author Juergen Hoeller
 * @since 2.5.5
 */
public class ConfigurableObjectInputStream extends ObjectInputStream {
...
	@Override
	protected Class<?> resolveClass(ObjectStreamClass classDesc) throws IOException, ClassNotFoundException {
		try {
			if (this.classLoader != null) {
				// Use the specified ClassLoader to resolve local classes.
				return ClassUtils.forName(classDesc.getName(), this.classLoader);
			}
			else {
				// Use the default ClassLoader...
				return super.resolveClass(classDesc);
			}
		}
		catch (ClassNotFoundException ex) {
			return resolveFallbackIfPossible(classDesc.getName(), ex);
		}
	}

	@Override
	protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException {
		if (!this.acceptProxyClasses) {
			throw new NotSerializableException("Not allowed to accept serialized proxy classes");
		}
		if (this.classLoader != null) {
			// Use the specified ClassLoader to resolve local proxy classes.
			Class<?>[] resolvedInterfaces = new Class<?>[interfaces.length];
			for (int i = 0; i < interfaces.length; i++) {
				try {
					resolvedInterfaces[i] = ClassUtils.forName(interfaces[i], this.classLoader);
				}
				catch (ClassNotFoundException ex) {
					resolvedInterfaces[i] = resolveFallbackIfPossible(interfaces[i], ex);
				}
			}
			try {
				return ClassUtils.createCompositeInterface(resolvedInterfaces, this.classLoader);
			}
			catch (IllegalArgumentException ex) {
				throw new ClassNotFoundException(null, ex);
			}
		}
		else {
			// Use ObjectInputStream's default ClassLoader...
			try {
				return super.resolveProxyClass(interfaces);
			}
			catch (ClassNotFoundException ex) {
				Class<?>[] resolvedInterfaces = new Class<?>[interfaces.length];
				for (int i = 0; i < interfaces.length; i++) {
					resolvedInterfaces[i] = resolveFallbackIfPossible(interfaces[i], ex);
				}
				return ClassUtils.createCompositeInterface(resolvedInterfaces, getFallbackClassLoader());
			}
		}
	}
...
    
//As you can see above, deserialization has special handling for custom incoming Classloader cases

Custom Serialization

After analyzing the source code, we can customize a more general way of serialization if we don't rely on the spring framework:

String serialization:

package com.caiya.test.serialization.jdk;

import com.caiya.test.serialization.Serializer;
import com.caiya.test.serialization.exception.SerializationException;

import java.nio.charset.Charset;

/**
 * Simple String to byte[] (and back) serializer. Converts Strings into bytes and vice-versa using the specified charset
 * (by default UTF-8).
 * <p/>
 * Useful when the interaction with the Redis happens mainly through Strings.
 * <p/>
 * Does not perform any null conversion since empty strings are valid keys/values.
 *
 * @author wangnan
 * @since 1.0
 */
public class StringSerializer implements Serializer<String> {


    private final Charset charset;

    public StringSerializer() {
        this(Charset.forName("UTF-8"));
    }

    public StringSerializer(Charset charset) {
        if (charset == null)
            throw new IllegalArgumentException("Charset must not be null!");

        this.charset = charset;
    }

    @Override
    public byte[] serialize(String string) throws SerializationException {
        return (string == null ? null : string.getBytes(charset));
    }

    @Override
    public String deserialize(byte[] bytes) throws SerializationException {
        return (bytes == null ? null : new String(bytes, charset));
    }
}

jdk serialization:

package com.caiya.test.serialization.jdk;

import com.caiya.test.serialization.Serializer;
import com.caiya.test.serialization.exception.NestedIOException;
import com.caiya.test.serialization.exception.SerializationException;
import com.caiya.test.serialization.exception.SerializationFailedException;

import java.io.*;

/**
 * Java Serialization serializer.
 *
 * @author wangnan
 * @since 1.0
 */
public class JdkSerializationSerializer implements Serializer<Object> {

    private final ClassLoader classLoader;

    public JdkSerializationSerializer() {
        this.classLoader = null;
    }

    public JdkSerializationSerializer(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }


    @Override
    public byte[] serialize(Object object) throws SerializationException {
        if (object == null) {
            return SerializationUtils.EMPTY_ARRAY;
        }

        try {
            if (!(object instanceof Serializable)) {
                throw new IllegalArgumentException(getClass().getSimpleName() + " requires a Serializable payload " +
                        "but received an object of type [" + object.getClass().getName() + "]");
            }

            ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteStream);
            objectOutputStream.writeObject(object);
            objectOutputStream.flush();

            return byteStream.toByteArray();
        } catch (Exception ex) {
            throw new SerializationException("Cannot serialize", ex);
        }
    }

    @Override
    public Object deserialize(byte[] bytes) throws SerializationException {
        if (SerializationUtils.isEmpty(bytes)) {
            return null;
        }

        try {
            ByteArrayInputStream byteStream = new ByteArrayInputStream(bytes);
            try {
                ObjectInputStream objectInputStream = new ObjectInputStream(byteStream);
// If you need to customize ClassLoader, you need an additional implementation.ObjectInputStream objectInputStream = new Configurable ObjectInputStream (byteStream, this.classLoader);
                try {
                    return objectInputStream.readObject();
                } catch (ClassNotFoundException ex) {
                    throw new NestedIOException("Failed to deserialize object type", ex);
                }
            } catch (Throwable ex) {
                throw new SerializationFailedException("Failed to deserialize payload. " +
                        "Is the byte array a result of corresponding serialization for " +
                        this.getClass().getSimpleName() + "?", ex);
            }
        } catch (Exception ex) {
            throw new SerializationException("Cannot deserialize", ex);
        }
    }
}

Topics: Redis Java JDK Spring