1 /* 2 * Copyright (c) 2007 Mockito contributors 3 * This program is made available under the terms of the MIT License. 4 */ 5 6 package org.mockito.internal.creation; 7 8 import org.mockito.Incubating; 9 import org.mockito.exceptions.base.MockitoSerializationIssue; 10 import org.mockito.internal.creation.jmock.ClassImposterizer; 11 import org.mockito.internal.util.MockUtil; 12 import org.mockito.internal.util.reflection.FieldSetter; 13 import org.mockito.mock.MockCreationSettings; 14 import org.mockito.mock.MockName; 15 16 import java.io.*; 17 import java.lang.reflect.Field; 18 import java.lang.reflect.Method; 19 import java.util.Set; 20 import java.util.concurrent.locks.Lock; 21 import java.util.concurrent.locks.ReentrantLock; 22 23 import static org.mockito.internal.util.StringJoiner.join; 24 25 /** 26 * This is responsible for serializing a mock, it is enabled if the mock is implementing 27 * {@link Serializable}. 28 * 29 * <p> 30 * The way it works is to enable serialization via the {@link #enableSerializationAcrossJVM(MockCreationSettings)}, 31 * if the mock settings is set to be serializable it will add the {@link org.mockito.internal.creation.AcrossJVMSerializationFeature.AcrossJVMMockitoMockSerializable} interface. 32 * This interface defines a the {@link org.mockito.internal.creation.AcrossJVMSerializationFeature.AcrossJVMMockitoMockSerializable#writeReplace()} 33 * whose signature match the one that is looked by the standard Java serialization. 34 * </p> 35 * 36 * <p> 37 * Then in the {@link MethodInterceptorFilter} of mockito, if the <code>writeReplace</code> method is called, 38 * it will use the custom implementation of this class {@link #writeReplace(Object)}. This method has a specific 39 * knowledge on how to serialize a mockito mock that is based on CGLIB. 40 * </p> 41 * 42 * <p><strong>Only one instance per mock! See {@link MethodInterceptorFilter}</strong></p> 43 * 44 * TODO use a proper way to add the interface 45 * TODO offer a way to disable completely this behavior, or maybe enable this behavior only with a specific setting 46 * TODO check the class is mockable in the deserialization side 47 * 48 * @see CglibMockMaker 49 * @see MethodInterceptorFilter 50 * @author Brice Dutheil 51 * @since 1.9.6 52 */ 53 @Incubating 54 public class AcrossJVMSerializationFeature implements Serializable { 55 private static final long serialVersionUID = 7411152578314420778L; 56 private static final String MOCKITO_PROXY_MARKER = "MockitoProxyMarker"; 57 private boolean instanceLocalCurrentlySerializingFlag = false; 58 private Lock mutex = new ReentrantLock(); 59 isWriteReplace(Method method)60 public boolean isWriteReplace(Method method) { 61 return method.getReturnType() == Object.class 62 && method.getParameterTypes().length == 0 63 && method.getName().equals("writeReplace"); 64 } 65 66 67 /** 68 * Custom implementation of the <code>writeReplace</code> method for serialization. 69 * 70 * Here's how it's working and why : 71 * <ol> 72 * <li> 73 * <p>When first entering in this method, it's because some is serializing the mock, with some code like : 74 * <pre class="code"><code class="java"> 75 * objectOutputStream.writeObject(mock); 76 * </code></pre> 77 * So, {@link ObjectOutputStream} will track the <code>writeReplace</code> method in the instance and 78 * execute it, which is wanted to replace the mock by another type that will encapsulate the actual mock. 79 * At this point, the code will return an {@link AcrossJVMMockSerializationProxy}.</p> 80 * </li> 81 * <li> 82 * <p>Now, in the constructor {@link AcrossJVMMockSerializationProxy#AcrossJVMMockSerializationProxy(Object)} 83 * the mock is being serialized in a custom way (using {@link MockitoMockObjectOutputStream}) to a 84 * byte array. So basically it means the code is performing double nested serialization of the passed 85 * <code>mockitoMock</code>.</p> 86 * 87 * <p>However the <code>ObjectOutputStream</code> will still detect the custom 88 * <code>writeReplace</code> and execute it. 89 * <em>(For that matter disabling replacement via {@link ObjectOutputStream#enableReplaceObject(boolean)} 90 * doesn't disable the <code>writeReplace</code> call, but just just toggle replacement in the 91 * written stream, <strong><code>writeReplace</code> is always called by 92 * <code>ObjectOutputStream</code></strong>.)</em></p> 93 * 94 * <p>In order to avoid this recursion, obviously leading to a {@link StackOverflowError}, this method is using 95 * a flag that marks the mock as already being replaced, and then shouldn't replace itself again. 96 * <strong>This flag is local to this class</strong>, which means the flag of this class unfortunately needs 97 * to be protected against concurrent access, hence the reentrant lock.</p> 98 * </li> 99 * </ol> 100 * 101 * 102 * @param mockitoMock The Mockito mock to be serialized. 103 * @return A wrapper ({@link AcrossJVMMockSerializationProxy}) to be serialized by the calling ObjectOutputStream. 104 * @throws ObjectStreamException 105 */ writeReplace(Object mockitoMock)106 public Object writeReplace(Object mockitoMock) throws ObjectStreamException { 107 try { 108 // reentrant lock for critical section. could it be improved ? 109 mutex.lock(); 110 // mark started flag // per thread, not per instance 111 // temporary loosy hack to avoid stackoverflow 112 if(mockIsCurrentlyBeingReplaced()) { 113 return mockitoMock; 114 } 115 mockReplacementStarted(); 116 117 return new AcrossJVMMockSerializationProxy(mockitoMock); 118 } catch (IOException ioe) { 119 MockUtil mockUtil = new MockUtil(); 120 MockName mockName = mockUtil.getMockName(mockitoMock); 121 String mockedType = mockUtil.getMockSettings(mockitoMock).getTypeToMock().getCanonicalName(); 122 throw new MockitoSerializationIssue(join( 123 "The mock '" + mockName + "' of type '" + mockedType + "'", 124 "The Java Standard Serialization reported an '" + ioe.getClass().getSimpleName() + "' saying :", 125 " " + ioe.getMessage() 126 ), ioe); 127 } finally { 128 // unmark 129 mockReplacementCompleted(); 130 mutex.unlock(); 131 } 132 } 133 134 mockReplacementCompleted()135 private void mockReplacementCompleted() { 136 instanceLocalCurrentlySerializingFlag = false; 137 } 138 139 mockReplacementStarted()140 private void mockReplacementStarted() { 141 instanceLocalCurrentlySerializingFlag = true; 142 } 143 144 mockIsCurrentlyBeingReplaced()145 private boolean mockIsCurrentlyBeingReplaced() { 146 return instanceLocalCurrentlySerializingFlag; 147 } 148 149 150 /** 151 * Enable serialization serialization that will work across classloaders / and JVM. 152 * 153 * <p>Only enable if settings says the mock should be serializable. In this case add the 154 * {@link AcrossJVMMockitoMockSerializable} to the extra interface list.</p> 155 * 156 * @param settings Mock creation settings. 157 * @param <T> Type param to not be bothered by the generics 158 */ enableSerializationAcrossJVM(MockCreationSettings<T> settings)159 public <T> void enableSerializationAcrossJVM(MockCreationSettings<T> settings) { 160 if (settings.isSerializable()) { 161 // havin faith that this set is modifiable 162 // TODO use a proper way to add the interface 163 settings.getExtraInterfaces().add(AcrossJVMMockitoMockSerializable.class); 164 } 165 } 166 167 168 /** 169 * This is the serialization proxy that will encapsulate the real mock data as a byte array. 170 * 171 * <p>When called in the constructor it will serialize the mock in a byte array using a 172 * custom {@link MockitoMockObjectOutputStream} that will annotate the mock class in the stream. 173 * other information are used in this class in order to facilitate deserialization. 174 * </p> 175 * 176 * <p>Deserialization of the mock will be performed by the {@link #readResolve()} method via 177 * the custom {@link MockitoMockObjectInputStream} that will be in charge of creating the mock class.</p> 178 */ 179 public static class AcrossJVMMockSerializationProxy implements Serializable { 180 181 182 private static final long serialVersionUID = -7600267929109286514L; 183 private byte[] serializedMock; 184 private Class typeToMock; 185 private Set<Class> extraInterfaces; 186 /** 187 * Creates the wrapper that be used in the serialization stream. 188 * 189 * <p>Immediately serializes the Mockito mock using specifically crafted {@link MockitoMockObjectOutputStream}, 190 * in a byte array.</p> 191 * 192 * @param mockitoMock The Mockito mock to serialize. 193 * @throws IOException 194 */ AcrossJVMMockSerializationProxy(Object mockitoMock)195 public AcrossJVMMockSerializationProxy(Object mockitoMock) throws IOException { 196 ByteArrayOutputStream out = new ByteArrayOutputStream(); 197 ObjectOutputStream objectOutputStream = new MockitoMockObjectOutputStream(out); 198 199 objectOutputStream.writeObject(mockitoMock); 200 201 objectOutputStream.close(); 202 out.close(); 203 204 MockCreationSettings mockSettings = new MockUtil().getMockSettings(mockitoMock); 205 this.serializedMock = out.toByteArray(); 206 this.typeToMock = mockSettings.getTypeToMock(); 207 this.extraInterfaces = mockSettings.getExtraInterfaces(); 208 } 209 210 /** 211 * Resolves the proxy to a new deserialized instance of the Mockito mock. 212 * 213 * <p>Uses the custom crafted {@link MockitoMockObjectInputStream} to deserialize the mock.</p> 214 * 215 * @return A deserialized instance of the Mockito mock. 216 * @throws ObjectStreamException 217 */ readResolve()218 private Object readResolve() throws ObjectStreamException { 219 try { 220 ByteArrayInputStream bis = new ByteArrayInputStream(serializedMock); 221 ObjectInputStream objectInputStream = new MockitoMockObjectInputStream(bis, typeToMock, extraInterfaces); 222 223 Object deserializedMock = objectInputStream.readObject(); 224 225 bis.close(); 226 objectInputStream.close(); 227 228 return deserializedMock; 229 } catch (IOException ioe) { 230 throw new MockitoSerializationIssue(join( 231 "Mockito mock cannot be deserialized to a mock of '" + typeToMock.getCanonicalName() + "'. The error was :", 232 " " + ioe.getMessage(), 233 "If you are unsure what is the reason of this exception, feel free to contact us on the mailing list." 234 ), ioe); 235 } catch (ClassNotFoundException cce) { 236 throw new MockitoSerializationIssue(join( 237 "A class couldn't be found while deserializing a Mockito mock, you should check your classpath. The error was :", 238 " " + cce.getMessage(), 239 "If you are still unsure what is the reason of this exception, feel free to contact us on the mailing list." 240 ), cce); 241 } 242 } 243 } 244 245 246 /** 247 * Special Mockito aware <code>ObjectInputStream</code> that will resolve the Mockito proxy class. 248 * 249 * <p> 250 * This specificaly crafted ObjectInoutStream has the most important role to resolve the Mockito generated 251 * class. It is doing so via the {@link #resolveClass(java.io.ObjectStreamClass)} which looks in the stream 252 * for a Mockito marker. If this marker is found it will try to resolve the mockito class otherwise it 253 * delegates class resolution to the default super behavior. 254 * The mirror method used for serializing the mock is {@link MockitoMockObjectOutputStream#annotateClass(Class)}. 255 * </p> 256 * 257 * <p> 258 * When this marker is found, {@link ClassImposterizer} methods are being used to create the mock class. 259 * <em>Note that behind the <code>ClassImposterizer</code> there is CGLIB and the 260 * {@link org.mockito.internal.creation.jmock.SearchingClassLoader} that will look if this enhanced class has 261 * already been created in an accessible classloader ; so basically this code trusts the ClassImposterizer 262 * code.</em> 263 * </p> 264 */ 265 public static class MockitoMockObjectInputStream extends ObjectInputStream { 266 private Class typeToMock; 267 private Set<Class> extraInterfaces; 268 MockitoMockObjectInputStream(InputStream in, Class typeToMock, Set<Class> extraInterfaces)269 public MockitoMockObjectInputStream(InputStream in, Class typeToMock, Set<Class> extraInterfaces) throws IOException { 270 super(in) ; 271 this.typeToMock = typeToMock; 272 this.extraInterfaces = extraInterfaces; 273 enableResolveObject(true); // ensure resolving is enabled 274 } 275 276 /** 277 * Resolve the Mockito proxy class if it is marked as such. 278 * 279 * <p>Uses the fields {@link #typeToMock} and {@link #extraInterfaces} to 280 * create the Mockito proxy class as the <code>ObjectStreamClass</code> 281 * doesn't carry useful information for this purpose.</p> 282 * 283 * @param desc Description of the class in the stream, not used. 284 * @return The class that will be used to deserialize the instance mock. 285 * @throws IOException 286 * @throws ClassNotFoundException 287 */ 288 @Override resolveClass(ObjectStreamClass desc)289 protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { 290 if (notMarkedAsAMockitoMock(readObject())) { 291 return super.resolveClass(desc); 292 } 293 294 // TODO check the class is mockable in the deserialization side 295 // ClassImposterizer.INSTANCE.canImposterise(typeToMock); 296 297 // create the Mockito mock class before it can even be deserialized 298 ClassImposterizer.INSTANCE.setConstructorsAccessible(typeToMock, true); 299 Class<?> proxyClass = ClassImposterizer.INSTANCE.createProxyClass( 300 typeToMock, 301 extraInterfaces.toArray(new Class[extraInterfaces.size()]) 302 ); 303 304 hackClassNameToMatchNewlyCreatedClass(desc, proxyClass); 305 306 return proxyClass; 307 308 } 309 310 /** 311 * Hack the <code>name</code> field of the given <code>ObjectStreamClass</code> with 312 * the <code>newProxyClass</code>. 313 * 314 * The parent ObjectInputStream will check the name of the class in the stream matches the name of the one 315 * that is created in this method. 316 * 317 * The CGLIB classes uses a hash of the classloader and/or maybe some other data that allow them to be 318 * relatively unique in a JVM. 319 * 320 * When names differ, which happens when the mock is deserialized in another ClassLoader, a 321 * <code>java.io.InvalidObjectException</code> is thrown, so this part of the code is hacking through 322 * the given <code>ObjectStreamClass</code> to change the name with the newly created class. 323 * 324 * @param descInstance The <code>ObjectStreamClass</code> that will be hacked. 325 * @param proxyClass The proxy class whose name will be applied. 326 * @throws InvalidObjectException 327 */ hackClassNameToMatchNewlyCreatedClass(ObjectStreamClass descInstance, Class<?> proxyClass)328 private void hackClassNameToMatchNewlyCreatedClass(ObjectStreamClass descInstance, Class<?> proxyClass) throws ObjectStreamException { 329 try { 330 Field classNameField = descInstance.getClass().getDeclaredField("name"); 331 new FieldSetter(descInstance, classNameField).set(proxyClass.getCanonicalName()); 332 } catch (NoSuchFieldException nsfe) { 333 // TODO use our own mockito mock serialization exception 334 throw new MockitoSerializationIssue(join( 335 "Wow, the class 'ObjectStreamClass' in the JDK don't have the field 'name',", 336 "this is definitely a bug in our code as it means the JDK team changed a few internal things.", 337 "", 338 "Please report an issue with the JDK used, a code sample and a link to download the JDK would be welcome." 339 ), nsfe); 340 } 341 } 342 343 /** 344 * Read the stream class annotation and identify it as a Mockito mock or not. 345 * 346 * @param marker The marker to identify. 347 * @return <code>true</code> if not marked as a Mockito, <code>false</code> if the class annotation marks a Mockito mock. 348 * @throws IOException 349 * @throws ClassNotFoundException 350 */ notMarkedAsAMockitoMock(Object marker)351 private boolean notMarkedAsAMockitoMock(Object marker) throws IOException, ClassNotFoundException { 352 return !MOCKITO_PROXY_MARKER.equals(marker); 353 } 354 } 355 356 357 /** 358 * Special Mockito aware <code>ObjectOutputStream</code>. 359 * 360 * <p> 361 * This output stream has the role of marking in the stream the Mockito class. This 362 * marking process is necessary to identify the proxy class that will need to be recreated. 363 * 364 * The mirror method used for deserializing the mock is 365 * {@link MockitoMockObjectInputStream#resolveClass(ObjectStreamClass)}. 366 * </p> 367 * 368 */ 369 private static class MockitoMockObjectOutputStream extends ObjectOutputStream { 370 private static final String NOTHING = ""; 371 private MockUtil mockUtil = new MockUtil(); 372 MockitoMockObjectOutputStream(ByteArrayOutputStream out)373 public MockitoMockObjectOutputStream(ByteArrayOutputStream out) throws IOException { 374 super(out); 375 } 376 377 /** 378 * Annotates (marks) the class if this class is a Mockito mock. 379 * 380 * @param cl The class to annotate. 381 * @throws IOException 382 */ 383 @Override annotateClass(Class<?> cl)384 protected void annotateClass(Class<?> cl) throws IOException { 385 writeObject(mockitoProxyClassMarker(cl)); 386 // might be also useful later, for embedding classloader info ...maybe ...maybe not 387 } 388 389 /** 390 * Returns the Mockito marker if this class is a Mockito mock. 391 * 392 * @param cl The class to mark. 393 * @return The marker if this is a Mockito proxy class, otherwise returns a void marker. 394 */ mockitoProxyClassMarker(Class<?> cl)395 private String mockitoProxyClassMarker(Class<?> cl) { 396 if (mockUtil.isMock(cl)) { 397 return MOCKITO_PROXY_MARKER; 398 } else { 399 return NOTHING; 400 } 401 } 402 } 403 404 405 /** 406 * Simple interface that hold a correct <code>writeReplace</code> signature that can be seen by an 407 * <code>ObjectOutputStream</code>. 408 * 409 * It will be applied before the creation of the mock when the mock setting says it should serializable. 410 * 411 * @see #enableSerializationAcrossJVM(org.mockito.mock.MockCreationSettings) 412 */ 413 public interface AcrossJVMMockitoMockSerializable { writeReplace()414 public Object writeReplace() throws java.io.ObjectStreamException; 415 } 416 } 417