All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.fitbur.mockito.internal.creation.bytebuddy.ByteBuddyCrossClassLoaderSerializationSupport Maven / Gradle / Ivy

There is a newer version: 1.0.0
Show newest version
/*
 * Copyright (c) 2007 Mockito contributors
 * This program is made available under the terms of the MIT License.
 */

package com.fitbur.mockito.internal.creation.bytebuddy;

import static com.fitbur.mockito.internal.creation.bytebuddy.MockFeatures.withMockFeatures;
import static com.fitbur.mockito.internal.creation.bytebuddy.MockMethodInterceptor.*;
import static com.fitbur.mockito.internal.util.StringJoiner.join;
import static com.fitbur.mockito.internal.util.reflection.FieldSetter.setField;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
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.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import com.fitbur.mockito.Incubating;
import com.fitbur.mockito.exceptions.base.MockitoSerializationIssue;
import com.fitbur.mockito.internal.configuration.plugins.Plugins;
import com.fitbur.mockito.internal.creation.bytebuddy.MockMethodInterceptor.ForWriteReplace;
import com.fitbur.mockito.internal.util.MockUtil;
import com.fitbur.mockito.internal.util.reflection.FieldSetter;
import com.fitbur.mockito.mock.MockCreationSettings;
import com.fitbur.mockito.mock.MockName;

/**
 * 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 flag {@link MockFeatures#crossClassLoaderSerializable}, * 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. *

* *

* Then in the proxy class there will be a generated writeReplace 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. *

* *

Only one instance per mock! See {@link MockMethodInterceptor}

* * TODO check the class is mockable in the deserialization side * * @see com.fitbur.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker * @see com.fitbur.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 writeReplace method for serialization. *

* Here's how it's working and why : *

    * *
  1. *

    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 CrossClassLoaderSerializableMock}.

    *
  2. *
  3. *

    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 * 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.

    *
  4. *
* * @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) { MockUtil mockUtil = new MockUtil(); 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. *

*

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. *

*

*

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 CrossClassLoaderSerializationProxy 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 MockitoMockObjectOutputStream}, * in a byte array.

* * @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 = new 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 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 ObjectInputStream that will resolve the Mockito proxy class. *

*

* 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)}. *

*

*

* When this marker is found, {@link ByteBuddyMockMaker#createProxyClass(MockFeatures)} methods are being used * to create the mock class. *

*/ 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 java.io.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 // create the Mockito mock class before it can even be deserialized Class proxyClass = ((ByteBuddyMockMaker) Plugins.getMockMaker()) .createProxyClass(withMockFeatures(typeToMock, extraInterfaces, true)); 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 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 true if not marked as a Mockito, false if the class annotation marks a Mockito mock. * @throws java.io.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 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 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. */ public interface CrossClassLoaderSerializableMock { Object writeReplace() throws ObjectStreamException; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy