From 1a9cbfe3586b4900a14e17982726a726897ed569 Mon Sep 17 00:00:00 2001 From: Rafael Winterhalter Date: Thu, 3 Mar 2022 19:50:55 +0100 Subject: [PATCH] Support subclass mocks on Graal VM. --- gradle/dependencies.gradle | 2 +- .../creation/bytebuddy/MockMethodAdvice.java | 5 +- .../creation/bytebuddy/ModuleHandler.java | 44 ++++++----- .../bytebuddy/SubclassBytecodeGenerator.java | 77 +++++++++++++++---- .../bytebuddy/SubclassInjectionLoader.java | 22 +++++- .../org/mockito/internal/util/Platform.java | 6 +- .../NoByteCodeDependenciesTest.java | 2 - 7 files changed, 118 insertions(+), 40 deletions(-) diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 0b15d0ec00..8348a23506 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -4,7 +4,7 @@ ext { def versions = [:] -versions.bytebuddy = '1.12.8' +versions.bytebuddy = '1.12.9' versions.junitJupiter = '5.8.2' versions.errorprone = '2.10.0' diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java b/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java index 580f22b309..fc92e49acf 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java @@ -33,6 +33,7 @@ import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.method.MethodList; import net.bytebuddy.description.method.ParameterDescription; +import net.bytebuddy.description.type.TypeDefinition; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.scaffold.MethodGraph; import net.bytebuddy.implementation.Implementation; @@ -188,7 +189,9 @@ public boolean isOverridden(Object instance, Method origin) { SoftReference reference = graphs.get(instance.getClass()); MethodGraph methodGraph = reference == null ? null : reference.get(); if (methodGraph == null) { - methodGraph = compiler.compile(new TypeDescription.ForLoadedType(instance.getClass())); + methodGraph = + compiler.compile( + (TypeDefinition) TypeDescription.ForLoadedType.of(instance.getClass())); graphs.put(instance.getClass(), new SoftReference<>(methodGraph)); } MethodGraph.Node node = diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/ModuleHandler.java b/src/main/java/org/mockito/internal/creation/bytebuddy/ModuleHandler.java index 38716d0776..6a716291ec 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/ModuleHandler.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/ModuleHandler.java @@ -4,13 +4,6 @@ */ package org.mockito.internal.creation.bytebuddy; -import static net.bytebuddy.matcher.ElementMatchers.isTypeInitializer; -import static org.mockito.internal.util.StringUtil.join; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.Random; - import net.bytebuddy.ByteBuddy; import net.bytebuddy.description.modifier.Ownership; import net.bytebuddy.description.modifier.Visibility; @@ -18,9 +11,18 @@ import net.bytebuddy.implementation.Implementation; import net.bytebuddy.implementation.MethodCall; import net.bytebuddy.implementation.StubMethod; +import net.bytebuddy.utility.GraalImageCode; +import net.bytebuddy.utility.RandomString; +import org.mockito.Mockito; import org.mockito.codegen.InjectionBase; import org.mockito.exceptions.base.MockitoException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import static net.bytebuddy.matcher.ElementMatchers.isTypeInitializer; +import static org.mockito.internal.util.StringUtil.join; + abstract class ModuleHandler { abstract boolean isOpened(Class source, Class target); @@ -35,9 +37,9 @@ abstract class ModuleHandler { abstract void adjustModuleGraph(Class source, Class target, boolean export, boolean read); - static ModuleHandler make(ByteBuddy byteBuddy, SubclassLoader loader, Random random) { + static ModuleHandler make(ByteBuddy byteBuddy, SubclassLoader loader) { try { - return new ModuleSystemFound(byteBuddy, loader, random); + return new ModuleSystemFound(byteBuddy, loader); } catch (Exception ignored) { return new NoModuleSystemFound(); } @@ -47,7 +49,6 @@ private static class ModuleSystemFound extends ModuleHandler { private final ByteBuddy byteBuddy; private final SubclassLoader loader; - private final Random random; private final int injectonBaseSuffix; @@ -58,15 +59,15 @@ private static class ModuleSystemFound extends ModuleHandler { canRead, addExports, addReads, - addOpens, forName; - private ModuleSystemFound(ByteBuddy byteBuddy, SubclassLoader loader, Random random) - throws Exception { + private ModuleSystemFound(ByteBuddy byteBuddy, SubclassLoader loader) throws Exception { this.byteBuddy = byteBuddy; this.loader = loader; - this.random = random; - injectonBaseSuffix = Math.abs(random.nextInt()); + injectonBaseSuffix = + GraalImageCode.getCurrent().isDefined() + ? 0 + : Math.abs(Mockito.class.hashCode()); Class moduleType = Class.forName("java.lang.Module"); getModule = Class.class.getMethod("getModule"); isOpen = moduleType.getMethod("isOpen", String.class, moduleType); @@ -75,7 +76,6 @@ private ModuleSystemFound(ByteBuddy byteBuddy, SubclassLoader loader, Random ran canRead = moduleType.getMethod("canRead", moduleType); addExports = moduleType.getMethod("addExports", String.class, moduleType); addReads = moduleType.getMethod("addReads", moduleType); - addOpens = moduleType.getMethod("addOpens", String.class, moduleType); forName = Class.class.getMethod("forName", String.class); } @@ -207,9 +207,12 @@ void adjustModuleGraph(Class source, Class target, boolean export, boolean ConstructorStrategy.Default.NO_CONSTRUCTORS) .name( String.format( - "%s$%d", + "%s$%s%s", "org.mockito.codegen.MockitoTypeCarrier", - Math.abs(random.nextInt()))) + RandomString.hashOf( + source.getName().hashCode()), + RandomString.hashOf( + target.getName().hashCode()))) .defineField( "mockitoType", Class.class, @@ -262,10 +265,11 @@ void adjustModuleGraph(Class source, Class target, boolean export, boolean .subclass(Object.class) .name( String.format( - "%s$%s$%d", + "%s$%s$%s%s", source.getName(), "MockitoModuleProbe", - Math.abs(random.nextInt()))) + RandomString.hashOf(source.getName().hashCode()), + RandomString.hashOf(target.getName().hashCode()))) .invokable(isTypeInitializer()) .intercept(implementation) .make() diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java b/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java index f67fdfc5a6..15e332f447 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java @@ -24,15 +24,13 @@ import java.io.IOException; import java.io.ObjectInputStream; +import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.Random; +import java.util.*; + import net.bytebuddy.ByteBuddy; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.modifier.SynchronizationState; @@ -44,6 +42,8 @@ import net.bytebuddy.implementation.Implementation; import net.bytebuddy.implementation.attribute.MethodAttributeAppender; import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.utility.GraalImageCode; +import net.bytebuddy.utility.RandomString; import org.mockito.codegen.InjectionBase; import org.mockito.exceptions.base.MockitoException; import org.mockito.internal.creation.bytebuddy.ByteBuddyCrossClassLoaderSerializationSupport.CrossClassLoaderSerializableMock; @@ -57,7 +57,6 @@ class SubclassBytecodeGenerator implements BytecodeGenerator { private final SubclassLoader loader; private final ModuleHandler handler; private final ByteBuddy byteBuddy; - private final Random random; private final Implementation readReplace; private final ElementMatcher matcher; @@ -87,8 +86,7 @@ protected SubclassBytecodeGenerator( this.readReplace = readReplace; this.matcher = matcher; byteBuddy = new ByteBuddy().with(TypeValidation.DISABLED); - random = new Random(); - handler = ModuleHandler.make(byteBuddy, loader, random); + handler = ModuleHandler.make(byteBuddy, loader); } private static boolean needsSamePackageClassLoader(MockFeatures features) { @@ -172,7 +170,8 @@ public Class mockClass(MockFeatures features) { && features.serializableMode != SerializableMode.ACROSS_CLASSLOADERS && !isComingFromJDK(features.mockedType) && (loader.isDisrespectingOpenness() - || handler.isOpened(features.mockedType, MockAccess.class)); + || handler.isOpened(features.mockedType, MockAccess.class)) + && !GraalImageCode.getCurrent().isDefined(); String typeName; if (localMock || (loader instanceof MultipleParentClassLoader @@ -185,7 +184,13 @@ public Class mockClass(MockFeatures features) { + features.mockedType.getSimpleName(); } String name = - String.format("%s$%s$%d", typeName, "MockitoMock", Math.abs(random.nextInt())); + String.format( + "%s$%s$%s", + typeName, + "MockitoMock", + GraalImageCode.getCurrent().isDefined() + ? suffix(features) + : RandomString.make()); if (localMock) { handler.adjustModuleGraph(features.mockedType, MockAccess.class, false, true); @@ -219,17 +224,36 @@ public Class mockClass(MockFeatures features) { } } } - + // Graal requires that the byte code of classes is identical what requires that interfaces + // are always + // defined in the exact same order. Therefore, we add an interface to the interface set if + // not mocking + // a class when Graal is active. + @SuppressWarnings("unchecked") + Class target = + GraalImageCode.getCurrent().isDefined() && features.mockedType.isInterface() + ? (Class) Object.class + : features.mockedType; DynamicType.Builder builder = byteBuddy - .subclass(features.mockedType) + .subclass(target) .name(name) .ignoreAlso(isGroovyMethod()) .annotateType( - features.stripAnnotations + features.stripAnnotations || features.mockedType.isInterface() ? new Annotation[0] : features.mockedType.getAnnotations()) - .implement(new ArrayList(features.interfaces)) + .implement( + new ArrayList<>( + GraalImageCode.getCurrent().isDefined() + ? sortedSerializable( + features.interfaces, + GraalImageCode.getCurrent().isDefined() + && features.mockedType + .isInterface() + ? features.mockedType + : void.class) + : features.interfaces)) .method(matcher) .intercept(dispatcher) .transform(withModifiers(SynchronizationState.PLAIN)) @@ -271,6 +295,31 @@ public Class mockClass(MockFeatures features) { .getLoaded(); } + private static CharSequence suffix(MockFeatures features) { + // Constructs a deterministic suffix for this mock to assure that mocks always carry the + // same name. + StringBuilder sb = new StringBuilder(); + Set names = new TreeSet<>(); + names.add(features.mockedType.getName()); + for (Class type : features.interfaces) { + names.add(type.getName()); + } + return sb.append(RandomString.hashOf(names.hashCode())) + .append(RandomString.hashOf(features.serializableMode.name().hashCode())) + .append(features.stripAnnotations ? "S" : "N"); + } + + private static Collection sortedSerializable( + Collection> interfaces, Class mockedType) { + SortedSet> types = new TreeSet<>(Comparator.comparing(Class::getName)); + types.addAll(interfaces); + if (mockedType != void.class) { + types.add(mockedType); + } + types.add(Serializable.class); + return types; + } + @Override public void mockClassStatic(Class type) { throw new MockitoException("The subclass byte code generator cannot create static mocks"); diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassInjectionLoader.java b/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassInjectionLoader.java index c70de01355..b5013891d7 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassInjectionLoader.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassInjectionLoader.java @@ -11,6 +11,7 @@ import net.bytebuddy.dynamic.loading.ClassInjector; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.utility.GraalImageCode; import org.mockito.codegen.InjectionBase; import org.mockito.exceptions.base.MockitoException; import org.mockito.internal.util.Platform; @@ -27,9 +28,14 @@ class SubclassInjectionLoader implements SubclassLoader { private final SubclassLoader loader; SubclassInjectionLoader() { - if (!Boolean.getBoolean("org.mockito.internal.noUnsafeInjection") + if (!Boolean.parseBoolean( + System.getProperty( + "org.mockito.internal.noUnsafeInjection", + Boolean.toString(GraalImageCode.getCurrent().isDefined()))) && ClassInjector.UsingReflection.isAvailable()) { this.loader = new WithReflection(); + } else if (GraalImageCode.getCurrent().isDefined()) { + this.loader = new WithIsolatedLoader(); } else if (ClassInjector.UsingLookup.isAvailable()) { this.loader = tryLookup(); } else { @@ -70,6 +76,20 @@ public ClassLoadingStrategy resolveStrategy( } } + private static class WithIsolatedLoader implements SubclassLoader { + + @Override + public boolean isDisrespectingOpenness() { + return false; + } + + @Override + public ClassLoadingStrategy resolveStrategy( + Class mockedType, ClassLoader classLoader, boolean localMock) { + return ClassLoadingStrategy.Default.WRAPPER; + } + } + private static class WithLookup implements SubclassLoader { private final Object lookup; diff --git a/src/main/java/org/mockito/internal/util/Platform.java b/src/main/java/org/mockito/internal/util/Platform.java index 946e4ff3be..fde59fbbe9 100644 --- a/src/main/java/org/mockito/internal/util/Platform.java +++ b/src/main/java/org/mockito/internal/util/Platform.java @@ -69,7 +69,11 @@ public static String describe() { } public static boolean isJava8BelowUpdate45() { - return isJava8BelowUpdate45(JVM_VERSION); + if (JVM_VERSION == null) { + return false; + } else { + return isJava8BelowUpdate45(JVM_VERSION); + } } static boolean isJava8BelowUpdate45(String jvmVersion) { diff --git a/src/test/java/org/mockitointegration/NoByteCodeDependenciesTest.java b/src/test/java/org/mockitointegration/NoByteCodeDependenciesTest.java index 2363fe6f35..3a2908dc1c 100644 --- a/src/test/java/org/mockitointegration/NoByteCodeDependenciesTest.java +++ b/src/test/java/org/mockitointegration/NoByteCodeDependenciesTest.java @@ -15,8 +15,6 @@ public class NoByteCodeDependenciesTest { - private ClassLoader contextClassLoader; - @Test public void pure_mockito_should_not_depend_bytecode_libraries() throws Exception {