/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*/
package com.liferay.portal.kernel.io;
import com.liferay.portal.kernel.memory.SoftReferenceThreadLocal;
import com.liferay.portal.kernel.util.ClassLoaderPool;
import com.liferay.portal.kernel.util.GetterUtil;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* Serializes data in a ClassLoader-aware manner.
*
* <p>
* The Serializer can perform better than {@link ObjectOutputStream} and {@link
* java.io.DataOutputStream}, with respect to encoding primary types, because it
* uses a more compact format (containing no BlockHeader) and simpler call stack
* involving {@link BigEndianCodec}, as compared to using an OutputStream
* wrapper on top of {@link java.io.Bits}.
* </p>
*
* <p>
* For Strings, the UTF encoding for ObjectOutputStream and DataOutputStream has
* a 2^16=64K length limitation, which is often too restrictive. Serializer has
* a 2^32=4G String length limitation, which is generally more than enough. For
* pure ASCII character Strings, the encoding performance is almost the same, if
* not better, than ObjectOutputStream and DataOutputStream. For Strings
* containing non-ASCII characters, the Serializer encodes each char to two
* bytes rather than performing UTF encoding. There is a trade-off between
* CPU/memory performance and compression rate.
* </p>
*
* <p>
* UTF encoding uses more CPU cycles to detect the unicode range for each char
* and the resulting output is variable length, which increases the memory
* burden when preparing the decoding buffer. Whereas, encoding each char to two
* bytes allows for better CPU/memory performance. Although inefficient with
* compression rates in comparison to UTF encoding, the char to two byte
* approach significantly simplifies the encoder's logic and the output length
* is predictably based on the length of the String, so the decoder can manage
* its decoding buffer efficiently. On average, a system uses more ASCII String
* scheming than non-ASCII String scheming. In most cases, when all system
* internal Strings are ASCII Strings and only Strings holding user input
* information can have non-ASCII characters, this Serializer performs best. In
* other cases, developers should consider using {@link ObjectOutputStream} or
* {@link java.io.DataOutputStream}.
* </p>
*
* <p>
* For ordinary Objects, all primary type wrappers are encoded to their raw
* values with one byte type headers. This is much more efficient than
* ObjectOutputStream's serialization format for primary type wrappers. Strings
* are output in the same way as {@link #writeString(java.lang.String)}, but
* also with one byte type headers. Objects are serialized by a new
* ObjectOutputStream, so no reference handler can be used across Object
* serialization. This is done intentionally to isolate each object. The
* Serializer is highly optimized for serializing primary types, but is not as
* good as ObjectOutputStream for serializing complex objects.
* </p>
*
* <p>
* On object serialization, the Serializer uses the {@link ClassLoaderPool} to
* look up the servlet context name corresponding to the object's ClassLoader.
* The servlet context name is written to the serialization stream. On object
* deserialization, the {@link Deserializer} uses the ClassLoaderPool to look up
* the ClassLoader corresponding to the servlet context name read from the
* deserialization stream. ObjectOutputStream and ObjectInputStream lack these
* features, making Serializer and Deserializer better choices for
* ClassLoader-aware Object serialization/deserialization, especially when
* plugins are involved.
* </p>
*
* @author Shuyang Zhou
* @see Deserializer
*/
public class Serializer {
public Serializer() {
BufferQueue bufferQueue = bufferQueueThreadLocal.get();
buffer = bufferQueue.dequeue();
}
public ByteBuffer toByteBuffer() {
ByteBuffer byteBuffer = ByteBuffer.wrap(Arrays.copyOf(buffer, index));
if (buffer.length <= THREADLOCAL_BUFFER_SIZE_LIMIT) {
BufferQueue bufferQueue = bufferQueueThreadLocal.get();
bufferQueue.enqueue(buffer);
}
buffer = null;
return byteBuffer;
}
public void writeBoolean(boolean b) {
BigEndianCodec.putBoolean(getBuffer(1), index++, b);
}
public void writeByte(byte b) {
getBuffer(1)[index++] = b;
}
public void writeChar(char c) {
BigEndianCodec.putChar(getBuffer(2), index, c);
index += 2;
}
public void writeDouble(double d) {
BigEndianCodec.putDouble(getBuffer(8), index, d);
index += 8;
}
public void writeFloat(float f) {
BigEndianCodec.putFloat(getBuffer(4), index, f);
index += 4;
}
public void writeInt(int i) {
BigEndianCodec.putInt(getBuffer(4), index, i);
index += 4;
}
public void writeLong(long l) {
BigEndianCodec.putLong(getBuffer(8), index, l);
index += 8;
}
public void writeObject(Serializable serializable) {
// The if block is ordered by frequency for better performance
if (serializable == null) {
writeByte(SerializationConstants.TC_NULL);
return;
}
else if (serializable instanceof Long) {
writeByte(SerializationConstants.TC_LONG);
writeLong((Long)serializable);
return;
}
else if (serializable instanceof String) {
writeByte(SerializationConstants.TC_STRING);
writeString((String)serializable);
return;
}
else if (serializable instanceof Integer) {
writeByte(SerializationConstants.TC_INTEGER);
writeInt((Integer)serializable);
return;
}
else if (serializable instanceof Boolean) {
writeByte(SerializationConstants.TC_BOOLEAN);
writeBoolean((Boolean)serializable);
return;
}
else if (serializable instanceof Class) {
Class<?> clazz = (Class<?>)serializable;
ClassLoader classLoader = clazz.getClassLoader();
String contextName = ClassLoaderPool.getContextName(classLoader);
writeByte(SerializationConstants.TC_CLASS);
writeString(contextName);
writeString(clazz.getName());
return;
}
else if (serializable instanceof Short) {
writeByte(SerializationConstants.TC_SHORT);
writeShort((Short)serializable);
return;
}
else if (serializable instanceof Character) {
writeByte(SerializationConstants.TC_CHARACTER);
writeChar((Character)serializable);
return;
}
else if (serializable instanceof Byte) {
writeByte(SerializationConstants.TC_BYTE);
writeByte((Byte)serializable);
return;
}
else if (serializable instanceof Double) {
writeByte(SerializationConstants.TC_DOUBLE);
writeDouble((Double)serializable);
return;
}
else if (serializable instanceof Float) {
writeByte(SerializationConstants.TC_FLOAT);
writeFloat((Float)serializable);
return;
}
else {
writeByte(SerializationConstants.TC_OBJECT);
}
try {
ObjectOutputStream objectOutputStream =
new AnnotatedObjectOutputStream(new BufferOutputStream());
objectOutputStream.writeObject(serializable);
objectOutputStream.flush();
}
catch (IOException ioe) {
throw new RuntimeException(
"Unable to write ordinary serializable object " + serializable,
ioe);
}
}
public void writeShort(short s) {
BigEndianCodec.putShort(getBuffer(2), index, s);
index += 2;
}
public void writeString(String s) {
int length = s.length();
boolean asciiCode = true;
for (int i = 0; i < length; i++) {
char c = s.charAt(i);
if ((c == 0) || (c > 127)) {
asciiCode = false;
break;
}
}
if (asciiCode) {
byte[] buffer = getBuffer(length + 5);
BigEndianCodec.putBoolean(buffer, index++, asciiCode);
BigEndianCodec.putInt(buffer, index, length);
index += 4;
for (int i = 0; i < length; i++) {
char c = s.charAt(i);
buffer[index++] = (byte)c;
}
}
else {
byte[] buffer = getBuffer(length * 2 + 5);
BigEndianCodec.putBoolean(buffer, index++, asciiCode);
BigEndianCodec.putInt(buffer, index, length);
index += 4;
for (int i = 0; i < length; i++) {
char c = s.charAt(i);
BigEndianCodec.putChar(buffer, index, c);
index += 2;
}
}
}
public void writeTo(OutputStream outputStream) throws IOException {
outputStream.write(buffer, 0, index);
if (buffer.length <= THREADLOCAL_BUFFER_SIZE_LIMIT) {
BufferQueue bufferQueue = bufferQueueThreadLocal.get();
bufferQueue.enqueue(buffer);
}
buffer = null;
}
/**
* Returns the required buffer length. This method is final so JIT can
* perform an inline expansion.
*
* @param ensureExtraSpace the extra byte space required to meet the
* buffer's minimum length
* @return the buffer value
*/
protected final byte[] getBuffer(int ensureExtraSpace) {
int minSize = index + ensureExtraSpace;
if (minSize < 0) {
// Cannot create byte[] with length longer than Integer.MAX_VALUE
throw new OutOfMemoryError();
}
int oldSize = buffer.length;
if (minSize > oldSize) {
int newSize = oldSize << 1;
if (newSize < minSize) {
// Overflow and insufficient growth protection
newSize = minSize;
}
buffer = Arrays.copyOf(buffer, newSize);
}
return buffer;
}
protected static final int THREADLOCAL_BUFFER_COUNT_LIMIT;
protected static final int THREADLOCAL_BUFFER_COUNT_MIN = 8;
protected static final int THREADLOCAL_BUFFER_SIZE_LIMIT;
protected static final int THREADLOCAL_BUFFER_SIZE_MIN = 16 * 1024;
/**
* Softens the local thread's pooled buffer memory.
*
* <p>
* Technically, we should soften each pooled buffer individually to achieve
* the best garbage collection (GC) interaction. However, that increases
* complexity of pooled buffer access and also burdens the GC's {@link
* java.lang.ref.SoftReference} process, hurting performance.
* </p>
*
* <p>
* Here, the entire ThreadLocal BufferQueue is softened. For threads that do
* serializing often, its BufferQueue will most likely stay valid. For
* threads that do serializing only occasionally, its BufferQueue will most
* likely be released by GC.
* </p>
*/
protected static final ThreadLocal<BufferQueue> bufferQueueThreadLocal =
new SoftReferenceThreadLocal<BufferQueue>() {
@Override
protected BufferQueue initialValue() {
return new BufferQueue();
}
};
static {
int threadLocalBufferCountLimit = GetterUtil.getInteger(
System.getProperty(
Serializer.class.getName() +
".thread.local.buffer.count.limit"));
if (threadLocalBufferCountLimit < THREADLOCAL_BUFFER_COUNT_MIN) {
threadLocalBufferCountLimit = THREADLOCAL_BUFFER_COUNT_MIN;
}
THREADLOCAL_BUFFER_COUNT_LIMIT = threadLocalBufferCountLimit;
int threadLocalBufferSizeLimit = GetterUtil.getInteger(
System.getProperty(
Serializer.class.getName() +
".thread.local.buffer.size.limit"));
if (threadLocalBufferSizeLimit < THREADLOCAL_BUFFER_SIZE_MIN) {
threadLocalBufferSizeLimit = THREADLOCAL_BUFFER_SIZE_MIN;
}
THREADLOCAL_BUFFER_SIZE_LIMIT = threadLocalBufferSizeLimit;
}
protected byte[] buffer;
protected int index;
protected static class BufferNode {
public BufferNode(byte[] buffer) {
this.buffer = buffer;
}
protected byte[] buffer;
protected BufferNode next;
}
/**
* Represents a descending <code>byte[]</code> queue ordered by array length.
*
* <p>
* The queue is small enough to simply use a linear scan search for
* maintaining its order. The entire queue data is held by a
* {@link java.lang.ref.SoftReference}, so when necessary, GC can release the whole
* buffer cache.
* </p>
*/
protected static class BufferQueue {
public byte[] dequeue() {
if (headBufferNode == null) {
return new byte[THREADLOCAL_BUFFER_SIZE_MIN];
}
BufferNode bufferNode = headBufferNode;
headBufferNode = headBufferNode.next;
// Help GC
bufferNode.next = null;
return bufferNode.buffer;
}
public void enqueue(byte[] buffer) {
BufferNode bufferNode = new BufferNode(buffer);
if (headBufferNode == null) {
headBufferNode = bufferNode;
count = 1;
return;
}
BufferNode previousBufferNode = null;
BufferNode currentBufferNode = headBufferNode;
while ((currentBufferNode != null) &&
(currentBufferNode.buffer.length >
bufferNode.buffer.length)) {
previousBufferNode = currentBufferNode;
currentBufferNode = currentBufferNode.next;
}
if (previousBufferNode == null) {
bufferNode.next = headBufferNode;
headBufferNode = bufferNode;
}
else {
bufferNode.next = currentBufferNode;
previousBufferNode.next = bufferNode;
}
if (++count > THREADLOCAL_BUFFER_COUNT_LIMIT) {
if (previousBufferNode == null) {
previousBufferNode = headBufferNode;
}
currentBufferNode = previousBufferNode.next;
while (currentBufferNode.next != null) {
previousBufferNode = currentBufferNode;
currentBufferNode = currentBufferNode.next;
}
// Dereference
previousBufferNode.next = null;
// Help GC
currentBufferNode.buffer = null;
currentBufferNode.next = null;
}
}
protected int count;
protected BufferNode headBufferNode;
}
protected class BufferOutputStream extends OutputStream {
@Override
public void write(byte[] bytes) {
write(bytes, 0, bytes.length);
}
@Override
public void write(byte[] bytes, int offset, int length) {
byte[] buffer = getBuffer(length);
System.arraycopy(bytes, offset, buffer, index, length);
index += length;
}
@Override
public void write(int b) {
getBuffer(1)[index++] = (byte)b;
}
}
}