org.powermock.api.mockito.repackaged.AcrossJVMSerializationFeature Maven / Gradle / Ivy
Show all versions of powermock-api-mockito2 Show documentation
/*
* Copyright (c) 2007 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package org.powermock.api.mockito.repackaged;
import org.mockito.Incubating;
import org.mockito.exceptions.base.MockitoSerializationIssue;
import org.mockito.internal.creation.instance.DefaultInstantiatorProvider;
import org.mockito.internal.creation.settings.CreationSettings;
import org.mockito.internal.util.MockUtil;
import org.mockito.internal.util.reflection.FieldSetter;
import org.mockito.mock.MockCreationSettings;
import org.mockito.mock.MockName;
import org.mockito.mock.SerializableMode;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static org.powermock.utils.StringJoiner.join;
/**
* This is responsible for serializing a mock, it is enabled if the mock is implementing
* {@link Serializable}.
*
*
* The way it works is to enable serialization via the {@link #enableSerializationAcrossJVM(MockCreationSettings)},
* if the mock settings is set to be serializable it will add the {@link AcrossJVMSerializationFeature.AcrossJVMMockitoMockSerializable}
* interface.
* This interface defines a the {@link AcrossJVMSerializationFeature.AcrossJVMMockitoMockSerializable#writeReplace()}
* whose signature match the one that is looked by the standard Java serialization.
*
*
*
* Then in the {@link MethodInterceptorFilter} of mockito, if the {@code writeReplace} method is called,
* it will use the custom implementation of this class {@link #writeReplace(Object)}. This method has a specific
* knowledge on how to serialize a mockito mock that is based on CGLIB.
*
*
* Only one instance per mock! See {@link org.powermock.api.mockito.repackaged.MethodInterceptorFilter}
*
* TODO use a proper way to add the interface
* TODO offer a way to disable completely this behavior, or maybe enable this behavior only with a specific setting
* TODO check the class is mockable in the deserialization side
*
* @see org.powermock.api.mockito.repackaged.CglibMockMaker
* @see org.powermock.api.mockito.repackaged.MethodInterceptorFilter
* @author Brice Dutheil
* @since 1.10.0
*/
@Incubating
class AcrossJVMSerializationFeature implements Serializable {
private static final long serialVersionUID = 7411152578314420778L;
private static final String MOCKITO_PROXY_MARKER = "MockitoProxyMarker";
private final Lock mutex = new ReentrantLock();
private boolean instanceLocalCurrentlySerializingFlag = false;
public boolean isWriteReplace(Method method) {
return method.getReturnType() == Object.class
&& method.getParameterTypes().length == 0
&& method.getName().equals("writeReplace");
}
/**
* Custom implementation of the writeReplace
method for serialization.
*
* Here's how it's working and why :
*
* -
*
When first entering in this method, it's because some is serializing the mock, with some code like :
*
* objectOutputStream.writeObject(mock);
*
* So, {@link ObjectOutputStream} will track the writeReplace
method in the instance and
* execute it, which is wanted to replace the mock by another type that will encapsulate the actual mock.
* At this point, the code will return an
* {@link AcrossJVMSerializationFeature.AcrossJVMMockSerializationProxy}.
*
* -
*
Now, in the constructor
* {@link AcrossJVMSerializationFeature.AcrossJVMMockSerializationProxy#AcrossJVMMockSerializationProxy(Object)}
* the mock is being serialized in a custom way (using
* {@link AcrossJVMSerializationFeature.MockitoMockObjectOutputStream}) to a
* byte array. So basically it means the code is performing double nested serialization of the passed
* mockitoMock
.
*
* However the ObjectOutputStream
will still detect the custom
* writeReplace
and execute it.
* (For that matter disabling replacement via {@link ObjectOutputStream#enableReplaceObject(boolean)}
* doesn't disable the writeReplace
call, but just just toggle replacement in the
* written stream, writeReplace
is always called by
* ObjectOutputStream
.)
*
* In order to avoid this recursion, obviously leading to a {@link StackOverflowError}, this method is using
* a flag that marks the mock as already being replaced, and then shouldn't replace itself again.
* This flag is local to this class, which means the flag of this class unfortunately needs
* to be protected against concurrent access, hence the reentrant lock.
*
*
*
* @param mockitoMock The Mockito mock to be serialized.
* @return A wrapper ({@link AcrossJVMMockSerializationProxy}) to be serialized by the calling ObjectOutputStream.
* @throws ObjectStreamException
*/
public Object writeReplace(Object mockitoMock) throws ObjectStreamException {
try {
// reentrant lock for critical section. could it be improved ?
mutex.lock();
// mark started flag // per thread, not per instance
// temporary loosy hack to avoid stackoverflow
if (mockIsCurrentlyBeingReplaced()) {
return mockitoMock;
}
mockReplacementStarted();
return new AcrossJVMMockSerializationProxy(mockitoMock);
} catch (IOException ioe) {
MockName mockName = MockUtil.getMockName(mockitoMock);
String mockedType = MockUtil.getMockSettings(mockitoMock).getTypeToMock().getCanonicalName();
throw new MockitoSerializationIssue(join(
"The mock '" + mockName + "' of type '" + mockedType + "'",
"The Java Standard Serialization reported an '" + ioe.getClass().getSimpleName() + "' saying :",
" " + ioe.getMessage()
), ioe);
} finally {
// unmark
mockReplacementCompleted();
mutex.unlock();
}
}
private void mockReplacementCompleted() {
instanceLocalCurrentlySerializingFlag = false;
}
private void mockReplacementStarted() {
instanceLocalCurrentlySerializingFlag = true;
}
private boolean mockIsCurrentlyBeingReplaced() {
return instanceLocalCurrentlySerializingFlag;
}
/**
* Enable serialization serialization that will work across classloaders / and JVM.
*
* Only enable if settings says the mock should be serializable. In this case add the
* {@link AcrossJVMMockitoMockSerializable} to the extra interface list.
*
* @param settings Mock creation settings.
* @param Type param to not be bothered by the generics
*/
public void enableSerializationAcrossJVM(MockCreationSettings settings) {
if (settings.getSerializableMode() == SerializableMode.ACROSS_CLASSLOADERS) {
// havin faith that this set is modifiable
// TODO use a proper way to add the interface
settings.getExtraInterfaces().add(AcrossJVMMockitoMockSerializable.class);
}
}
/**
* Simple interface that hold a correct writeReplace
signature that can be seen by an
* ObjectOutputStream
.
*
* It will be applied before the creation of the mock when the mock setting says it should serializable.
*
* @see #enableSerializationAcrossJVM(org.mockito.mock.MockCreationSettings)
*/
public interface AcrossJVMMockitoMockSerializable {
public Object writeReplace() throws ObjectStreamException;
}
/**
* This is the serialization proxy that will encapsulate the real mock data as a byte array.
*
* When called in the constructor it will serialize the mock in a byte array using a
* custom {@link AcrossJVMSerializationFeature.MockitoMockObjectOutputStream} that
* will annotate the mock class in the stream.
* Other information are used in this class in order to facilitate deserialization.
*
*
* Deserialization of the mock will be performed by the {@link #readResolve()} method via
* the custom {@link MockitoMockObjectInputStream} that will be in charge of creating the mock class.
*/
public static class AcrossJVMMockSerializationProxy implements Serializable {
private static final long serialVersionUID = -7600267929109286514L;
private final byte[] serializedMock;
private final Class typeToMock;
private final Set extraInterfaces;
/**
* Creates the wrapper that be used in the serialization stream.
*
* Immediately serializes the Mockito mock using specifically crafted
* {@link AcrossJVMSerializationFeature.MockitoMockObjectOutputStream},
* in a byte array.
*
* @param mockitoMock The Mockito mock to serialize.
* @throws IOException
*/
public AcrossJVMMockSerializationProxy(Object mockitoMock) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new MockitoMockObjectOutputStream(out);
objectOutputStream.writeObject(mockitoMock);
objectOutputStream.close();
out.close();
MockCreationSettings mockSettings = MockUtil.getMockSettings(mockitoMock);
this.serializedMock = out.toByteArray();
this.typeToMock = mockSettings.getTypeToMock();
this.extraInterfaces = mockSettings.getExtraInterfaces();
}
/**
* Resolves the proxy to a new deserialized instance of the Mockito mock.
*
* Uses the custom crafted {@link MockitoMockObjectInputStream} to deserialize the mock.
*
* @return A deserialized instance of the Mockito mock.
* @throws ObjectStreamException
*/
private Object readResolve() throws ObjectStreamException {
try {
ByteArrayInputStream bis = new ByteArrayInputStream(serializedMock);
ObjectInputStream objectInputStream = new MockitoMockObjectInputStream(bis, typeToMock, extraInterfaces);
Object deserializedMock = objectInputStream.readObject();
bis.close();
objectInputStream.close();
return deserializedMock;
} catch (IOException ioe) {
throw new MockitoSerializationIssue(join(
"Mockito mock cannot be deserialized to a mock of '" + typeToMock.getCanonicalName() + "'. The error was :",
" " + ioe.getMessage(),
"If you are unsure what is the reason of this exception, feel free to contact us on the mailing list."
), ioe);
} catch (ClassNotFoundException cce) {
throw new MockitoSerializationIssue(join(
"A class couldn't be found while deserializing a Mockito mock, you should check your classpath. The error was :",
" " + cce.getMessage(),
"If you are still unsure what is the reason of this exception, feel free to contact us on the mailing list."
), cce);
}
}
}
/**
* Special Mockito aware ObjectInputStream
that will resolve the Mockito proxy class.
*
*
* This specificaly crafted ObjectInoutStream has the most important role to resolve the Mockito generated
* class. It is doing so via the {@link #resolveClass(ObjectStreamClass)} which looks in the stream
* for a Mockito marker. If this marker is found it will try to resolve the mockito class otherwise it
* delegates class resolution to the default super behavior.
* The mirror method used for serializing the mock is
* {@link AcrossJVMSerializationFeature.MockitoMockObjectOutputStream#annotateClass(Class)}.
*
*
*
* When this marker is found, {@link org.powermock.api.mockito.repackaged.ClassImposterizer} methods are being used to create the mock class.
* Note that behind the ClassImposterizer
there is CGLIB and the
* {@link org.powermock.api.mockito.repackaged.SearchingClassLoader} that will look if this enhanced class has
* already been created in an accessible classloader ; so basically this code trusts the ClassImposterizer
* code.
*
*/
public static class MockitoMockObjectInputStream extends ObjectInputStream {
private final Class typeToMock;
private final Set extraInterfaces;
public MockitoMockObjectInputStream(InputStream in, Class typeToMock, Set extraInterfaces) throws IOException {
super(in);
this.typeToMock = typeToMock;
this.extraInterfaces = extraInterfaces;
enableResolveObject(true); // ensure resolving is enabled
}
/**
* Resolve the Mockito proxy class if it is marked as such.
*
* Uses the fields {@link #typeToMock} and {@link #extraInterfaces} to
* create the Mockito proxy class as the ObjectStreamClass
* doesn't carry useful information for this purpose.
*
* @param desc Description of the class in the stream, not used.
* @return The class that will be used to deserialize the instance mock.
* @throws IOException
* @throws ClassNotFoundException
*/
@Override
protected Class> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (notMarkedAsAMockitoMock(readObject())) {
return super.resolveClass(desc);
}
// TODO check the class is mockable in the deserialization side
// ClassImposterizer.INSTANCE.canImposterise(typeToMock);
// create the Mockito mock class before it can even be deserialized
//TODO SF unify creation of imposterizer, constructor code duplicated
ClassImposterizer imposterizer = new ClassImposterizer(new DefaultInstantiatorProvider().getInstantiator(new CreationSettings()));
imposterizer.setConstructorsAccessible(typeToMock, true);
Class> proxyClass = imposterizer.createProxyClass(
typeToMock,
extraInterfaces.toArray(new Class[extraInterfaces.size()])
);
hackClassNameToMatchNewlyCreatedClass(desc, proxyClass);
return proxyClass;
}
/**
* Hack the name
field of the given ObjectStreamClass
with
* the newProxyClass
.
*
* The parent ObjectInputStream will check the name of the class in the stream matches the name of the one
* that is created in this method.
*
* The CGLIB classes uses a hash of the classloader and/or maybe some other data that allow them to be
* relatively unique in a JVM.
*
* When names differ, which happens when the mock is deserialized in another ClassLoader, a
* java.io.InvalidObjectException
is thrown, so this part of the code is hacking through
* the given ObjectStreamClass
to change the name with the newly created class.
*
* @param descInstance The ObjectStreamClass
that will be hacked.
* @param proxyClass The proxy class whose name will be applied.
* @throws InvalidObjectException
*/
private void hackClassNameToMatchNewlyCreatedClass(ObjectStreamClass descInstance, Class> proxyClass) throws ObjectStreamException {
try {
Field classNameField = descInstance.getClass().getDeclaredField("name");
FieldSetter.setField(descInstance, classNameField, proxyClass.getCanonicalName());
} catch (NoSuchFieldException nsfe) {
// TODO use our own mockito mock serialization exception
throw new MockitoSerializationIssue(join(
"Wow, the class 'ObjectStreamClass' in the JDK don't have the field 'name',",
"this is definitely a bug in our code as it means the JDK team changed a few internal things.",
"",
"Please report an issue with the JDK used, a code sample and a link to download the JDK would be welcome."
), nsfe);
}
}
/**
* Read the stream class annotation and identify it as a Mockito mock or not.
*
* @param marker The marker to identify.
* @return true
if not marked as a Mockito, false
if the class annotation marks a Mockito mock.
* @throws IOException
* @throws ClassNotFoundException
*/
private boolean notMarkedAsAMockitoMock(Object marker) throws IOException, ClassNotFoundException {
return !MOCKITO_PROXY_MARKER.equals(marker);
}
}
/**
* Special Mockito aware ObjectOutputStream
.
*
*
* This output stream has the role of marking in the stream the Mockito class. This
* marking process is necessary to identify the proxy class that will need to be recreated.
*
* The mirror method used for deserializing the mock is
* {@link MockitoMockObjectInputStream#resolveClass(ObjectStreamClass)}.
*
*/
private static class MockitoMockObjectOutputStream extends ObjectOutputStream {
private static final String NOTHING = "";
public MockitoMockObjectOutputStream(ByteArrayOutputStream out) throws IOException {
super(out);
}
/**
* Annotates (marks) the class if this class is a Mockito mock.
*
* @param cl The class to annotate.
* @throws IOException
*/
@Override
protected void annotateClass(Class> cl) throws IOException {
writeObject(mockitoProxyClassMarker(cl));
// might be also useful later, for embedding classloader info ...maybe ...maybe not
}
/**
* Returns the Mockito marker if this class is a Mockito mock.
*
* @param cl The class to mark.
* @return The marker if this is a Mockito proxy class, otherwise returns a void marker.
*/
private String mockitoProxyClassMarker(Class> cl) {
if (AcrossJVMMockitoMockSerializable.class.isAssignableFrom(cl)) {
return MOCKITO_PROXY_MARKER;
} else {
return NOTHING;
}
}
}
}