Skip to content

Commit

Permalink
Add support for creating constructors without using Objenesis (and un…
Browse files Browse the repository at this point in the history
…safe API)
  • Loading branch information
raphw committed Jul 13, 2020
1 parent 05b39bf commit ddb2acf
Show file tree
Hide file tree
Showing 6 changed files with 267 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
*/
public class InstantiationException extends MockitoException {

/**
* @since 3.5.0
*/
public InstantiationException(String message) {
super(message);
}

/**
* @since 2.15.4
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.WeakHashMap;
Expand All @@ -24,6 +25,7 @@

import net.bytebuddy.agent.ByteBuddyAgent;
import org.mockito.Incubating;
import org.mockito.creation.instance.InstantiationException;
import org.mockito.creation.instance.Instantiator;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.exceptions.base.MockitoInitializationException;
Expand Down Expand Up @@ -94,7 +96,8 @@
* support this feature.
*/
@Incubating
public class InlineByteBuddyMockMaker implements ClassCreatingMockMaker, InlineMockMaker {
public class InlineByteBuddyMockMaker
implements ClassCreatingMockMaker, InlineMockMaker, Instantiator {

private static final Instrumentation INSTRUMENTATION;

Expand Down Expand Up @@ -189,6 +192,8 @@ public class InlineByteBuddyMockMaker implements ClassCreatingMockMaker, InlineM
private final DetachedThreadLocal<Map<Class<?>, MockMethodInterceptor>> mockedStatics =
new DetachedThreadLocal<>(DetachedThreadLocal.Cleaner.INLINE);

private final ThreadLocal<Boolean> mockConstruction = ThreadLocal.withInitial(() -> false);

public InlineByteBuddyMockMaker() {
if (INITIALIZATION_ERROR != null) {
throw new MockitoInitializationException(
Expand All @@ -202,16 +207,25 @@ public InlineByteBuddyMockMaker() {
}
bytecodeGenerator =
new TypeCachingBytecodeGenerator(
new InlineBytecodeGenerator(INSTRUMENTATION, mocks, mockedStatics), true);
new InlineBytecodeGenerator(
INSTRUMENTATION, mocks, mockedStatics, mockConstruction::get),
true);
}

@Override
public <T> T createMock(MockCreationSettings<T> settings, MockHandler handler) {
Class<? extends T> type = createMockType(settings);

Instantiator instantiator = Plugins.getInstantiatorProvider().getInstantiator(settings);
try {
T instance = instantiator.newInstance(type);
T instance;
try {
// We attempt to use the "native" mock maker first that avoids Objenesis and Unsafe
instance = newInstance(type);
} catch (InstantiationException ignored) {
Instantiator instantiator =
Plugins.getInstantiatorProvider().getInstantiator(settings);
instance = instantiator.newInstance(type);
}
MockMethodInterceptor mockMethodInterceptor =
new MockMethodInterceptor(handler, settings);
mocks.put(instance, mockMethodInterceptor);
Expand Down Expand Up @@ -395,6 +409,64 @@ public <T> StaticMockControl<T> createStaticMock(
return new InlineStaticMockControl<>(type, interceptors, settings, handler);
}

@Override
@SuppressWarnings("unchecked")
public <T> T newInstance(Class<T> cls) throws InstantiationException {
Constructor<?>[] constructors = cls.getDeclaredConstructors();
if (constructors.length == 0) {
throw new InstantiationException(cls.getTypeName() + " does not define a constructor");
}
Constructor<?> selected = constructors[0];
for (Constructor<?> constructor : constructors) {
if (Modifier.isPublic(constructor.getModifiers())) {
selected = constructor;
break;
}
}
Class<?>[] types = selected.getParameterTypes();
Object[] arguments = new Object[types.length];
int index = 0;
for (Class<?> type : types) {
arguments[index++] = makeStandardArgument(type);
}
try {
if (!Modifier.isPublic(selected.getModifiers())
|| !Modifier.isPublic(cls.getModifiers())) {
selected.setAccessible(true);
}
mockConstruction.set(true);
try {
return (T) selected.newInstance(arguments);
} finally {
mockConstruction.set(false);
}
} catch (Exception e) {
throw new InstantiationException("Could not instantiate " + cls.getTypeName(), e);
}
}

private Object makeStandardArgument(Class<?> type) {
if (type == boolean.class) {
return false;
} else if (type == byte.class) {
return (byte) 0;
} else if (type == short.class) {
return (short) 0;
} else if (type == char.class) {
return (char) 0;
} else if (type == int.class) {
return 0;
} else if (type == long.class) {
return 0L;
} else if (type == float.class) {
return 0f;
} else if (type == double.class) {
return 0d;
} else {
return null;
}
}

private static class InlineStaticMockControl<T> implements StaticMockControl<T> {

private final Class<T> type;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.lang.reflect.Modifier;
import java.security.ProtectionDomain;
import java.util.*;
import java.util.function.BooleanSupplier;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.ClassFileVersion;
Expand Down Expand Up @@ -75,7 +76,8 @@ public class InlineBytecodeGenerator implements BytecodeGenerator, ClassFileTran
public InlineBytecodeGenerator(
Instrumentation instrumentation,
WeakConcurrentMap<Object, MockMethodInterceptor> mocks,
DetachedThreadLocal<Map<Class<?>, MockMethodInterceptor>> mockedStatics) {
DetachedThreadLocal<Map<Class<?>, MockMethodInterceptor>> mockedStatics,
BooleanSupplier isMockConstruction) {
preload();
this.instrumentation = instrumentation;
byteBuddy =
Expand Down Expand Up @@ -118,6 +120,7 @@ public InlineBytecodeGenerator(
Advice.withCustomMapping()
.bind(MockMethodAdvice.Identifier.class, identifier)
.to(MockMethodAdvice.ForStatic.class))
.constructor(any(), new MockMethodAdvice.ConstructorShortcut(identifier))
.method(
isHashCode(),
Advice.withCustomMapping()
Expand Down Expand Up @@ -150,7 +153,8 @@ public InlineBytecodeGenerator(
this.canRead = canRead;
this.redefineModule = redefineModule;
MockMethodDispatcher.set(
identifier, new MockMethodAdvice(mocks, mockedStatics, identifier));
identifier,
new MockMethodAdvice(mocks, mockedStatics, identifier, isMockConstruction));
instrumentation.addTransformer(this, true);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,25 @@
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.function.BooleanSupplier;

import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.asm.AsmVisitorWrapper;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.scaffold.MethodGraph;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.implementation.bind.annotation.Argument;
import net.bytebuddy.implementation.bind.annotation.This;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.jar.asm.Label;
import net.bytebuddy.jar.asm.MethodVisitor;
import net.bytebuddy.jar.asm.Opcodes;
import net.bytebuddy.jar.asm.Type;
import net.bytebuddy.pool.TypePool;
import net.bytebuddy.utility.OpenedClassReader;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.internal.creation.bytebuddy.inject.MockMethodDispatcher;
import org.mockito.internal.debugging.LocationImpl;
Expand All @@ -34,6 +45,8 @@
import org.mockito.internal.util.concurrent.DetachedThreadLocal;
import org.mockito.internal.util.concurrent.WeakConcurrentMap;

import static net.bytebuddy.matcher.ElementMatchers.*;

public class MockMethodAdvice extends MockMethodDispatcher {

private final WeakConcurrentMap<Object, MockMethodInterceptor> interceptors;
Expand All @@ -46,13 +59,17 @@ public class MockMethodAdvice extends MockMethodDispatcher {
private final WeakConcurrentMap<Class<?>, SoftReference<MethodGraph>> graphs =
new WeakConcurrentMap.WithInlinedExpunction<Class<?>, SoftReference<MethodGraph>>();

private final BooleanSupplier isMockConstruction;

public MockMethodAdvice(
WeakConcurrentMap<Object, MockMethodInterceptor> interceptors,
DetachedThreadLocal<Map<Class<?>, MockMethodInterceptor>> mockedStatics,
String identifier) {
String identifier,
BooleanSupplier isMockConstruction) {
this.interceptors = interceptors;
this.mockedStatics = mockedStatics;
this.identifier = identifier;
this.isMockConstruction = isMockConstruction;
}

@SuppressWarnings("unused")
Expand Down Expand Up @@ -183,6 +200,11 @@ public boolean isOverridden(Object instance, Method origin) {
.represents(origin.getDeclaringClass());
}

@Override
public boolean isConstructorMock(Class<?> type) {
return isMockConstruction.getAsBoolean();
}

private static class RealMethodCall implements RealMethod {

private final SelfCallInfo selfCallInfo;
Expand Down Expand Up @@ -344,6 +366,133 @@ boolean checkSelfCall(Object value) {
}
}

static class ConstructorShortcut
implements AsmVisitorWrapper.ForDeclaredMethods.MethodVisitorWrapper {

private final String identifier;

ConstructorShortcut(String identifier) {
this.identifier = identifier;
}

@Override
public MethodVisitor wrap(
TypeDescription instrumentedType,
MethodDescription instrumentedMethod,
MethodVisitor methodVisitor,
Implementation.Context implementationContext,
TypePool typePool,
int writerFlags,
int readerFlags) {
if (instrumentedMethod.isConstructor() && !instrumentedType.represents(Object.class)) {
MethodList<MethodDescription.InDefinedShape> constructors =
instrumentedType
.getSuperClass()
.asErasure()
.getDeclaredMethods()
.filter(isConstructor().and(not(isPrivate())));
int arguments = Integer.MAX_VALUE;
boolean visible = false;
MethodDescription.InDefinedShape current = null;
for (MethodDescription.InDefinedShape constructor : constructors) {
if (constructor.getParameters().size() < arguments
&& (!visible || constructor.isPackagePrivate())) {
current = constructor;
visible = constructor.isPackagePrivate();
}
}
if (current != null) {
final MethodDescription.InDefinedShape selected = current;
return new MethodVisitor(OpenedClassReader.ASM_API, methodVisitor) {
@Override
public void visitCode() {
super.visitCode();
/*
* The byte code that is added to the start of the method is roughly equivalent to:
*
* if (MockMethodDispatcher.isConstructorMock(<identifier>, Current.class) {
* super(<default arguments>);
* return;
* }
*
* This avoids the invocation of the original constructor chain but fullfils the
* verifier requirement to invoke a super constructor.
*/
Label label = new Label();
super.visitLdcInsn(identifier);
if (implementationContext
.getClassFileVersion()
.isAtLeast(ClassFileVersion.JAVA_V5)) {
super.visitLdcInsn(Type.getType(instrumentedType.getDescriptor()));
} else {
super.visitLdcInsn(instrumentedType.getName());
super.visitMethodInsn(
Opcodes.INVOKESTATIC,
Type.getInternalName(Class.class),
"forName",
Type.getMethodDescriptor(
Type.getType(Class.class),
Type.getType(String.class)),
false);
}
super.visitMethodInsn(
Opcodes.INVOKESTATIC,
Type.getInternalName(MockMethodDispatcher.class),
"isConstructorMock",
Type.getMethodDescriptor(
Type.BOOLEAN_TYPE,
Type.getType(String.class),
Type.getType(Class.class)),
false);
super.visitInsn(Opcodes.ICONST_0);
super.visitJumpInsn(Opcodes.IF_ICMPEQ, label);
super.visitVarInsn(Opcodes.ALOAD, 0);
for (TypeDescription type :
selected.getParameters().asTypeList().asErasures()) {
if (type.represents(boolean.class)
|| type.represents(byte.class)
|| type.represents(short.class)
|| type.represents(char.class)
|| type.represents(int.class)) {
super.visitInsn(Opcodes.ICONST_0);
} else if (type.represents(long.class)) {
super.visitInsn(Opcodes.LCONST_0);
} else if (type.represents(float.class)) {
super.visitInsn(Opcodes.FCONST_0);
} else if (type.represents(double.class)) {
super.visitInsn(Opcodes.DCONST_0);
} else {
super.visitInsn(Opcodes.ACONST_NULL);
}
}
super.visitMethodInsn(
Opcodes.INVOKESPECIAL,
selected.getDeclaringType().getInternalName(),
selected.getInternalName(),
selected.getDescriptor(),
false);
super.visitInsn(Opcodes.RETURN);
super.visitLabel(label);
if (implementationContext
.getClassFileVersion()
.isAtLeast(ClassFileVersion.JAVA_V6)) {
super.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
}
}

@Override
public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(
Math.max(maxStack, Math.max(3, selected.getStackSize())),
maxLocals);
}
};
}
}
return methodVisitor;
}
}

@Retention(RetentionPolicy.RUNTIME)
@interface Identifier {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ public static void set(String identifier, MockMethodDispatcher dispatcher) {
DISPATCHERS.putIfAbsent(identifier, dispatcher);
}

@SuppressWarnings("unused")
public static boolean isConstructorMock(String identifier, Class<?> type) {
return DISPATCHERS.get(identifier).isConstructorMock(type);
}

public abstract Callable<?> handle(Object instance, Method origin, Object[] arguments)
throws Throwable;

Expand All @@ -49,4 +54,6 @@ public abstract Callable<?> handleStatic(Class<?> type, Method origin, Object[]
public abstract boolean isMockedStatic(Class<?> type);

public abstract boolean isOverridden(Object instance, Method origin);

public abstract boolean isConstructorMock(Class<?> type);
}

0 comments on commit ddb2acf

Please sign in to comment.