| /* |
| * Copyright (c) 2016 Mockito contributors |
| * This program is made available under the terms of the MIT License. |
| */ |
| package org.mockito.internal.creation.bytebuddy; |
| |
| import net.bytebuddy.ByteBuddy; |
| import net.bytebuddy.ClassFileVersion; |
| import net.bytebuddy.asm.Advice; |
| import net.bytebuddy.asm.AsmVisitorWrapper; |
| import net.bytebuddy.description.field.FieldDescription; |
| import net.bytebuddy.description.field.FieldList; |
| import net.bytebuddy.description.method.MethodDescription; |
| import net.bytebuddy.description.method.MethodList; |
| import net.bytebuddy.description.method.ParameterDescription; |
| import net.bytebuddy.description.type.TypeDescription; |
| import net.bytebuddy.dynamic.ClassFileLocator; |
| import net.bytebuddy.dynamic.scaffold.MethodGraph; |
| import net.bytebuddy.dynamic.scaffold.TypeValidation; |
| import net.bytebuddy.implementation.Implementation; |
| import net.bytebuddy.jar.asm.ClassVisitor; |
| import net.bytebuddy.jar.asm.MethodVisitor; |
| import net.bytebuddy.jar.asm.Opcodes; |
| import net.bytebuddy.matcher.ElementMatchers; |
| import net.bytebuddy.pool.TypePool; |
| import net.bytebuddy.utility.RandomString; |
| import org.mockito.exceptions.base.MockitoException; |
| import org.mockito.internal.util.concurrent.WeakConcurrentMap; |
| import org.mockito.internal.util.concurrent.WeakConcurrentSet; |
| import org.mockito.mock.SerializableMode; |
| |
| import java.lang.instrument.ClassFileTransformer; |
| import java.lang.instrument.Instrumentation; |
| import java.lang.reflect.Modifier; |
| import java.security.ProtectionDomain; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.Set; |
| |
| import static net.bytebuddy.implementation.MethodDelegation.withDefaultConfiguration; |
| import static net.bytebuddy.implementation.bind.annotation.TargetMethodAnnotationDrivenBinder.ParameterBinder.ForFixedValue.OfConstant.of; |
| import static net.bytebuddy.matcher.ElementMatchers.*; |
| import static org.mockito.internal.util.StringUtil.join; |
| |
| public class InlineBytecodeGenerator implements BytecodeGenerator, ClassFileTransformer { |
| |
| private static final String PRELOAD = "org.mockito.inline.preload"; |
| |
| @SuppressWarnings("unchecked") |
| static final Set<Class<?>> EXCLUDES = new HashSet<Class<?>>(Arrays.asList(Class.class, |
| Boolean.class, |
| Byte.class, |
| Short.class, |
| Character.class, |
| Integer.class, |
| Long.class, |
| Float.class, |
| Double.class, |
| String.class)); |
| |
| private final Instrumentation instrumentation; |
| |
| private final ByteBuddy byteBuddy; |
| |
| private final WeakConcurrentSet<Class<?>> mocked; |
| |
| private final BytecodeGenerator subclassEngine; |
| |
| private final AsmVisitorWrapper mockTransformer; |
| |
| private volatile Throwable lastException; |
| |
| public InlineBytecodeGenerator(Instrumentation instrumentation, WeakConcurrentMap<Object, MockMethodInterceptor> mocks) { |
| preload(); |
| this.instrumentation = instrumentation; |
| byteBuddy = new ByteBuddy() |
| .with(TypeValidation.DISABLED) |
| .with(Implementation.Context.Disabled.Factory.INSTANCE) |
| .with(MethodGraph.Compiler.ForDeclaredMethods.INSTANCE); |
| mocked = new WeakConcurrentSet<Class<?>>(WeakConcurrentSet.Cleaner.INLINE); |
| String identifier = RandomString.make(); |
| subclassEngine = new TypeCachingBytecodeGenerator(new SubclassBytecodeGenerator(withDefaultConfiguration() |
| .withBinders(of(MockMethodAdvice.Identifier.class, identifier)) |
| .to(MockMethodAdvice.ForReadObject.class), isAbstract().or(isNative()).or(isToString())), false); |
| mockTransformer = new AsmVisitorWrapper.ForDeclaredMethods() |
| .method(isVirtual() |
| .and(not(isBridge().or(isHashCode()).or(isEquals()).or(isDefaultFinalizer()))) |
| .and(not(isDeclaredBy(nameStartsWith("java.")).<MethodDescription>and(isPackagePrivate()))), |
| Advice.withCustomMapping() |
| .bind(MockMethodAdvice.Identifier.class, identifier) |
| .to(MockMethodAdvice.class)) |
| .method(isHashCode(), |
| Advice.withCustomMapping() |
| .bind(MockMethodAdvice.Identifier.class, identifier) |
| .to(MockMethodAdvice.ForHashCode.class)) |
| .method(isEquals(), |
| Advice.withCustomMapping() |
| .bind(MockMethodAdvice.Identifier.class, identifier) |
| .to(MockMethodAdvice.ForEquals.class)); |
| MockMethodDispatcher.set(identifier, new MockMethodAdvice(mocks, identifier)); |
| instrumentation.addTransformer(this, true); |
| } |
| |
| /** |
| * Mockito allows to mock about any type, including such types that we are relying on ourselves. This can cause a circularity: |
| * In order to check if an instance is a mock we need to look up if this instance is registered in the {@code mocked} set. But to look |
| * up this instance, we need to create key instances that rely on weak reference properties. Loading the later classes will happen before |
| * the key instances are completed what will cause Mockito to check if those key instances are themselves mocks what causes a loop which |
| * results in a circularity error. This is not normally a problem as we explicitly check if the instance that we investigate is one of |
| * our instance of which we hold a reference by reference equality what does not cause any code execution. But it seems like the load |
| * order plays a role here with unloaded types being loaded before we even get to check the mock instance property. To avoid this, we are |
| * making sure that crucuial JVM types are loaded before we create the first inline mock. Unfortunately, these types dependant on a JVM's |
| * implementation and we can only maintain types that we know of from well-known JVM implementations such as HotSpot and extend this list |
| * once we learn of further problematic types for future Java versions. To allow users to whitelist their own types, we do not also offer |
| * a property that allows running problematic tests before a new Mockito version can be released and that allows us to ask users to |
| * easily validate that whitelisting actually solves a problem as circularities could also be caused by other problems. |
| */ |
| private static void preload() { |
| String preloads = System.getProperty(PRELOAD); |
| if (preloads == null) { |
| preloads = "java.lang.WeakPairMap,java.lang.WeakPairMap$Pair,java.lang.WeakPairMap$Pair$Weak"; |
| } |
| for (String preload : preloads.split(",")) { |
| try { |
| Class.forName(preload, false, null); |
| } catch (ClassNotFoundException ignored) { |
| } |
| } |
| } |
| |
| @Override |
| public <T> Class<? extends T> mockClass(MockFeatures<T> features) { |
| boolean subclassingRequired = !features.interfaces.isEmpty() |
| || features.serializableMode != SerializableMode.NONE |
| || Modifier.isAbstract(features.mockedType.getModifiers()); |
| |
| checkSupportedCombination(subclassingRequired, features); |
| |
| synchronized (this) { |
| triggerRetransformation(features); |
| } |
| |
| return subclassingRequired ? |
| subclassEngine.mockClass(features) : |
| features.mockedType; |
| } |
| |
| private <T> void triggerRetransformation(MockFeatures<T> features) { |
| Set<Class<?>> types = new HashSet<Class<?>>(); |
| Class<?> type = features.mockedType; |
| do { |
| if (mocked.add(type)) { |
| types.add(type); |
| addInterfaces(types, type.getInterfaces()); |
| } |
| type = type.getSuperclass(); |
| } while (type != null); |
| if (!types.isEmpty()) { |
| try { |
| instrumentation.retransformClasses(types.toArray(new Class<?>[types.size()])); |
| Throwable throwable = lastException; |
| if (throwable != null) { |
| throw new IllegalStateException(join("Byte Buddy could not instrument all classes within the mock's type hierarchy", |
| "", |
| "This problem should never occur for javac-compiled classes. This problem has been observed for classes that are:", |
| " - Compiled by older versions of scalac", |
| " - Classes that are part of the Android distribution"), throwable); |
| } |
| } catch (Exception exception) { |
| for (Class<?> failed : types) { |
| mocked.remove(failed); |
| } |
| throw new MockitoException("Could not modify all classes " + types, exception); |
| } finally { |
| lastException = null; |
| } |
| } |
| } |
| |
| private <T> void checkSupportedCombination(boolean subclassingRequired, MockFeatures<T> features) { |
| if (subclassingRequired |
| && !features.mockedType.isArray() |
| && !features.mockedType.isPrimitive() |
| && Modifier.isFinal(features.mockedType.getModifiers())) { |
| throw new MockitoException("Unsupported settings with this type '" + features.mockedType.getName() + "'"); |
| } |
| } |
| |
| private void addInterfaces(Set<Class<?>> types, Class<?>[] interfaces) { |
| for (Class<?> type : interfaces) { |
| if (mocked.add(type)) { |
| types.add(type); |
| addInterfaces(types, type.getInterfaces()); |
| } |
| } |
| } |
| |
| @Override |
| public byte[] transform(ClassLoader loader, |
| String className, |
| Class<?> classBeingRedefined, |
| ProtectionDomain protectionDomain, |
| byte[] classfileBuffer) { |
| if (classBeingRedefined == null |
| || !mocked.contains(classBeingRedefined) |
| || EXCLUDES.contains(classBeingRedefined)) { |
| return null; |
| } else { |
| try { |
| return byteBuddy.redefine(classBeingRedefined, ClassFileLocator.Simple.of(classBeingRedefined.getName(), classfileBuffer)) |
| // Note: The VM erases parameter meta data from the provided class file (bug). We just add this information manually. |
| .visit(new ParameterWritingVisitorWrapper(classBeingRedefined)) |
| .visit(mockTransformer) |
| .make() |
| .getBytes(); |
| } catch (Throwable throwable) { |
| lastException = throwable; |
| return null; |
| } |
| } |
| } |
| |
| private static class ParameterWritingVisitorWrapper extends AsmVisitorWrapper.AbstractBase { |
| |
| private final Class<?> type; |
| |
| private ParameterWritingVisitorWrapper(Class<?> type) { |
| this.type = type; |
| } |
| |
| @Override |
| public ClassVisitor wrap(TypeDescription instrumentedType, |
| ClassVisitor classVisitor, |
| Implementation.Context implementationContext, |
| TypePool typePool, |
| FieldList<FieldDescription.InDefinedShape> fields, |
| MethodList<?> methods, |
| int writerFlags, |
| int readerFlags) { |
| return implementationContext.getClassFileVersion().isAtLeast(ClassFileVersion.JAVA_V8) |
| ? new ParameterAddingClassVisitor(classVisitor, new TypeDescription.ForLoadedType(type)) |
| : classVisitor; |
| } |
| |
| private static class ParameterAddingClassVisitor extends ClassVisitor { |
| |
| private final TypeDescription typeDescription; |
| |
| private ParameterAddingClassVisitor(ClassVisitor cv, TypeDescription typeDescription) { |
| super(Opcodes.ASM6, cv); |
| this.typeDescription = typeDescription; |
| } |
| |
| @Override |
| public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { |
| MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions); |
| MethodList<?> methodList = typeDescription.getDeclaredMethods().filter((name.equals(MethodDescription.CONSTRUCTOR_INTERNAL_NAME) |
| ? isConstructor() |
| : ElementMatchers.<MethodDescription>named(name)).and(hasDescriptor(desc))); |
| if (methodList.size() == 1 && methodList.getOnly().getParameters().hasExplicitMetaData()) { |
| for (ParameterDescription parameterDescription : methodList.getOnly().getParameters()) { |
| methodVisitor.visitParameter(parameterDescription.getName(), parameterDescription.getModifiers()); |
| } |
| return new MethodParameterStrippingMethodVisitor(methodVisitor); |
| } else { |
| return methodVisitor; |
| } |
| } |
| } |
| |
| private static class MethodParameterStrippingMethodVisitor extends MethodVisitor { |
| |
| public MethodParameterStrippingMethodVisitor(MethodVisitor mv) { |
| super(Opcodes.ASM5, mv); |
| } |
| |
| @Override |
| public void visitParameter(String name, int access) { |
| // suppress to avoid additional writing of the parameter if retained. |
| } |
| } |
| } |
| } |