/* * Copyright (c) 2007 Mockito contributors * This program is made available under the terms of the MIT License. */ package org.mockito.internal.creation.bytebuddy; import org.mockito.Incubating; import org.mockito.exceptions.base.MockitoSerializationIssue; import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.creation.settings.CreationSettings; import org.mockito.internal.util.MockUtil; import org.mockito.mock.MockCreationSettings; import org.mockito.mock.MockName; import org.mockito.mock.SerializableMode; import java.io.*; import java.lang.reflect.Field; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import static org.mockito.internal.creation.bytebuddy.MockMethodInterceptor.ForWriteReplace; import static org.mockito.internal.util.StringJoiner.join; import static org.mockito.internal.util.reflection.FieldSetter.setField; /** * This is responsible for serializing a mock, it is enabled if the mock is implementing {@link Serializable}. * * <p> * The way it works is to enable serialization with mode {@link SerializableMode#ACROSS_CLASSLOADERS}, * if the mock settings is set to be serializable the mock engine will implement the * {@link CrossClassLoaderSerializableMock} marker interface. * This interface defines a the {@link CrossClassLoaderSerializableMock#writeReplace()} * whose signature match the one that is looked by the standard Java serialization. * </p> * * <p> * Then in the proxy class there will be a generated <code>writeReplace</code> that will delegate to * {@link ForWriteReplace#doWriteReplace(MockAccess)} of mockito, and in turn will delegate to 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 ByteBuddy and will ignore other Mockito MockMakers. * </p> * * <p><strong>Only one instance per mock! See {@link MockMethodInterceptor}</strong></p> * * TODO check the class is mockable in the deserialization side * * @see SubclassByteBuddyMockMaker * @see org.mockito.internal.creation.bytebuddy.MockMethodInterceptor * @author Brice Dutheil * @since 1.10.0 */ @Incubating class ByteBuddyCrossClassLoaderSerializationSupport implements Serializable { private static final long serialVersionUID = 7411152578314420778L; private static final String MOCKITO_PROXY_MARKER = "ByteBuddyMockitoProxyMarker"; private boolean instanceLocalCurrentlySerializingFlag = false; private final Lock mutex = new ReentrantLock(); /** * Custom implementation of the <code>writeReplace</code> method for serialization. * <p/> * Here's how it's working and why : * <ol> * * <li> * <p>When first entering in this method, it's because some is serializing the mock, with some code like :</p> * * <pre class="code"><code class="java"> * objectOutputStream.writeObject(mock); * </code></pre> * * <p>So, {@link ObjectOutputStream} will track the <code>writeReplace</code> 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 CrossClassLoaderSerializableMock}.</p> * </li> * <li> * <p>Now, in the constructor * {@link CrossClassLoaderSerializationProxy#CrossClassLoaderSerializationProxy(java.lang.Object)} * the mock is being serialized in a custom way (using {@link MockitoMockObjectOutputStream}) to a * byte array. So basically it means the code is performing double nested serialization of the passed * <code>mockitoMock</code>.</p> * * <p>However the <code>ObjectOutputStream</code> will still detect the custom * <code>writeReplace</code> and execute it. * <em>(For that matter disabling replacement via {@link ObjectOutputStream#enableReplaceObject(boolean)} * doesn't disable the <code>writeReplace</code> call, but just just toggle replacement in the * written stream, <strong><code>writeReplace</code> is always called by * <code>ObjectOutputStream</code></strong>.)</em></p> * * <p>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. * <strong>This flag is local to this class</strong>, which means the flag of this class unfortunately needs * to be protected against concurrent access, hence the reentrant lock.</p> * </li> * </ol> * * @param mockitoMock The Mockito mock to be serialized. * @return A wrapper ({@link CrossClassLoaderSerializationProxy}) to be serialized by the calling ObjectOutputStream. * @throws java.io.ObjectStreamException */ public Object writeReplace(Object mockitoMock) throws ObjectStreamException { // reentrant lock for critical section. could it be improved ? mutex.lock(); try { // mark started flag // per thread, not per instance // temporary loosy hack to avoid stackoverflow if (mockIsCurrentlyBeingReplaced()) { return mockitoMock; } mockReplacementStarted(); return new CrossClassLoaderSerializationProxy(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; } /** * This is the serialization proxy that will encapsulate the real mock data as a byte array. * <p/> * <p>When called in the constructor it will serialize the mock in a byte array using a * custom {@link MockitoMockObjectOutputStream} that will annotate the mock class in the stream. * Other information are used in this class in order to facilitate deserialization. * </p> * <p/> * <p>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.</p> */ public static class CrossClassLoaderSerializationProxy implements Serializable { private static final long serialVersionUID = -7600267929109286514L; private final byte[] serializedMock; private final Class<?> typeToMock; private final Set<Class<?>> extraInterfaces; /** * Creates the wrapper that be used in the serialization stream. * * <p>Immediately serializes the Mockito mock using specifically crafted {@link MockitoMockObjectOutputStream}, * in a byte array.</p> * * @param mockitoMock The Mockito mock to serialize. * @throws java.io.IOException */ public CrossClassLoaderSerializationProxy(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. * <p/> * <p>Uses the custom crafted {@link MockitoMockObjectInputStream} to deserialize the mock.</p> * * @return A deserialized instance of the Mockito mock. * @throws java.io.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 <code>ObjectInputStream</code> that will resolve the Mockito proxy class. * <p/> * <p> * This specifically 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 MockitoMockObjectOutputStream#annotateClass(Class)}. * </p> * <p/> * <p> * When this marker is found, {@link ByteBuddyMockMaker#createMockType(MockCreationSettings)} methods are being used * to create the mock class. * </p> */ public static class MockitoMockObjectInputStream extends ObjectInputStream { private final Class<?> typeToMock; private final Set<Class<?>> extraInterfaces; public MockitoMockObjectInputStream(InputStream in, Class<?> typeToMock, Set<Class<?>> 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. * <p/> * <p>Uses the fields {@link #typeToMock} and {@link #extraInterfaces} to * create the Mockito proxy class as the <code>ObjectStreamClass</code> * doesn't carry useful information for this purpose.</p> * * @param desc Description of the class in the stream, not used. * @return The class that will be used to deserialize the instance mock. * @throws java.io.IOException * @throws ClassNotFoundException */ @Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { if (notMarkedAsAMockitoMock(readObject())) { return super.resolveClass(desc); } // create the Mockito mock class before it can even be deserialized try { @SuppressWarnings("unchecked") Class<?> proxyClass = ((ClassCreatingMockMaker) Plugins.getMockMaker()).createMockType( new CreationSettings() .setTypeToMock(typeToMock) .setExtraInterfaces(extraInterfaces) .setSerializableMode(SerializableMode.ACROSS_CLASSLOADERS)); hackClassNameToMatchNewlyCreatedClass(desc, proxyClass); return proxyClass; } catch (ClassCastException cce) { throw new MockitoSerializationIssue(join( "A Byte Buddy-generated mock cannot be deserialized into a non-Byte Buddy generated mock class", "", "The mock maker in use was: " + Plugins.getMockMaker().getClass() ), cce); } } /** * Hack the <code>name</code> field of the given <code>ObjectStreamClass</code> with * the <code>newProxyClass</code>. * <p/> * 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. * <p/> * 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. * <p/> * When names differ, which happens when the mock is deserialized in another ClassLoader, a * <code>java.io.InvalidObjectException</code> is thrown, so this part of the code is hacking through * the given <code>ObjectStreamClass</code> to change the name with the newly created class. * * @param descInstance The <code>ObjectStreamClass</code> that will be hacked. * @param proxyClass The proxy class whose name will be applied. * @throws java.io.InvalidObjectException */ private void hackClassNameToMatchNewlyCreatedClass(ObjectStreamClass descInstance, Class<?> proxyClass) throws ObjectStreamException { try { Field classNameField = descInstance.getClass().getDeclaredField("name"); setField(descInstance, classNameField,proxyClass.getCanonicalName()); } catch (NoSuchFieldException nsfe) { 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 <code>true</code> if not marked as a Mockito, <code>false</code> if the class annotation marks a Mockito mock. */ private boolean notMarkedAsAMockitoMock(Object marker) { return !MOCKITO_PROXY_MARKER.equals(marker); } } /** * Special Mockito aware <code>ObjectOutputStream</code>. * <p/> * <p> * 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. * <p/> * The mirror method used for deserializing the mock is * {@link MockitoMockObjectInputStream#resolveClass(ObjectStreamClass)}. * </p> */ 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 java.io.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 (CrossClassLoaderSerializableMock.class.isAssignableFrom(cl)) { return MOCKITO_PROXY_MARKER; } else { return NOTHING; } } } /** * Simple interface that hold a correct <code>writeReplace</code> signature that can be seen by an * <code>ObjectOutputStream</code>. * <p/> * It will be applied before the creation of the mock when the mock setting says it should serializable. */ public interface CrossClassLoaderSerializableMock { Object writeReplace(); } }