| /* |
| * 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.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.IllegalClassFormatException; |
| import java.lang.instrument.Instrumentation; |
| import java.lang.instrument.UnmodifiableClassException; |
| 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.to; |
| 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.*; |
| |
| public class InlineBytecodeGenerator implements BytecodeGenerator, ClassFileTransformer { |
| |
| @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 String identifier; |
| |
| private final MockMethodAdvice advice; |
| |
| private final BytecodeGenerator subclassEngine; |
| |
| public InlineBytecodeGenerator(Instrumentation instrumentation, WeakConcurrentMap<Object, MockMethodInterceptor> mocks) { |
| this.instrumentation = instrumentation; |
| byteBuddy = new ByteBuddy() |
| .with(TypeValidation.DISABLED) |
| .with(Implementation.Context.Disabled.Factory.INSTANCE); |
| mocked = new WeakConcurrentSet<Class<?>>(WeakConcurrentSet.Cleaner.INLINE); |
| identifier = RandomString.make(); |
| advice = new MockMethodAdvice(mocks, identifier); |
| subclassEngine = new TypeCachingBytecodeGenerator(new SubclassBytecodeGenerator(withDefaultConfiguration() |
| .withBinders(of(MockMethodAdvice.Identifier.class, identifier)) |
| .to(MockMethodAdvice.ForReadObject.class), isAbstract().or(isNative())), false); |
| MockMethodDispatcher.set(identifier, advice); |
| instrumentation.addTransformer(this, true); |
| } |
| |
| @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()])); |
| } catch (UnmodifiableClassException exception) { |
| for (Class<?> failed : types) { |
| mocked.remove(failed); |
| } |
| throw new MockitoException("Could not modify all classes " + types, exception); |
| } |
| } |
| } |
| |
| 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) throws IllegalClassFormatException { |
| 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(Advice.withCustomMapping() |
| .bind(MockMethodAdvice.Identifier.class, identifier) |
| .to(MockMethodAdvice.class).on(isVirtual() |
| .and(not(isBridge().or(isHashCode()).or(isEquals()).or(isDefaultFinalizer()))) |
| .and(not(isDeclaredBy(nameStartsWith("java.")).<MethodDescription>and(isPackagePrivate()))))) |
| .visit(Advice.withCustomMapping() |
| .bind(MockMethodAdvice.Identifier.class, identifier) |
| .to(MockMethodAdvice.ForHashCode.class).on(isHashCode())) |
| .visit(Advice.withCustomMapping() |
| .bind(MockMethodAdvice.Identifier.class, identifier) |
| .to(MockMethodAdvice.ForEquals.class).on(isEquals())) |
| .make() |
| .getBytes(); |
| } catch (Throwable 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.ASM5, 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. |
| } |
| } |
| } |
| } |