diff --git a/src/main/java/org/mockito/internal/configuration/IndependentAnnotationEngine.java b/src/main/java/org/mockito/internal/configuration/IndependentAnnotationEngine.java index 35e212b5d9..43a35eee6e 100644 --- a/src/main/java/org/mockito/internal/configuration/IndependentAnnotationEngine.java +++ b/src/main/java/org/mockito/internal/configuration/IndependentAnnotationEngine.java @@ -5,7 +5,6 @@ package org.mockito.internal.configuration; import static org.mockito.internal.exceptions.Reporter.moreThanOneAnnotationNotAllowed; -import static org.mockito.internal.util.reflection.FieldSetter.setField; import java.lang.annotation.Annotation; import java.lang.reflect.Field; @@ -19,7 +18,9 @@ import org.mockito.MockitoAnnotations; import org.mockito.ScopedMock; import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.plugins.AnnotationEngine; +import org.mockito.plugins.MemberAccessor; /** * Initializes fields annotated with @{@link org.mockito.Mock} or @{@link org.mockito.Captor}. @@ -76,8 +77,9 @@ public AutoCloseable process(Class clazz, Object testInstance) { if (mock != null) { throwIfAlreadyAssigned(field, alreadyAssigned); alreadyAssigned = true; + final MemberAccessor accessor = Plugins.getMemberAccessor(); try { - setField(testInstance, field, mock); + accessor.set(field, testInstance, mock); } catch (Exception e) { for (ScopedMock scopedMock : scopedMocks) { scopedMock.close(); diff --git a/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java b/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java index b4686b30d8..b7a4a2cef9 100644 --- a/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java +++ b/src/main/java/org/mockito/internal/configuration/SpyAnnotationEngine.java @@ -22,8 +22,10 @@ import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.util.MockUtil; import org.mockito.plugins.AnnotationEngine; +import org.mockito.plugins.MemberAccessor; /** * Process fields annotated with @Spy. @@ -50,23 +52,23 @@ public class SpyAnnotationEngine @Override public AutoCloseable process(Class context, Object testInstance) { Field[] fields = context.getDeclaredFields(); + MemberAccessor accessor = Plugins.getMemberAccessor(); for (Field field : fields) { if (field.isAnnotationPresent(Spy.class) && !field.isAnnotationPresent(InjectMocks.class)) { assertNoIncompatibleAnnotations(Spy.class, field, Mock.class, Captor.class); - field.setAccessible(true); Object instance; try { - instance = field.get(testInstance); + instance = accessor.get(field, testInstance); if (MockUtil.isMock(instance)) { // instance has been spied earlier // for example happens when MockitoAnnotations.openMocks is called two // times. Mockito.reset(instance); } else if (instance != null) { - field.set(testInstance, spyInstance(field, instance)); + accessor.set(field, testInstance, spyInstance(field, instance)); } else { - field.set(testInstance, spyNewInstance(testInstance, field)); + accessor.set(field, testInstance, spyNewInstance(testInstance, field)); } } catch (Exception e) { throw new MockitoException( @@ -123,8 +125,8 @@ private static Object spyNewInstance(Object testInstance, Field field) Constructor constructor = noArgConstructorOf(type); if (Modifier.isPrivate(constructor.getModifiers())) { - constructor.setAccessible(true); - return Mockito.mock(type, settings.spiedInstance(constructor.newInstance())); + MemberAccessor accessor = Plugins.getMemberAccessor(); + return Mockito.mock(type, settings.spiedInstance(accessor.newInstance(constructor))); } else { return Mockito.mock(type, settings.useConstructor()); } diff --git a/src/main/java/org/mockito/internal/configuration/injection/SpyOnInjectedFieldsHandler.java b/src/main/java/org/mockito/internal/configuration/injection/SpyOnInjectedFieldsHandler.java index cd8fcc5e3a..6230e1b86f 100644 --- a/src/main/java/org/mockito/internal/configuration/injection/SpyOnInjectedFieldsHandler.java +++ b/src/main/java/org/mockito/internal/configuration/injection/SpyOnInjectedFieldsHandler.java @@ -5,7 +5,6 @@ package org.mockito.internal.configuration.injection; import static org.mockito.Mockito.withSettings; -import static org.mockito.internal.util.reflection.FieldSetter.setField; import java.lang.reflect.Field; import java.util.Set; @@ -13,8 +12,10 @@ import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.util.MockUtil; import org.mockito.internal.util.reflection.FieldReader; +import org.mockito.plugins.MemberAccessor; /** * Handler for field annotated with @InjectMocks and @Spy. @@ -26,6 +27,8 @@ */ public class SpyOnInjectedFieldsHandler extends MockInjectionStrategy { + private final MemberAccessor accessor = Plugins.getMemberAccessor(); + @Override protected boolean processInjection(Field field, Object fieldOwner, Set mockCandidates) { FieldReader fieldReader = new FieldReader(fieldOwner, field); @@ -46,7 +49,7 @@ protected boolean processInjection(Field field, Object fieldOwner, Set m .spiedInstance(instance) .defaultAnswer(Mockito.CALLS_REAL_METHODS) .name(field.getName())); - setField(fieldOwner, field, mock); + accessor.set(field, fieldOwner, mock); } } catch (Exception e) { throw new MockitoException("Problems initiating spied field " + field.getName(), e); diff --git a/src/main/java/org/mockito/internal/configuration/injection/filter/TerminalMockCandidateFilter.java b/src/main/java/org/mockito/internal/configuration/injection/filter/TerminalMockCandidateFilter.java index a682d92320..6c3c329b67 100644 --- a/src/main/java/org/mockito/internal/configuration/injection/filter/TerminalMockCandidateFilter.java +++ b/src/main/java/org/mockito/internal/configuration/injection/filter/TerminalMockCandidateFilter.java @@ -5,13 +5,14 @@ package org.mockito.internal.configuration.injection.filter; import static org.mockito.internal.exceptions.Reporter.cannotInjectDependency; -import static org.mockito.internal.util.reflection.FieldSetter.setField; import java.lang.reflect.Field; import java.util.Collection; import java.util.List; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.util.reflection.BeanPropertySetter; +import org.mockito.plugins.MemberAccessor; /** * This node returns an actual injecter which will be either : @@ -30,18 +31,17 @@ public OngoingInjector filterCandidate( if (mocks.size() == 1) { final Object matchingMock = mocks.iterator().next(); - return new OngoingInjector() { - public Object thenInject() { - try { - if (!new BeanPropertySetter(injectee, candidateFieldToBeInjected) - .set(matchingMock)) { - setField(injectee, candidateFieldToBeInjected, matchingMock); - } - } catch (RuntimeException e) { - throw cannotInjectDependency(candidateFieldToBeInjected, matchingMock, e); + MemberAccessor accessor = Plugins.getMemberAccessor(); + return () -> { + try { + if (!new BeanPropertySetter(injectee, candidateFieldToBeInjected) + .set(matchingMock)) { + accessor.set(candidateFieldToBeInjected, injectee, matchingMock); } - return matchingMock; + } catch (RuntimeException | IllegalAccessException e) { + throw cannotInjectDependency(candidateFieldToBeInjected, matchingMock, e); } + return matchingMock; }; } diff --git a/src/main/java/org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java b/src/main/java/org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java index 46bf3a7c0e..f3192cba5a 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java @@ -8,19 +8,13 @@ import java.util.Map; import org.mockito.internal.creation.instance.InstantiatorProvider2Adapter; -import org.mockito.plugins.AnnotationEngine; -import org.mockito.plugins.InstantiatorProvider; -import org.mockito.plugins.InstantiatorProvider2; -import org.mockito.plugins.MockMaker; -import org.mockito.plugins.MockitoLogger; -import org.mockito.plugins.MockitoPlugins; -import org.mockito.plugins.PluginSwitch; -import org.mockito.plugins.StackTraceCleanerProvider; +import org.mockito.plugins.*; class DefaultMockitoPlugins implements MockitoPlugins { private static final Map DEFAULT_PLUGINS = new HashMap(); static final String INLINE_ALIAS = "mock-maker-inline"; + static final String MODULE_ALIAS = "member-accessor-module"; static { // Keep the mapping: plugin interface name -> plugin implementation class name @@ -41,6 +35,11 @@ class DefaultMockitoPlugins implements MockitoPlugins { INLINE_ALIAS, "org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker"); DEFAULT_PLUGINS.put( MockitoLogger.class.getName(), "org.mockito.internal.util.ConsoleMockitoLogger"); + DEFAULT_PLUGINS.put( + MemberAccessor.class.getName(), + "org.mockito.internal.util.reflection.ReflectionMemberAccessor"); + DEFAULT_PLUGINS.put( + MODULE_ALIAS, "org.mockito.internal.util.reflection.ModuleMemberAccessor"); } @Override diff --git a/src/main/java/org/mockito/internal/configuration/plugins/PluginRegistry.java b/src/main/java/org/mockito/internal/configuration/plugins/PluginRegistry.java index 0419001285..ba7aae1728 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/PluginRegistry.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/PluginRegistry.java @@ -5,13 +5,7 @@ package org.mockito.internal.configuration.plugins; import org.mockito.internal.creation.instance.InstantiatorProviderAdapter; -import org.mockito.plugins.AnnotationEngine; -import org.mockito.plugins.InstantiatorProvider; -import org.mockito.plugins.InstantiatorProvider2; -import org.mockito.plugins.MockMaker; -import org.mockito.plugins.MockitoLogger; -import org.mockito.plugins.PluginSwitch; -import org.mockito.plugins.StackTraceCleanerProvider; +import org.mockito.plugins.*; class PluginRegistry { @@ -22,6 +16,10 @@ class PluginRegistry { new PluginLoader(pluginSwitch, DefaultMockitoPlugins.INLINE_ALIAS) .loadPlugin(MockMaker.class); + private final MemberAccessor memberAccessor = + new PluginLoader(pluginSwitch, DefaultMockitoPlugins.MODULE_ALIAS) + .loadPlugin(MemberAccessor.class); + private final StackTraceCleanerProvider stackTraceCleanerProvider = new PluginLoader(pluginSwitch).loadPlugin(StackTraceCleanerProvider.class); @@ -62,6 +60,16 @@ MockMaker getMockMaker() { return mockMaker; } + /** + * Returns the implementation of the member accessor available for the current runtime. + * + *

Returns {@link org.mockito.internal.util.reflection.ReflectionMemberAccessor} if no + * {@link org.mockito.plugins.MockMaker} extension exists or is visible in the current classpath.

+ */ + MemberAccessor getMemberAccessor() { + return memberAccessor; + } + /** * Returns the instantiator provider available for the current runtime. * diff --git a/src/main/java/org/mockito/internal/configuration/plugins/Plugins.java b/src/main/java/org/mockito/internal/configuration/plugins/Plugins.java index 8469981202..603a03008a 100644 --- a/src/main/java/org/mockito/internal/configuration/plugins/Plugins.java +++ b/src/main/java/org/mockito/internal/configuration/plugins/Plugins.java @@ -4,12 +4,7 @@ */ package org.mockito.internal.configuration.plugins; -import org.mockito.plugins.AnnotationEngine; -import org.mockito.plugins.InstantiatorProvider2; -import org.mockito.plugins.MockMaker; -import org.mockito.plugins.MockitoLogger; -import org.mockito.plugins.MockitoPlugins; -import org.mockito.plugins.StackTraceCleanerProvider; +import org.mockito.plugins.*; /** * Access to Mockito behavior that can be reconfigured by plugins @@ -35,6 +30,16 @@ public static MockMaker getMockMaker() { return registry.getMockMaker(); } + /** + * Returns the implementation of the member accessor available for the current runtime. + * + *

Returns default member accessor if no + * {@link org.mockito.plugins.MemberAccessor} extension exists or is visible in the current classpath.

+ */ + public static MemberAccessor getMemberAccessor() { + return registry.getMemberAccessor(); + } + /** * Returns the instantiator provider available for the current runtime. * diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/ByteBuddyCrossClassLoaderSerializationSupport.java b/src/main/java/org/mockito/internal/creation/bytebuddy/ByteBuddyCrossClassLoaderSerializationSupport.java index 4bb4405e73..90ccee882c 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/ByteBuddyCrossClassLoaderSerializationSupport.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/ByteBuddyCrossClassLoaderSerializationSupport.java @@ -6,7 +6,6 @@ import static org.mockito.internal.creation.bytebuddy.MockMethodInterceptor.ForWriteReplace; import static org.mockito.internal.util.StringUtil.join; -import static org.mockito.internal.util.reflection.FieldSetter.setField; import java.io.*; import java.lang.reflect.Field; @@ -22,6 +21,7 @@ import org.mockito.mock.MockCreationSettings; import org.mockito.mock.MockName; import org.mockito.mock.SerializableMode; +import org.mockito.plugins.MemberAccessor; /** * This is responsible for serializing a mock, it is enabled if the mock is implementing {@link Serializable}. @@ -318,8 +318,14 @@ protected Class resolveClass(ObjectStreamClass desc) private void hackClassNameToMatchNewlyCreatedClass( ObjectStreamClass descInstance, Class proxyClass) throws ObjectStreamException { try { + MemberAccessor accessor = Plugins.getMemberAccessor(); Field classNameField = descInstance.getClass().getDeclaredField("name"); - setField(descInstance, classNameField, proxyClass.getCanonicalName()); + try { + accessor.set(classNameField, descInstance, proxyClass.getCanonicalName()); + } catch (IllegalAccessException e) { + throw new MockitoSerializationIssue( + "Access to " + classNameField + " was denied", e); + } } catch (NoSuchFieldException nsfe) { throw new MockitoSerializationIssue( join( 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 33c58a6340..3b1490775b 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodAdvice.java @@ -40,6 +40,7 @@ import net.bytebuddy.pool.TypePool; import net.bytebuddy.utility.OpenedClassReader; import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.creation.bytebuddy.inject.MockMethodDispatcher; import org.mockito.internal.debugging.LocationImpl; import org.mockito.internal.exceptions.stacktrace.ConditionalStackTraceFilter; @@ -49,6 +50,7 @@ import org.mockito.internal.invocation.mockref.MockWeakReference; import org.mockito.internal.util.concurrent.DetachedThreadLocal; import org.mockito.internal.util.concurrent.WeakConcurrentMap; +import org.mockito.plugins.MemberAccessor; import static net.bytebuddy.matcher.ElementMatchers.*; @@ -244,10 +246,6 @@ public boolean isInvokable() { @Override public Object invoke() throws Throwable { - if (!Modifier.isPublic( - origin.getDeclaringClass().getModifiers() & origin.getModifiers())) { - origin.setAccessible(true); - } selfCallInfo.set(instanceRef.get()); return tryInvoke(origin, instanceRef.get(), arguments); } @@ -279,10 +277,6 @@ public boolean isInvokable() { @Override public Object invoke() throws Throwable { Method method = origin.getJavaMethod(); - if (!Modifier.isPublic( - method.getDeclaringClass().getModifiers() & method.getModifiers())) { - method.setAccessible(true); - } MockMethodDispatcher mockMethodDispatcher = MockMethodDispatcher.get(identifier, instanceRef.get()); if (!(mockMethodDispatcher instanceof MockMethodAdvice)) { @@ -324,9 +318,6 @@ public boolean isInvokable() { @Override public Object invoke() throws Throwable { - if (!Modifier.isPublic(type.getModifiers() & origin.getModifiers())) { - origin.setAccessible(true); - } selfCallInfo.set(type); return tryInvoke(origin, null, arguments); } @@ -334,8 +325,9 @@ public Object invoke() throws Throwable { private static Object tryInvoke(Method origin, Object instance, Object[] arguments) throws Throwable { + MemberAccessor accessor = Plugins.getMemberAccessor(); try { - return origin.invoke(instance, arguments); + return accessor.invoke(origin, instance, arguments); } catch (InvocationTargetException exception) { Throwable cause = exception.getCause(); new ConditionalStackTraceFilter() diff --git a/src/main/java/org/mockito/internal/creation/instance/ConstructorInstantiator.java b/src/main/java/org/mockito/internal/creation/instance/ConstructorInstantiator.java index 94e12fa86a..13f11d7419 100644 --- a/src/main/java/org/mockito/internal/creation/instance/ConstructorInstantiator.java +++ b/src/main/java/org/mockito/internal/creation/instance/ConstructorInstantiator.java @@ -14,8 +14,9 @@ import org.mockito.creation.instance.InstantiationException; import org.mockito.creation.instance.Instantiator; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.util.Primitives; -import org.mockito.internal.util.reflection.AccessibilityChanger; +import org.mockito.plugins.MemberAccessor; public class ConstructorInstantiator implements Instantiator { @@ -64,9 +65,8 @@ private T withParams(Class cls, Object... params) { private static T invokeConstructor(Constructor constructor, Object... params) throws java.lang.InstantiationException, IllegalAccessException, InvocationTargetException { - AccessibilityChanger accessibility = new AccessibilityChanger(); - accessibility.enableAccess(constructor); - return (T) constructor.newInstance(params); + MemberAccessor accessor = Plugins.getMemberAccessor(); + return (T) accessor.newInstance(constructor, params); } private InstantiationException paramsException(Class cls, Exception e) { diff --git a/src/main/java/org/mockito/internal/junit/util/JUnitFailureHacker.java b/src/main/java/org/mockito/internal/junit/util/JUnitFailureHacker.java index 89bd4f1f4b..d6e784c647 100644 --- a/src/main/java/org/mockito/internal/junit/util/JUnitFailureHacker.java +++ b/src/main/java/org/mockito/internal/junit/util/JUnitFailureHacker.java @@ -7,7 +7,9 @@ import java.lang.reflect.Field; import org.junit.runner.notification.Failure; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.exceptions.ExceptionIncludingMockitoWarnings; +import org.mockito.plugins.MemberAccessor; @Deprecated public class JUnitFailureHacker { @@ -36,11 +38,11 @@ private boolean isEmpty(String warnings) { } private static Object getInternalState(Object target, String field) { + MemberAccessor accessor = Plugins.getMemberAccessor(); Class c = target.getClass(); try { Field f = getFieldFromHierarchy(c, field); - f.setAccessible(true); - return f.get(target); + return accessor.get(f, target); } catch (Exception e) { throw new RuntimeException( "Unable to get internal state on a private field. Please report to mockito mailing list.", @@ -49,11 +51,11 @@ private static Object getInternalState(Object target, String field) { } private static void setInternalState(Object target, String field, Object value) { + MemberAccessor accessor = Plugins.getMemberAccessor(); Class c = target.getClass(); try { Field f = getFieldFromHierarchy(c, field); - f.setAccessible(true); - f.set(target, value); + accessor.set(f, target, value); } catch (Exception e) { throw new RuntimeException( "Unable to set internal state on a private field. Please report to mockito mailing list.", diff --git a/src/main/java/org/mockito/internal/matchers/apachecommons/EqualsBuilder.java b/src/main/java/org/mockito/internal/matchers/apachecommons/EqualsBuilder.java index 9a43040e34..1789a5d7a3 100644 --- a/src/main/java/org/mockito/internal/matchers/apachecommons/EqualsBuilder.java +++ b/src/main/java/org/mockito/internal/matchers/apachecommons/EqualsBuilder.java @@ -4,7 +4,9 @@ */ package org.mockito.internal.matchers.apachecommons; -import java.lang.reflect.AccessibleObject; +import org.mockito.internal.configuration.plugins.Plugins; +import org.mockito.plugins.MemberAccessor; + import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Arrays; @@ -253,20 +255,16 @@ public static boolean reflectionEquals( return false; } EqualsBuilder equalsBuilder = new EqualsBuilder(); - try { - reflectionAppend(lhs, rhs, testClass, equalsBuilder, testTransients, excludeFields); - while (testClass.getSuperclass() != null && testClass != reflectUpToClass) { - testClass = testClass.getSuperclass(); - reflectionAppend(lhs, rhs, testClass, equalsBuilder, testTransients, excludeFields); - } - } catch (IllegalArgumentException e) { - // In this case, we tried to test a subclass vs. a superclass and - // the subclass has ivars or the ivars are transient and - // we are testing transients. - // If a subclass has ivars that we are trying to test them, we get an - // exception and we know that the objects are not equal. + if (reflectionAppend(lhs, rhs, testClass, equalsBuilder, testTransients, excludeFields)) { return false; } + while (testClass.getSuperclass() != null && testClass != reflectUpToClass) { + testClass = testClass.getSuperclass(); + if (reflectionAppend( + lhs, rhs, testClass, equalsBuilder, testTransients, excludeFields)) { + return false; + } + } return equalsBuilder.isEquals(); } @@ -281,7 +279,7 @@ public static boolean reflectionEquals( * @param useTransients whether to test transient fields * @param excludeFields array of field names to exclude from testing */ - private static void reflectionAppend( + private static boolean reflectionAppend( Object lhs, Object rhs, Class clazz, @@ -293,7 +291,7 @@ private static void reflectionAppend( excludeFields != null ? Arrays.asList(excludeFields) : Collections.emptyList(); - AccessibleObject.setAccessible(fields, true); + MemberAccessor accessor = Plugins.getMemberAccessor(); for (int i = 0; i < fields.length && builder.isEquals; i++) { Field f = fields[i]; if (!excludedFieldList.contains(f.getName()) @@ -301,14 +299,18 @@ private static void reflectionAppend( && (useTransients || !Modifier.isTransient(f.getModifiers())) && (!Modifier.isStatic(f.getModifiers()))) { try { - builder.append(f.get(lhs), f.get(rhs)); - } catch (IllegalAccessException e) { - // this can't happen. Would get a Security exception instead - // throw a runtime exception in case the impossible happens. - throw new InternalError("Unexpected IllegalAccessException"); + builder.append(accessor.get(f, lhs), accessor.get(f, rhs)); + } catch (RuntimeException | IllegalAccessException ignored) { + // In this case, we tried to test a subclass vs. a superclass and + // the subclass has ivars or the ivars are transient and we are + // testing transients. If a subclass has ivars that we are trying + // to test them, we get an exception and we know that the objects + // are not equal. + return true; } } } + return false; } // ------------------------------------------------------------------------- diff --git a/src/main/java/org/mockito/internal/stubbing/defaultanswers/ForwardsInvocations.java b/src/main/java/org/mockito/internal/stubbing/defaultanswers/ForwardsInvocations.java index 0df0f6c3ef..0f2aaa3975 100644 --- a/src/main/java/org/mockito/internal/stubbing/defaultanswers/ForwardsInvocations.java +++ b/src/main/java/org/mockito/internal/stubbing/defaultanswers/ForwardsInvocations.java @@ -11,8 +11,10 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.invocation.Invocation; import org.mockito.invocation.InvocationOnMock; +import org.mockito.plugins.MemberAccessor; import org.mockito.stubbing.Answer; /** @@ -41,13 +43,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { mockMethod, delegateMethod, invocation.getMock(), delegatedObject); } + MemberAccessor accessor = Plugins.getMemberAccessor(); Object[] rawArguments = ((Invocation) invocation).getRawArguments(); - try { - delegateMethod.setAccessible(true); - } catch (SecurityException ignore) { - // try to invoke anyway - } - return delegateMethod.invoke(delegatedObject, rawArguments); + return accessor.invoke(delegateMethod, delegatedObject, rawArguments); } catch (NoSuchMethodException e) { throw delegatedMethodDoesNotExistOnDelegate( mockMethod, invocation.getMock(), delegatedObject); diff --git a/src/main/java/org/mockito/internal/util/JavaEightUtil.java b/src/main/java/org/mockito/internal/util/JavaEightUtil.java index bc39d44b68..20c6e80c3b 100644 --- a/src/main/java/org/mockito/internal/util/JavaEightUtil.java +++ b/src/main/java/org/mockito/internal/util/JavaEightUtil.java @@ -181,7 +181,7 @@ private static Object invokeNullaryFactoryMethod(final String fqcn, final String private static Object getStaticFieldValue(final String fqcn, final String fieldName) { try { final Class type = getClass(fqcn); - final Field field = type.getDeclaredField(fieldName); + final Field field = type.getField(fieldName); return field.get(null); // any exception is really unexpected since the type name has // already been verified diff --git a/src/main/java/org/mockito/internal/util/reflection/AccessibilityChanger.java b/src/main/java/org/mockito/internal/util/reflection/AccessibilityChanger.java deleted file mode 100644 index 33725e0137..0000000000 --- a/src/main/java/org/mockito/internal/util/reflection/AccessibilityChanger.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2007 Mockito contributors - * This program is made available under the terms of the MIT License. - */ -package org.mockito.internal.util.reflection; - -import java.lang.reflect.AccessibleObject; - -public class AccessibilityChanger { - - private Boolean wasAccessible = null; - - /** - * safely disables access - */ - public void safelyDisableAccess(AccessibleObject accessibleObject) { - assert wasAccessible != null : "accessibility info shall not be null"; - try { - accessibleObject.setAccessible(wasAccessible); - } catch (Throwable t) { - // ignore - } - } - - /** - * changes the accessibleObject accessibility and returns true if accessibility was changed - */ - public void enableAccess(AccessibleObject accessibleObject) { - wasAccessible = accessibleObject.isAccessible(); - accessibleObject.setAccessible(true); - } -} diff --git a/src/main/java/org/mockito/internal/util/reflection/BeanPropertySetter.java b/src/main/java/org/mockito/internal/util/reflection/BeanPropertySetter.java index f91a35c202..7a33dcec7b 100644 --- a/src/main/java/org/mockito/internal/util/reflection/BeanPropertySetter.java +++ b/src/main/java/org/mockito/internal/util/reflection/BeanPropertySetter.java @@ -4,6 +4,9 @@ */ package org.mockito.internal.util.reflection; +import org.mockito.internal.configuration.plugins.Plugins; +import org.mockito.plugins.MemberAccessor; + import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -51,13 +54,11 @@ public BeanPropertySetter(final Object target, final Field propertyField) { */ public boolean set(final Object value) { - AccessibilityChanger changer = new AccessibilityChanger(); + MemberAccessor accessor = Plugins.getMemberAccessor(); Method writeMethod = null; try { writeMethod = target.getClass().getMethod(setterName(field.getName()), field.getType()); - - changer.enableAccess(writeMethod); - writeMethod.invoke(target, value); + accessor.invoke(writeMethod, target, value); return true; } catch (InvocationTargetException e) { throw new RuntimeException( @@ -83,10 +84,6 @@ public boolean set(final Object value) { e); } catch (NoSuchMethodException e) { reportNoSetterFound(); - } finally { - if (writeMethod != null) { - changer.safelyDisableAccess(writeMethod); - } } reportNoSetterFound(); diff --git a/src/main/java/org/mockito/internal/util/reflection/FieldCopier.java b/src/main/java/org/mockito/internal/util/reflection/FieldCopier.java deleted file mode 100644 index 67f7203944..0000000000 --- a/src/main/java/org/mockito/internal/util/reflection/FieldCopier.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (c) 2007 Mockito contributors - * This program is made available under the terms of the MIT License. - */ -package org.mockito.internal.util.reflection; - -import java.lang.reflect.Field; - -public class FieldCopier { - - public void copyValue(T from, T to, Field field) throws IllegalAccessException { - Object value = field.get(from); - field.set(to, value); - } -} diff --git a/src/main/java/org/mockito/internal/util/reflection/FieldInitializer.java b/src/main/java/org/mockito/internal/util/reflection/FieldInitializer.java index a5c5d708b5..53bc8a1f6a 100644 --- a/src/main/java/org/mockito/internal/util/reflection/FieldInitializer.java +++ b/src/main/java/org/mockito/internal/util/reflection/FieldInitializer.java @@ -4,9 +4,10 @@ */ package org.mockito.internal.util.reflection; -import static java.lang.reflect.Modifier.isStatic; - -import static org.mockito.internal.util.reflection.FieldSetter.setField; +import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.configuration.plugins.Plugins; +import org.mockito.internal.util.MockUtil; +import org.mockito.plugins.MemberAccessor; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -17,8 +18,7 @@ import java.util.Comparator; import java.util.List; -import org.mockito.exceptions.base.MockitoException; -import org.mockito.internal.util.MockUtil; +import static java.lang.reflect.Modifier.isStatic; /** * Initialize a field with type instance if a default constructor can be found. @@ -87,9 +87,6 @@ private FieldInitializer(Object fieldOwner, Field field, ConstructorInstantiator * @return Actual field instance. */ public FieldInitializationReport initialize() { - final AccessibilityChanger changer = new AccessibilityChanger(); - changer.enableAccess(field); - try { return acquireFieldInstance(); } catch (IllegalAccessException e) { @@ -100,8 +97,6 @@ public FieldInitializationReport initialize() { + field.getType().getSimpleName() + "'", e); - } finally { - changer.safelyDisableAccess(field); } } @@ -142,7 +137,8 @@ private void checkNotEnum(Field field) { } private FieldInitializationReport acquireFieldInstance() throws IllegalAccessException { - Object fieldInstance = field.get(fieldOwner); + final MemberAccessor accessor = Plugins.getMemberAccessor(); + Object fieldInstance = accessor.get(field, fieldOwner); if (fieldInstance != null) { return new FieldInitializationReport(fieldInstance, false, false); } @@ -198,17 +194,15 @@ static class NoArgConstructorInstantiator implements ConstructorInstantiator { } public FieldInitializationReport instantiate() { - final AccessibilityChanger changer = new AccessibilityChanger(); - Constructor constructor = null; + final MemberAccessor invoker = Plugins.getMemberAccessor(); try { - constructor = field.getType().getDeclaredConstructor(); - changer.enableAccess(constructor); + Constructor constructor = field.getType().getDeclaredConstructor(); final Object[] noArg = new Object[0]; - Object newFieldInstance = constructor.newInstance(noArg); - setField(testClass, field, newFieldInstance); + Object newFieldInstance = invoker.newInstance(constructor, noArg); + invoker.set(field, testClass, newFieldInstance); - return new FieldInitializationReport(field.get(testClass), true, false); + return new FieldInitializationReport(invoker.get(field, testClass), true, false); } catch (NoSuchMethodException e) { throw new MockitoException( "the type '" @@ -230,10 +224,6 @@ public FieldInitializationReport instantiate() { throw new MockitoException( "IllegalAccessException (see the stack trace for cause): " + e.toString(), e); - } finally { - if (constructor != null) { - changer.safelyDisableAccess(constructor); - } } } } @@ -289,18 +279,14 @@ private int countMockableParams(Constructor constructor) { } public FieldInitializationReport instantiate() { - final AccessibilityChanger changer = new AccessibilityChanger(); - Constructor constructor = null; + final MemberAccessor accessor = Plugins.getMemberAccessor(); + Constructor constructor = biggestConstructor(field.getType()); + final Object[] args = argResolver.resolveTypeInstances(constructor.getParameterTypes()); try { - constructor = biggestConstructor(field.getType()); - changer.enableAccess(constructor); - - final Object[] args = - argResolver.resolveTypeInstances(constructor.getParameterTypes()); - Object newFieldInstance = constructor.newInstance(args); - setField(testClass, field, newFieldInstance); + Object newFieldInstance = accessor.newInstance(constructor, args); + accessor.set(field, testClass, newFieldInstance); - return new FieldInitializationReport(field.get(testClass), false, true); + return new FieldInitializationReport(accessor.get(field, testClass), false, true); } catch (IllegalArgumentException e) { throw new MockitoException( "internal error : argResolver provided incorrect types for constructor " @@ -323,10 +309,6 @@ public FieldInitializationReport instantiate() { throw new MockitoException( "IllegalAccessException (see the stack trace for cause): " + e.toString(), e); - } finally { - if (constructor != null) { - changer.safelyDisableAccess(constructor); - } } } diff --git a/src/main/java/org/mockito/internal/util/reflection/FieldReader.java b/src/main/java/org/mockito/internal/util/reflection/FieldReader.java index 707eee6515..e202c714a0 100644 --- a/src/main/java/org/mockito/internal/util/reflection/FieldReader.java +++ b/src/main/java/org/mockito/internal/util/reflection/FieldReader.java @@ -7,17 +7,18 @@ import java.lang.reflect.Field; import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.configuration.plugins.Plugins; +import org.mockito.plugins.MemberAccessor; public class FieldReader { final Object target; final Field field; - final AccessibilityChanger changer = new AccessibilityChanger(); + final MemberAccessor accessor = Plugins.getMemberAccessor(); public FieldReader(Object target, Field field) { this.target = target; this.field = field; - changer.enableAccess(field); } public boolean isNull() { @@ -26,7 +27,7 @@ public boolean isNull() { public Object read() { try { - return field.get(target); + return accessor.get(field, target); } catch (Exception e) { throw new MockitoException( "Cannot read state from field: " + field + ", on instance: " + target); diff --git a/src/main/java/org/mockito/internal/util/reflection/FieldSetter.java b/src/main/java/org/mockito/internal/util/reflection/FieldSetter.java deleted file mode 100644 index 46f906d9bf..0000000000 --- a/src/main/java/org/mockito/internal/util/reflection/FieldSetter.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2007 Mockito contributors - * This program is made available under the terms of the MIT License. - */ -package org.mockito.internal.util.reflection; - -import java.lang.reflect.Field; - -public class FieldSetter { - - private FieldSetter() {} - - public static void setField(Object target, Field field, Object value) { - AccessibilityChanger changer = new AccessibilityChanger(); - changer.enableAccess(field); - try { - field.set(target, value); - } catch (IllegalAccessException e) { - throw new RuntimeException( - "Access not authorized on field '" - + field - + "' of object '" - + target - + "' with value: '" - + value - + "'", - e); - } catch (IllegalArgumentException e) { - throw new RuntimeException( - "Wrong argument on field '" - + field - + "' of object '" - + target - + "' with value: '" - + value - + "', \n" - + "reason : " - + e.getMessage(), - e); - } - changer.safelyDisableAccess(field); - } -} diff --git a/src/main/java/org/mockito/internal/util/reflection/InstanceField.java b/src/main/java/org/mockito/internal/util/reflection/InstanceField.java index 5585874f46..03cd02c051 100644 --- a/src/main/java/org/mockito/internal/util/reflection/InstanceField.java +++ b/src/main/java/org/mockito/internal/util/reflection/InstanceField.java @@ -4,12 +4,13 @@ */ package org.mockito.internal.util.reflection; -import static org.mockito.internal.util.reflection.FieldSetter.setField; - import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import org.mockito.exceptions.base.MockitoException; +import org.mockito.internal.configuration.plugins.Plugins; import org.mockito.internal.util.Checks; +import org.mockito.plugins.MemberAccessor; /** * Represents an accessible instance field. @@ -47,10 +48,14 @@ public Object read() { * Set the given value to the field of this instance. * * @param value The value that should be written to the field. - * @see FieldSetter */ public void set(Object value) { - setField(instance, field, value); + MemberAccessor accessor = Plugins.getMemberAccessor(); + try { + accessor.set(field, instance, value); + } catch (IllegalAccessException e) { + throw new MockitoException("Access to " + field + " was denied", e); + } } /** diff --git a/src/main/java/org/mockito/internal/util/reflection/InstrumentationMemberAccessor.java b/src/main/java/org/mockito/internal/util/reflection/InstrumentationMemberAccessor.java new file mode 100644 index 0000000000..c74c79efa6 --- /dev/null +++ b/src/main/java/org/mockito/internal/util/reflection/InstrumentationMemberAccessor.java @@ -0,0 +1,363 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.util.reflection; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.agent.ByteBuddyAgent; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.MethodCall; +import org.mockito.exceptions.base.MockitoInitializationException; +import org.mockito.plugins.MemberAccessor; + +import java.lang.instrument.Instrumentation; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.*; +import java.util.*; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static org.mockito.internal.util.StringUtil.join; + +class InstrumentationMemberAccessor implements MemberAccessor { + + private static final Map, Class> WRAPPERS = new HashMap<>(); + + private static final Instrumentation INSTRUMENTATION; + private static final Dispatcher DISPATCHER; + + private static final Throwable INITIALIZATION_ERROR; + + static { + WRAPPERS.put(boolean.class, Boolean.class); + WRAPPERS.put(byte.class, Byte.class); + WRAPPERS.put(short.class, Short.class); + WRAPPERS.put(char.class, Character.class); + WRAPPERS.put(int.class, Integer.class); + WRAPPERS.put(long.class, Long.class); + WRAPPERS.put(float.class, Float.class); + WRAPPERS.put(double.class, Double.class); + Instrumentation instrumentation; + Dispatcher dispatcher; + Throwable throwable; + try { + instrumentation = ByteBuddyAgent.install(); + // We need to generate a dispatcher instance that is located in a distinguished class + // loader to create a unique (unnamed) module to which we can open other packages to. + // This way, we assure that classes within Mockito's module (which might be a shared, + // unnamed module) do not face escalated privileges where tests might pass that would + // otherwise fail without Mockito's opening. + dispatcher = + new ByteBuddy() + .subclass(Dispatcher.class) + .method(named("getLookup")) + .intercept(MethodCall.invoke(MethodHandles.class.getMethod("lookup"))) + .method(named("getModule")) + .intercept( + MethodCall.invoke(Class.class.getMethod("getModule")) + .onMethodCall( + MethodCall.invoke( + Object.class.getMethod("getClass")))) + .method(named("setAccessible")) + .intercept( + MethodCall.invoke( + AccessibleObject.class.getMethod( + "setAccessible", boolean.class)) + .onArgument(0) + .withArgument(1)) + .make() + .load( + InstrumentationMemberAccessor.class.getClassLoader(), + ClassLoadingStrategy.Default.WRAPPER) + .getLoaded() + .getConstructor() + .newInstance(); + throwable = null; + } catch (Throwable t) { + instrumentation = null; + dispatcher = null; + throwable = t; + } + INSTRUMENTATION = instrumentation; + DISPATCHER = dispatcher; + INITIALIZATION_ERROR = throwable; + } + + private final MethodHandle getModule, isOpen, redefineModule, privateLookupIn; + + InstrumentationMemberAccessor() { + if (INITIALIZATION_ERROR != null) { + throw new MockitoInitializationException( + join( + "Could not initialize the Mockito instrumentation member accessor", + "", + "This is unexpected on JVMs from Java 9 or later - possibly, the instrumentation API could not be resolved"), + INITIALIZATION_ERROR); + } + try { + Class module = Class.forName("java.lang.Module"); + getModule = + MethodHandles.publicLookup() + .findVirtual(Class.class, "getModule", MethodType.methodType(module)); + isOpen = + MethodHandles.publicLookup() + .findVirtual( + module, + "isOpen", + MethodType.methodType(boolean.class, String.class, module)); + redefineModule = + MethodHandles.publicLookup() + .findVirtual( + Instrumentation.class, + "redefineModule", + MethodType.methodType( + void.class, + module, + Set.class, + Map.class, + Map.class, + Set.class, + Map.class)); + privateLookupIn = + MethodHandles.publicLookup() + .findStatic( + MethodHandles.class, + "privateLookupIn", + MethodType.methodType( + MethodHandles.Lookup.class, + Class.class, + MethodHandles.Lookup.class)); + } catch (Throwable t) { + throw new MockitoInitializationException( + "Could not resolve instrumentation invoker", t); + } + } + + @Override + public Object newInstance(Constructor constructor, Object... arguments) + throws InstantiationException, InvocationTargetException { + if (Modifier.isAbstract(constructor.getDeclaringClass().getModifiers())) { + throw new InstantiationException( + "Cannot instantiate abstract " + constructor.getDeclaringClass().getTypeName()); + } + assureArguments(constructor, null, null, arguments, constructor.getParameterTypes()); + try { + Object module = getModule.bindTo(constructor.getDeclaringClass()).invokeWithArguments(); + String packageName = constructor.getDeclaringClass().getPackage().getName(); + assureOpen(module, packageName); + MethodHandle handle = + ((MethodHandles.Lookup) + privateLookupIn.invokeExact( + constructor.getDeclaringClass(), + DISPATCHER.getLookup())) + .unreflectConstructor(constructor); + try { + return handle.invokeWithArguments(arguments); + } catch (Throwable t) { + throw new InvocationTargetException(t); + } + } catch (InvocationTargetException e) { + throw e; + } catch (Throwable t) { + throw new IllegalStateException( + "Could not construct " + + constructor + + " with arguments " + + Arrays.toString(arguments), + t); + } + } + + @Override + public Object invoke(Method method, Object target, Object... arguments) + throws InvocationTargetException { + assureArguments( + method, + Modifier.isStatic(method.getModifiers()) ? null : target, + method.getDeclaringClass(), + arguments, + method.getParameterTypes()); + try { + Object module = getModule.bindTo(method.getDeclaringClass()).invokeWithArguments(); + String packageName = method.getDeclaringClass().getPackage().getName(); + assureOpen(module, packageName); + MethodHandle handle = + ((MethodHandles.Lookup) + privateLookupIn.invokeExact( + method.getDeclaringClass(), DISPATCHER.getLookup())) + .unreflect(method); + if (!Modifier.isStatic(method.getModifiers())) { + handle = handle.bindTo(target); + } + try { + return handle.invokeWithArguments(arguments); + } catch (Throwable t) { + throw new InvocationTargetException(t); + } + } catch (InvocationTargetException e) { + throw e; + } catch (Throwable t) { + throw new IllegalStateException( + "Could not invoke " + + method + + " on " + + target + + " with arguments " + + Arrays.toString(arguments), + t); + } + } + + @Override + public Object get(Field field, Object target) { + assureArguments( + field, + Modifier.isStatic(field.getModifiers()) ? null : target, + field.getDeclaringClass(), + new Object[0], + new Class[0]); + try { + Object module = getModule.bindTo(field.getDeclaringClass()).invokeWithArguments(); + String packageName = field.getDeclaringClass().getPackage().getName(); + assureOpen(module, packageName); + MethodHandle handle = + ((MethodHandles.Lookup) + privateLookupIn.invokeExact( + field.getDeclaringClass(), DISPATCHER.getLookup())) + .unreflectGetter(field); + if (!Modifier.isStatic(field.getModifiers())) { + handle = handle.bindTo(target); + } + return handle.invokeWithArguments(); + } catch (Throwable t) { + throw new IllegalStateException("Could not read " + field + " on " + target, t); + } + } + + @Override + public void set(Field field, Object target, Object value) throws IllegalAccessException { + assureArguments( + field, + Modifier.isStatic(field.getModifiers()) ? null : target, + field.getDeclaringClass(), + new Object[] {value}, + new Class[] {field.getType()}); + boolean illegalAccess = false; + try { + Object module = getModule.bindTo(field.getDeclaringClass()).invokeWithArguments(); + String packageName = field.getDeclaringClass().getPackage().getName(); + assureOpen(module, packageName); + // Method handles do not allow setting final fields where setAccessible(true) + // is required before unreflecting. + boolean isFinal; + if (Modifier.isFinal(field.getModifiers())) { + isFinal = true; + try { + DISPATCHER.setAccessible(field, true); + } catch (Throwable ignored) { + illegalAccess = + true; // To distinguish from propagated illegal access exception. + throw new IllegalAccessException( + "Could not make final field " + field + " accessible"); + } + } else { + isFinal = false; + } + try { + MethodHandle handle = + ((MethodHandles.Lookup) + privateLookupIn.invokeExact( + field.getDeclaringClass(), DISPATCHER.getLookup())) + .unreflectSetter(field); + if (!Modifier.isStatic(field.getModifiers())) { + handle = handle.bindTo(target); + } + handle.invokeWithArguments(value); + } finally { + if (isFinal) { + DISPATCHER.setAccessible(field, false); + } + } + } catch (Throwable t) { + if (illegalAccess) { + throw (IllegalAccessException) t; + } else { + throw new IllegalStateException("Could not read " + field + " on " + target, t); + } + } + } + + private void assureOpen(Object module, String packageName) throws Throwable { + if (!(Boolean) isOpen.invokeWithArguments(module, packageName, DISPATCHER.getModule())) { + redefineModule + .bindTo(INSTRUMENTATION) + .invokeWithArguments( + module, + Collections.emptySet(), + Collections.emptyMap(), + Collections.singletonMap( + packageName, Collections.singleton(DISPATCHER.getModule())), + Collections.emptySet(), + Collections.emptyMap()); + } + } + + private static void assureArguments( + AccessibleObject target, + Object owner, + Class type, + Object[] values, + Class[] types) { + if (owner != null) { + if (!type.isAssignableFrom(owner.getClass())) { + throw new IllegalArgumentException("Cannot access " + target + " on " + owner); + } + } + if (types.length != values.length) { + throw new IllegalArgumentException( + "Incorrect number of arguments for " + + target + + ": expected " + + types.length + + " but recevied " + + values.length); + } + for (int index = 0; index < values.length; index++) { + if (values[index] == null) { + if (types[index].isPrimitive()) { + throw new IllegalArgumentException( + "Cannot assign null to primitive type " + + types[index].getTypeName() + + " for " + + index + + " parameter of " + + target); + } + } else { + Class resolved = WRAPPERS.getOrDefault(types[index], types[index]); + if (!resolved.isAssignableFrom(values[index].getClass())) { + throw new IllegalArgumentException( + "Cannot assign value of type " + + values[index].getClass() + + " to " + + resolved + + " for " + + index + + " parameter of " + + target); + } + } + } + } + + public interface Dispatcher { + + MethodHandles.Lookup getLookup(); + + Object getModule(); + + void setAccessible(AccessibleObject target, boolean value); + } +} diff --git a/src/main/java/org/mockito/internal/util/reflection/LenientCopyTool.java b/src/main/java/org/mockito/internal/util/reflection/LenientCopyTool.java index ccdd923835..95b30198db 100644 --- a/src/main/java/org/mockito/internal/util/reflection/LenientCopyTool.java +++ b/src/main/java/org/mockito/internal/util/reflection/LenientCopyTool.java @@ -4,13 +4,15 @@ */ package org.mockito.internal.util.reflection; +import org.mockito.internal.configuration.plugins.Plugins; +import org.mockito.plugins.MemberAccessor; + import java.lang.reflect.Field; import java.lang.reflect.Modifier; -@SuppressWarnings("unchecked") public class LenientCopyTool { - FieldCopier fieldCopier = new FieldCopier(); + MemberAccessor accessor = Plugins.getMemberAccessor(); public void copyToMock(T from, T mock) { copy(from, mock, from.getClass()); @@ -35,14 +37,11 @@ private void copyValues(T from, T mock, Class classFrom) { if (Modifier.isStatic(field.getModifiers())) { continue; } - AccessibilityChanger accessibilityChanger = new AccessibilityChanger(); try { - accessibilityChanger.enableAccess(field); - fieldCopier.copyValue(from, mock, field); + Object value = accessor.get(field, from); + accessor.set(field, mock, value); } catch (Throwable t) { // Ignore - be lenient - if some field cannot be copied then let's be it - } finally { - accessibilityChanger.safelyDisableAccess(field); } } } diff --git a/src/main/java/org/mockito/internal/util/reflection/ModuleMemberAccessor.java b/src/main/java/org/mockito/internal/util/reflection/ModuleMemberAccessor.java new file mode 100644 index 0000000000..8be77860ad --- /dev/null +++ b/src/main/java/org/mockito/internal/util/reflection/ModuleMemberAccessor.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.util.reflection; + +import net.bytebuddy.ClassFileVersion; +import org.mockito.plugins.MemberAccessor; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ModuleMemberAccessor implements MemberAccessor { + + private final MemberAccessor delegate; + + public ModuleMemberAccessor() { + if (ClassFileVersion.ofThisVm().isAtLeast(ClassFileVersion.JAVA_V9)) { + delegate = new InstrumentationMemberAccessor(); + } else { + delegate = new ReflectionMemberAccessor(); + } + } + + @Override + public Object newInstance(Constructor constructor, Object... arguments) + throws InstantiationException, InvocationTargetException, IllegalAccessException { + return delegate.newInstance(constructor, arguments); + } + + @Override + public Object invoke(Method method, Object target, Object... arguments) + throws InvocationTargetException, IllegalAccessException { + return delegate.invoke(method, target, arguments); + } + + @Override + public Object get(Field field, Object target) throws IllegalAccessException { + return delegate.get(field, target); + } + + @Override + public void set(Field field, Object target, Object value) throws IllegalAccessException { + delegate.set(field, target, value); + } +} diff --git a/src/main/java/org/mockito/internal/util/reflection/ReflectionMemberAccessor.java b/src/main/java/org/mockito/internal/util/reflection/ReflectionMemberAccessor.java new file mode 100644 index 0000000000..e61a5bcc02 --- /dev/null +++ b/src/main/java/org/mockito/internal/util/reflection/ReflectionMemberAccessor.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2007 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.util.reflection; + +import org.mockito.plugins.MemberAccessor; + +import java.lang.reflect.*; +import java.util.Arrays; + +public class ReflectionMemberAccessor implements MemberAccessor { + + @Override + public Object newInstance(Constructor constructor, Object... arguments) + throws InstantiationException, InvocationTargetException, IllegalAccessException { + silentSetAccessible(constructor, true); + try { + return constructor.newInstance(arguments); + } catch (InvocationTargetException + | IllegalAccessException + | InstantiationException + | IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException( + "Failed to invoke " + constructor + " with " + Arrays.toString(arguments), e); + } finally { + silentSetAccessible(constructor, false); + } + } + + @Override + public Object invoke(Method method, Object target, Object... arguments) + throws InvocationTargetException, IllegalAccessException { + silentSetAccessible(method, true); + try { + return method.invoke(target, arguments); + } catch (InvocationTargetException | IllegalAccessException | IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("Could not invoke " + method + " on " + target, e); + } finally { + silentSetAccessible(method, false); + } + } + + @Override + public Object get(Field field, Object target) throws IllegalAccessException { + silentSetAccessible(field, true); + try { + return field.get(target); + } catch (IllegalAccessException | IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("Could not read " + field + " from " + target); + } finally { + silentSetAccessible(field, false); + } + } + + @Override + public void set(Field field, Object target, Object value) throws IllegalAccessException { + silentSetAccessible(field, true); + try { + field.set(target, value); + } catch (IllegalAccessException | IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("Could not write " + field + " to " + target, e); + } finally { + silentSetAccessible(field, false); + } + } + + private static void silentSetAccessible(AccessibleObject object, boolean value) { + try { + object.setAccessible(value); + } catch (Exception ignored) { + } + } +} diff --git a/src/main/java/org/mockito/plugins/MemberAccessor.java b/src/main/java/org/mockito/plugins/MemberAccessor.java new file mode 100644 index 0000000000..081814e1db --- /dev/null +++ b/src/main/java/org/mockito/plugins/MemberAccessor.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.plugins; + +import org.mockito.Incubating; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * A member accessor is responsible for invoking methods, constructors and for setting + * and reading field values. + */ +@Incubating +public interface MemberAccessor { + + Object newInstance(Constructor constructor, Object... arguments) + throws InstantiationException, InvocationTargetException, IllegalAccessException; + + Object invoke(Method method, Object target, Object... arguments) + throws InvocationTargetException, IllegalAccessException; + + Object get(Field field, Object target) throws IllegalAccessException; + + void set(Field field, Object target, Object value) throws IllegalAccessException; +} diff --git a/src/test/java/org/mockito/internal/util/reflection/AccessibilityChangerTest.java b/src/test/java/org/mockito/internal/util/reflection/AccessibilityChangerTest.java deleted file mode 100644 index 032cb53770..0000000000 --- a/src/test/java/org/mockito/internal/util/reflection/AccessibilityChangerTest.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2007 Mockito contributors - * This program is made available under the terms of the MIT License. - */ -package org.mockito.internal.util.reflection; - -import static org.mockitoutil.VmArgAssumptions.assumeVmArgPresent; - -import java.lang.reflect.Field; -import java.util.Observable; - -import org.junit.Test; - -public class AccessibilityChangerTest { - - @SuppressWarnings("unused") - private Observable whatever; - - @Test - public void should_enable_and_safely_disable() throws Exception { - AccessibilityChanger changer = new AccessibilityChanger(); - changer.enableAccess(field("whatever")); - changer.safelyDisableAccess(field("whatever")); - } - - @Test(expected = java.lang.AssertionError.class) - public void safelyDisableAccess_should_fail_when_enableAccess_not_called() throws Exception { - assumeVmArgPresent("-ea"); - new AccessibilityChanger().safelyDisableAccess(field("whatever")); - } - - private Field field(String fieldName) throws NoSuchFieldException { - return this.getClass().getDeclaredField(fieldName); - } -} diff --git a/src/test/java/org/mockito/internal/util/reflection/LenientCopyToolTest.java b/src/test/java/org/mockito/internal/util/reflection/LenientCopyToolTest.java index 76ebd337b9..b8c742aff7 100644 --- a/src/test/java/org/mockito/internal/util/reflection/LenientCopyToolTest.java +++ b/src/test/java/org/mockito/internal/util/reflection/LenientCopyToolTest.java @@ -13,6 +13,7 @@ import java.util.LinkedList; import org.junit.Test; +import org.mockito.plugins.MemberAccessor; import org.mockitoutil.TestBase; @SuppressWarnings("unchecked") @@ -144,19 +145,20 @@ public void shouldEnableAndThenDisableAccessibility() throws Exception { @Test public void shouldContinueEvenIfThereAreProblemsCopyingSingleFieldValue() throws Exception { // given - tool.fieldCopier = mock(FieldCopier.class); + tool.accessor = mock(MemberAccessor.class); doNothing() - .doThrow(new IllegalAccessException()) + .doThrow(new IllegalStateException()) .doNothing() - .when(tool.fieldCopier) - .copyValue(anyObject(), anyObject(), any(Field.class)); + .when(tool.accessor) + .set(any(Field.class), anyObject(), anyObject()); // when tool.copyToMock(from, to); // then - verify(tool.fieldCopier, atLeast(3)).copyValue(any(), any(), any(Field.class)); + verify(tool.accessor, atLeast(3)).get(any(Field.class), any()); + verify(tool.accessor, atLeast(3)).set(any(Field.class), any(), any()); } @Test diff --git a/src/test/java/org/mockito/internal/util/reflection/MemberAccessorTest.java b/src/test/java/org/mockito/internal/util/reflection/MemberAccessorTest.java new file mode 100644 index 0000000000..ee390312ed --- /dev/null +++ b/src/test/java/org/mockito/internal/util/reflection/MemberAccessorTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.util.reflection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.plugins.MemberAccessor; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@RunWith(Parameterized.class) +public class MemberAccessorTest { + + @Parameterized.Parameters + public static Collection data() { + List data = new ArrayList<>(); + data.add(new Object[] {new ReflectionMemberAccessor()}); + data.add(new Object[] {new ModuleMemberAccessor()}); + return data; + } + + private final MemberAccessor accessor; + + public MemberAccessorTest(MemberAccessor accessor) { + this.accessor = accessor; + } + + @Test + public void test_read_field() throws Exception { + assertThat(accessor.get(Sample.class.getDeclaredField("field"), new Sample("foo"))) + .isEqualTo("foo"); + } + + @Test + public void test_read_static_field() throws Exception { + Sample.staticField = "foo"; + assertThat(accessor.get(Sample.class.getDeclaredField("staticField"), null)) + .isEqualTo("foo"); + } + + @Test + public void test_write_field() throws Exception { + Sample sample = new Sample("foo"); + accessor.set(Sample.class.getDeclaredField("field"), sample, "bar"); + assertThat(sample.field).isEqualTo("bar"); + } + + @Test + public void test_write_static_field() throws Exception { + Sample.staticField = "foo"; + accessor.set(Sample.class.getDeclaredField("staticField"), null, "bar"); + assertThat(Sample.staticField).isEqualTo("bar"); + } + + @Test + public void test_invoke() throws Exception { + assertThat( + accessor.invoke( + Sample.class.getDeclaredMethod("test", String.class), + new Sample(null), + "foo")) + .isEqualTo("foo"); + } + + @Test + public void test_invoke_invocation_exception() { + assertThatThrownBy( + () -> + accessor.invoke( + Sample.class.getDeclaredMethod("test", String.class), + new Sample(null), + "exception")) + .isInstanceOf(InvocationTargetException.class); + } + + @Test + public void test_invoke_illegal_arguments() { + assertThatThrownBy( + () -> + accessor.invoke( + Sample.class.getDeclaredMethod("test", String.class), + new Sample(null), + 42)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void test_new_instance() throws Exception { + assertThat(accessor.newInstance(Sample.class.getDeclaredConstructor(String.class), "foo")) + .isInstanceOf(Sample.class); + } + + @Test + public void test_new_instance_illegal_arguments() { + assertThatThrownBy( + () -> + accessor.newInstance( + Sample.class.getDeclaredConstructor(String.class), 42)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void test_new_instance_invocation_exception() { + assertThatThrownBy( + () -> + accessor.newInstance( + Sample.class.getDeclaredConstructor(String.class), + "exception")) + .isInstanceOf(InvocationTargetException.class); + } + + @Test + public void test_new_instance_instantiation_exception() { + assertThatThrownBy( + () -> accessor.newInstance(AbstractSample.class.getDeclaredConstructor())) + .isInstanceOf(InstantiationException.class); + } + + @Test + public void test_set_final_field() throws Exception { + Sample sample = new Sample("foo"); + accessor.set(Sample.class.getDeclaredField("finalField"), sample, "foo"); + assertThat(sample.finalField).isEqualTo("foo"); + } + + private static class Sample { + + private String field; + + private final String finalField = null; + + private static String staticField = "foo"; + + public Sample(String field) { + if ("exception".equals(field)) { + throw new RuntimeException(); + } + this.field = field; + } + + private String test(String value) { + if ("exception".equals(value)) { + throw new RuntimeException(); + } + return value; + } + } + + private abstract static class AbstractSample {} +} diff --git a/src/test/java/org/mockitointegration/NoByteCodeDependenciesTest.java b/src/test/java/org/mockitointegration/NoByteCodeDependenciesTest.java index 342007122c..2363fe6f35 100644 --- a/src/test/java/org/mockitointegration/NoByteCodeDependenciesTest.java +++ b/src/test/java/org/mockitointegration/NoByteCodeDependenciesTest.java @@ -41,6 +41,10 @@ public void pure_mockito_should_not_depend_bytecode_libraries() throws Exception pureMockitoAPIClasses.remove("org.mockito.internal.exceptions.stacktrace.StackTraceFilter"); pureMockitoAPIClasses.remove("org.mockito.internal.util.MockUtil"); + // Remove instrumentation-based member accessor which is optional. + pureMockitoAPIClasses.remove( + "org.mockito.internal.util.reflection.InstrumentationMemberAccessor"); + for (String pureMockitoAPIClass : pureMockitoAPIClasses) { checkDependency(classLoader_without_bytecode_libraries, pureMockitoAPIClass); } diff --git a/src/test/java/org/mockitousage/annotation/MockInjectionUsingSetterOrPropertyTest.java b/src/test/java/org/mockitousage/annotation/MockInjectionUsingSetterOrPropertyTest.java index 765c4b8d0a..0d2af98d67 100644 --- a/src/test/java/org/mockitousage/annotation/MockInjectionUsingSetterOrPropertyTest.java +++ b/src/test/java/org/mockitousage/annotation/MockInjectionUsingSetterOrPropertyTest.java @@ -12,6 +12,7 @@ import java.util.TreeSet; import org.assertj.core.api.Assertions; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; @@ -50,10 +51,19 @@ public class MockInjectionUsingSetterOrPropertyTest extends TestBase { @Spy private TreeSet searchTree = new TreeSet(); + private AutoCloseable session; + @Before public void enforces_new_instances() { - // initMocks called in TestBase Before method, so instances are not the same - MockitoAnnotations.openMocks(this); + // openMocks called in TestBase Before method, so instances are not the same + session = MockitoAnnotations.openMocks(this); + } + + @After + public void close_new_instances() throws Exception { + if (session != null) { + session.close(); + } } @Test diff --git a/src/test/java/org/mockitoutil/ClassLoaders.java b/src/test/java/org/mockitoutil/ClassLoaders.java index 2b3f51051d..308b309ec8 100644 --- a/src/test/java/org/mockitoutil/ClassLoaders.java +++ b/src/test/java/org/mockitoutil/ClassLoaders.java @@ -36,6 +36,8 @@ import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; +import org.mockito.internal.configuration.plugins.Plugins; +import org.mockito.plugins.MemberAccessor; import org.objenesis.Objenesis; import org.objenesis.ObjenesisStd; import org.objenesis.instantiator.ObjectInstantiator; @@ -167,9 +169,8 @@ Runnable reloadTaskInClassLoader(Runnable task) { continue; } if (declaredField.getType() == field.getType()) { // don't copy this - field.setAccessible(true); - declaredField.setAccessible(true); - declaredField.set(reloaded, field.get(task)); + MemberAccessor accessor = Plugins.getMemberAccessor(); + accessor.set(declaredField, reloaded, accessor.get(field, task)); } } diff --git a/subprojects/inline/src/main/resources/mockito-extensions/org.mockito.plugins.MemberAccessor b/subprojects/inline/src/main/resources/mockito-extensions/org.mockito.plugins.MemberAccessor new file mode 100644 index 0000000000..1422f9900b --- /dev/null +++ b/subprojects/inline/src/main/resources/mockito-extensions/org.mockito.plugins.MemberAccessor @@ -0,0 +1 @@ +member-accessor-module diff --git a/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleAccessTest.java b/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleAccessTest.java new file mode 100644 index 0000000000..6382a253b8 --- /dev/null +++ b/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleAccessTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.moduletest; + +import org.junit.Test; +import org.mockito.internal.util.reflection.ModuleMemberAccessor; +import org.mockito.internal.util.reflection.ReflectionMemberAccessor; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Path; +import java.util.concurrent.Callable; + +import static junit.framework.TestCase.fail; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.moduletest.ModuleUtil.layer; +import static org.mockito.moduletest.ModuleUtil.modularJar; + +public class ModuleAccessTest { + + @Test + public void can_access_non_opened_module_with_module_member_accessor() throws Exception { + Path jar = modularJar(false, false, false); + ModuleLayer layer = layer(jar, false, true); + + ClassLoader loader = layer.findLoader("mockito.test"); + Class type = loader.loadClass("sample.MyCallable"); + + ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(loader); + try { + Class moduleMemberAccessor = loader.loadClass(ModuleMemberAccessor.class.getName()); + Object instance = moduleMemberAccessor.getConstructor().newInstance(); + @SuppressWarnings("unchecked") + Callable mock = (Callable) moduleMemberAccessor + .getMethod("newInstance", Constructor.class, Object[].class) + .invoke(instance, type.getConstructor(), new Object[0]); + assertThat(mock.call()).isEqualTo("foo"); + } finally { + Thread.currentThread().setContextClassLoader(contextLoader); + } + } + + @Test + public void cannot_access_non_opened_module_with_reflection_member_accessor() throws Exception { + Path jar = modularJar(false, false, false); + ModuleLayer layer = layer(jar, false, true); + + ClassLoader loader = layer.findLoader("mockito.test"); + Class type = loader.loadClass("sample.MyCallable"); + + ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(loader); + try { + Class moduleMemberAccessor = loader.loadClass(ReflectionMemberAccessor.class.getName()); + try { + Object instance = moduleMemberAccessor.getConstructor().newInstance(); + moduleMemberAccessor + .getMethod("newInstance", Constructor.class, Object[].class) + .invoke(instance, type.getConstructor(), new Object[0]); + fail(); + } catch (InvocationTargetException e) { + assertThat(e.getCause()).isInstanceOf(IllegalAccessException.class); + } + } finally { + Thread.currentThread().setContextClassLoader(contextLoader); + } + } +} diff --git a/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleHandlingTest.java b/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleHandlingTest.java index ed732c8089..fec5e9a841 100644 --- a/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleHandlingTest.java +++ b/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleHandlingTest.java @@ -4,15 +4,7 @@ */ package org.mockito.moduletest; -import java.util.concurrent.locks.Lock; -import net.bytebuddy.ByteBuddy; -import net.bytebuddy.description.modifier.Visibility; import net.bytebuddy.dynamic.loading.ClassInjector; -import net.bytebuddy.implementation.FixedValue; -import net.bytebuddy.jar.asm.ClassWriter; -import net.bytebuddy.jar.asm.ModuleVisitor; -import net.bytebuddy.jar.asm.Opcodes; -import net.bytebuddy.utility.OpenedClassReader; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -22,33 +14,19 @@ import org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker; import org.mockito.stubbing.OngoingStubbing; -import java.io.IOException; -import java.lang.module.Configuration; -import java.lang.module.ModuleDescriptor; -import java.lang.module.ModuleFinder; -import java.lang.module.ModuleReader; -import java.lang.module.ModuleReference; import java.lang.reflect.InvocationTargetException; -import java.net.MalformedURLException; -import java.net.URI; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; import java.util.concurrent.Callable; -import java.util.jar.JarEntry; -import java.util.jar.JarOutputStream; -import java.util.stream.Stream; +import java.util.concurrent.locks.Lock; -import static net.bytebuddy.matcher.ElementMatchers.named; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.hamcrest.core.Is.is; import static org.junit.Assume.assumeThat; +import static org.mockito.moduletest.ModuleUtil.layer; +import static org.mockito.moduletest.ModuleUtil.modularJar; @RunWith(Parameterized.class) public class ModuleHandlingTest { @@ -71,7 +49,7 @@ public void can_define_class_in_open_reading_module() throws Exception { assumeThat(Plugins.getMockMaker() instanceof InlineByteBuddyMockMaker, is(false)); Path jar = modularJar(true, true, true); - ModuleLayer layer = layer(jar, true); + ModuleLayer layer = layer(jar, true, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("sample.MyCallable"); @@ -97,7 +75,7 @@ public void can_define_class_in_open_java_util_module() throws Exception { assumeThat(Plugins.getMockMaker() instanceof InlineByteBuddyMockMaker, is(false)); Path jar = modularJar(true, true, true); - ModuleLayer layer = layer(jar, true); + ModuleLayer layer = layer(jar, true, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("java.util.concurrent.locks.Lock"); @@ -125,7 +103,7 @@ public void inline_mock_maker_can_mock_closed_modules() throws Exception { assumeThat(Plugins.getMockMaker() instanceof InlineByteBuddyMockMaker, is(true)); Path jar = modularJar(false, false, false); - ModuleLayer layer = layer(jar, false); + ModuleLayer layer = layer(jar, false, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("sample.MyCallable"); @@ -145,7 +123,7 @@ public void can_define_class_in_open_reading_private_module() throws Exception { assumeThat(Plugins.getMockMaker() instanceof InlineByteBuddyMockMaker, is(false)); Path jar = modularJar(false, true, true); - ModuleLayer layer = layer(jar, true); + ModuleLayer layer = layer(jar, true, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("sample.MyCallable"); @@ -171,7 +149,7 @@ public void can_define_class_in_open_non_reading_module() throws Exception { assumeThat(Plugins.getMockMaker() instanceof InlineByteBuddyMockMaker, is(false)); Path jar = modularJar(true, true, true); - ModuleLayer layer = layer(jar, false); + ModuleLayer layer = layer(jar, false, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("sample.MyCallable"); @@ -197,7 +175,7 @@ public void can_define_class_in_open_non_reading_non_exporting_module() throws E assumeThat(Plugins.getMockMaker() instanceof InlineByteBuddyMockMaker, is(false)); Path jar = modularJar(true, false, true); - ModuleLayer layer = layer(jar, false); + ModuleLayer layer = layer(jar, false, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("sample.MyCallable"); @@ -223,7 +201,7 @@ public void can_define_class_in_closed_module() throws Exception { assumeThat(Plugins.getMockMaker() instanceof InlineByteBuddyMockMaker, is(false)); Path jar = modularJar(true, true, false); - ModuleLayer layer = layer(jar, false); + ModuleLayer layer = layer(jar, false, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("sample.MyCallable"); @@ -252,7 +230,7 @@ public void cannot_define_class_in_non_opened_non_exported_module_if_lookup_inje assumeThat(!Boolean.getBoolean("org.mockito.internal.noUnsafeInjection") && ClassInjector.UsingReflection.isAvailable(), is(true)); Path jar = modularJar(false, false, false); - ModuleLayer layer = layer(jar, false); + ModuleLayer layer = layer(jar, false, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("sample.MyCallable"); @@ -279,7 +257,7 @@ public void can_define_class_in_non_opened_non_exported_module_if_unsafe_injecti assumeThat(!Boolean.getBoolean("org.mockito.internal.noUnsafeInjection") && ClassInjector.UsingReflection.isAvailable(), is(false)); Path jar = modularJar(false, false, false); - ModuleLayer layer = layer(jar, false); + ModuleLayer layer = layer(jar, false, namedModules); ClassLoader loader = layer.findLoader("mockito.test"); Class type = loader.loadClass("sample.MyCallable"); @@ -298,120 +276,4 @@ public void can_define_class_in_non_opened_non_exported_module_if_unsafe_injecti Thread.currentThread().setContextClassLoader(contextLoader); } } - - private static Path modularJar(boolean isPublic, boolean isExported, boolean isOpened) throws IOException { - Path jar = Files.createTempFile("sample-module", ".jar"); - try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(jar))) { - out.putNextEntry(new JarEntry("module-info.class")); - out.write(moduleInfo(isExported, isOpened)); - out.closeEntry(); - out.putNextEntry(new JarEntry("sample/MyCallable.class")); - out.write(type(isPublic)); - out.closeEntry(); - } - return jar; - } - - private static byte[] type(boolean isPublic) { - return new ByteBuddy() - .subclass(Callable.class) - .name("sample.MyCallable") - .merge(isPublic ? Visibility.PUBLIC : Visibility.PACKAGE_PRIVATE) - .method(named("call")) - .intercept(FixedValue.value("foo")) - .make() - .getBytes(); - } - - private static byte[] moduleInfo(boolean isExported, boolean isOpened) { - ClassWriter classWriter = new ClassWriter(OpenedClassReader.ASM_API); - classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null); - ModuleVisitor mv = classWriter.visitModule("mockito.test", 0, null); - mv.visitRequire("java.base", Opcodes.ACC_MANDATED, null); - mv.visitPackage("sample"); - if (isExported) { - mv.visitExport("sample", 0); - } - if (isOpened) { - mv.visitOpen("sample", 0); - } - mv.visitEnd(); - classWriter.visitEnd(); - return classWriter.toByteArray(); - } - - private ModuleLayer layer(Path jar, boolean canRead) throws MalformedURLException { - Set modules = new HashSet<>(); - modules.add("mockito.test"); - ModuleFinder moduleFinder = ModuleFinder.of(jar); - if (namedModules) { - modules.add("org.mockito"); - modules.add("net.bytebuddy"); - modules.add("net.bytebuddy.agent"); - // We do not list all packages but only roots and packages that interact with the mock where - // we attempt to validate an interaction of two modules. This is of course a bit hacky as those - // libraries would normally be entirely encapsulated in an automatic module with all their classes - // but it is sufficient for the test and saves us a significant amount of code. - moduleFinder = ModuleFinder.compose(moduleFinder, - automaticModule("org.mockito", "org.mockito", "org.mockito.internal.creation.bytebuddy"), - automaticModule("net.bytebuddy", "net.bytebuddy"), - automaticModule("net.bytebuddy.agent", "net.bytebuddy.agent")); - } - Configuration configuration = Configuration.resolve( - moduleFinder, - Collections.singletonList(ModuleLayer.boot().configuration()), - ModuleFinder.of(), - modules - ); - ClassLoader classLoader = new ReplicatingClassLoader(jar); - ModuleLayer.Controller controller = ModuleLayer.defineModules( - configuration, - Collections.singletonList(ModuleLayer.boot()), - module -> classLoader - ); - if (canRead) { - controller.addReads( - controller.layer().findModule("mockito.test").orElseThrow(IllegalStateException::new), - Mockito.class.getModule() - ); - } - return controller.layer(); - } - - private static ModuleFinder automaticModule(String moduleName, String... packages) { - ModuleDescriptor descriptor = ModuleDescriptor.newAutomaticModule(moduleName) - .packages(new HashSet<>(Arrays.asList(packages))) - .build(); - ModuleReference reference = new ModuleReference(descriptor, null) { - @Override - public ModuleReader open() { - return new ModuleReader() { - @Override - public Optional find(String name) { - return Optional.empty(); - } - - @Override - public Stream list() { - return Stream.empty(); - } - - @Override - public void close() { - } - }; - } - }; - return new ModuleFinder() { - @Override - public Optional find(String name) { - return name.equals(moduleName) ? Optional.of(reference) : Optional.empty(); - } - - @Override - public Set findAll() { - return Collections.singleton(reference); - } - }; - } } diff --git a/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleUtil.java b/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleUtil.java new file mode 100644 index 0000000000..49632fd71d --- /dev/null +++ b/subprojects/module-test/src/test/java/org/mockito/moduletest/ModuleUtil.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.moduletest; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.implementation.FixedValue; +import net.bytebuddy.jar.asm.ClassWriter; +import net.bytebuddy.jar.asm.ModuleVisitor; +import net.bytebuddy.jar.asm.Opcodes; +import net.bytebuddy.utility.OpenedClassReader; +import org.mockito.Mockito; + +import java.io.IOException; +import java.lang.module.*; +import java.net.MalformedURLException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.stream.Stream; + +import static net.bytebuddy.matcher.ElementMatchers.named; + +public class ModuleUtil { + + public static Path modularJar(boolean isPublic, boolean isExported, boolean isOpened) throws IOException { + Path jar = Files.createTempFile("sample-module", ".jar"); + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(jar))) { + out.putNextEntry(new JarEntry("module-info.class")); + out.write(moduleInfo(isExported, isOpened)); + out.closeEntry(); + out.putNextEntry(new JarEntry("sample/MyCallable.class")); + out.write(type(isPublic)); + out.closeEntry(); + } + return jar; + } + + private static byte[] type(boolean isPublic) { + return new ByteBuddy() + .subclass(Callable.class) + .name("sample.MyCallable") + .merge(isPublic ? Visibility.PUBLIC : Visibility.PACKAGE_PRIVATE) + .method(named("call")) + .intercept(FixedValue.value("foo")) + .make() + .getBytes(); + } + + private static byte[] moduleInfo(boolean isExported, boolean isOpened) { + ClassWriter classWriter = new ClassWriter(OpenedClassReader.ASM_API); + classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null); + ModuleVisitor mv = classWriter.visitModule("mockito.test", 0, null); + mv.visitRequire("java.base", Opcodes.ACC_MANDATED, null); + mv.visitPackage("sample"); + if (isExported) { + mv.visitExport("sample", 0); + } + if (isOpened) { + mv.visitOpen("sample", 0); + } + mv.visitEnd(); + classWriter.visitEnd(); + return classWriter.toByteArray(); + } + + public static ModuleLayer layer(Path jar, boolean canRead, boolean namedModules) throws MalformedURLException { + Set modules = new HashSet<>(); + modules.add("mockito.test"); + ModuleFinder moduleFinder = ModuleFinder.of(jar); + if (namedModules) { + modules.add("org.mockito"); + modules.add("net.bytebuddy"); + modules.add("net.bytebuddy.agent"); + // We do not list all packages but only roots and packages that interact with the mock where + // we attempt to validate an interaction of two modules. This is of course a bit hacky as those + // libraries would normally be entirely encapsulated in an automatic module with all their classes + // but it is sufficient for the test and saves us a significant amount of code. + moduleFinder = ModuleFinder.compose(moduleFinder, + automaticModule("org.mockito", "org.mockito", "org.mockito.internal.creation.bytebuddy"), + automaticModule("net.bytebuddy", "net.bytebuddy"), + automaticModule("net.bytebuddy.agent", "net.bytebuddy.agent")); + } + Configuration configuration = Configuration.resolve( + moduleFinder, + Collections.singletonList(ModuleLayer.boot().configuration()), + ModuleFinder.of(), + modules + ); + ClassLoader classLoader = new ReplicatingClassLoader(jar); + ModuleLayer.Controller controller = ModuleLayer.defineModules( + configuration, + Collections.singletonList(ModuleLayer.boot()), + module -> classLoader + ); + if (canRead) { + controller.addReads( + controller.layer().findModule("mockito.test").orElseThrow(IllegalStateException::new), + Mockito.class.getModule() + ); + } + return controller.layer(); + } + + private static ModuleFinder automaticModule(String moduleName, String... packages) { + ModuleDescriptor descriptor = ModuleDescriptor.newAutomaticModule(moduleName) + .packages(new HashSet<>(Arrays.asList(packages))) + .build(); + ModuleReference reference = new ModuleReference(descriptor, null) { + @Override + public ModuleReader open() { + return new ModuleReader() { + @Override + public Optional find(String name) { + return Optional.empty(); + } + + @Override + public Stream list() { + return Stream.empty(); + } + + @Override + public void close() { + } + }; + } + }; + return new ModuleFinder() { + @Override + public Optional find(String name) { + return name.equals(moduleName) ? Optional.of(reference) : Optional.empty(); + } + + @Override + public Set findAll() { + return Collections.singleton(reference); + } + }; + } +} diff --git a/subprojects/module-test/src/test/java/org/mockito/moduletest/ReplicatingClassLoader.java b/subprojects/module-test/src/test/java/org/mockito/moduletest/ReplicatingClassLoader.java index 9ee006c880..c9cf0a32d2 100644 --- a/subprojects/module-test/src/test/java/org/mockito/moduletest/ReplicatingClassLoader.java +++ b/subprojects/module-test/src/test/java/org/mockito/moduletest/ReplicatingClassLoader.java @@ -48,4 +48,9 @@ public Class loadClass(String name) throws ClassNotFoundException { } } } + + @Override + protected URL findResource(String moduleName, String name) { + return Mockito.class.getResource("/" + name); + } }