@@ -1589,6 +1590,19 @@
* Due to the defined scope of the mocked construction, object construction returns to its original behavior once the scope is
* released. To define mock behavior and to verify method invocations, use the MockedConstruction that is returned.
*
+ *
+ * The JVM offers the {@link java.lang.reflect.Proxy} facility for creating dynamic proxies of interface types. For most applications, Mockito
+ * must be capable of mocking classes as supported by the default mock maker, or even final classes, as supported by the inline mock maker. To
+ * create such mocks, Mockito requires to setup diverse JVM facilities and must apply code generation. If only interfaces are supposed to be
+ * mocked, one can however choose to use a {@link org.mockito.internal.creation.proxy.ProxyMockMaker} which avoids diverse overhead but limits
+ * mocking to interfaces.
+ *
+ * This mock maker can be activated explicitly by the mockito extension mechanism, just create in the classpath a file
+ * /mockito-extensions/org.mockito.plugins.MockMaker containing the value mock-maker-proxy.
+ *
+ *
*/
@CheckReturnValue
@SuppressWarnings("unchecked")
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 d06a8fb620..559a90d592 100644
--- a/src/main/java/org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java
+++ b/src/main/java/org/mockito/internal/configuration/plugins/DefaultMockitoPlugins.java
@@ -21,6 +21,7 @@ class DefaultMockitoPlugins implements MockitoPlugins {
private static final Map DEFAULT_PLUGINS = new HashMap<>();
static final String INLINE_ALIAS = "mock-maker-inline";
+ static final String PROXY_ALIAS = "mock-maker-proxy";
static final String MODULE_ALIAS = "member-accessor-module";
static {
@@ -40,6 +41,7 @@ class DefaultMockitoPlugins implements MockitoPlugins {
"org.mockito.internal.configuration.InjectingAnnotationEngine");
DEFAULT_PLUGINS.put(
INLINE_ALIAS, "org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker");
+ DEFAULT_PLUGINS.put(PROXY_ALIAS, "org.mockito.internal.creation.proxy.ProxyMockMaker");
DEFAULT_PLUGINS.put(
MockitoLogger.class.getName(), "org.mockito.internal.util.ConsoleMockitoLogger");
DEFAULT_PLUGINS.put(
diff --git a/src/main/java/org/mockito/internal/creation/proxy/ProxyMockMaker.java b/src/main/java/org/mockito/internal/creation/proxy/ProxyMockMaker.java
new file mode 100644
index 0000000000..31a13124c4
--- /dev/null
+++ b/src/main/java/org/mockito/internal/creation/proxy/ProxyMockMaker.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (c) 2021 Mockito contributors
+ * This program is made available under the terms of the MIT License.
+ */
+package org.mockito.internal.creation.proxy;
+
+import org.mockito.exceptions.base.MockitoException;
+import org.mockito.internal.debugging.LocationImpl;
+import org.mockito.internal.invocation.RealMethod;
+import org.mockito.internal.invocation.SerializableMethod;
+import org.mockito.internal.util.Platform;
+import org.mockito.invocation.MockHandler;
+import org.mockito.mock.MockCreationSettings;
+import org.mockito.plugins.MockMaker;
+
+import java.lang.reflect.*;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.mockito.internal.invocation.DefaultInvocationFactory.createInvocation;
+import static org.mockito.internal.util.StringUtil.join;
+
+/**
+ * A mock maker that is using the {@link Proxy} utility and is therefore only capable of mocking interfaces but
+ * does not rely on manual byte code generation but only uses official and public Java API.
+ */
+public class ProxyMockMaker implements MockMaker {
+
+ private final Method invokeDefault;
+
+ public ProxyMockMaker() {
+ Method m;
+ try {
+ m =
+ InvocationHandler.class.getMethod(
+ "invokeDefault", Object.class, Method.class, Object[].class);
+ } catch (NoSuchMethodException ignored) {
+ m = null;
+ }
+ invokeDefault = m;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public T createMock(MockCreationSettings settings, MockHandler handler) {
+ Class>[] ifaces = new Class>[settings.getExtraInterfaces().size() + 1];
+ ifaces[0] = settings.getTypeToMock();
+ int index = 1;
+ for (Class> iface : settings.getExtraInterfaces()) {
+ ifaces[index++] = iface;
+ }
+ return (T)
+ Proxy.newProxyInstance(
+ settings.getTypeToMock().getClassLoader(),
+ ifaces,
+ new MockInvocationHandler(handler, settings));
+ }
+
+ @Override
+ public MockHandler getHandler(Object mock) {
+ if (!Proxy.isProxyClass(mock.getClass())) {
+ return null;
+ }
+ InvocationHandler handler = Proxy.getInvocationHandler(mock);
+ if (!(handler instanceof MockInvocationHandler)) {
+ return null;
+ }
+ return ((MockInvocationHandler) handler).handler.get();
+ }
+
+ @Override
+ public void resetMock(Object mock, MockHandler newHandler, MockCreationSettings settings) {
+ ((MockInvocationHandler) Proxy.getInvocationHandler(mock)).handler.set(newHandler);
+ }
+
+ @Override
+ public TypeMockability isTypeMockable(Class> type) {
+ return new TypeMockability() {
+ @Override
+ public boolean mockable() {
+ return type.isInterface();
+ }
+
+ @Override
+ public String nonMockableReason() {
+ return mockable() ? "" : "non-interface";
+ }
+ };
+ }
+
+ private class MockInvocationHandler implements InvocationHandler {
+
+ private final AtomicReference> handler;
+
+ private final MockCreationSettings> settings;
+
+ private MockInvocationHandler(MockHandler> handler, MockCreationSettings> settings) {
+ this.handler = new AtomicReference<>(handler);
+ this.settings = settings;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ if (method.getDeclaringClass() == Object.class) {
+ switch (method.getName()) {
+ case "hashCode":
+ return System.identityHashCode(proxy);
+ case "equals":
+ return proxy == args[0];
+ case "toString":
+ return "";
+ default:
+ throw new MockitoException(
+ join(
+ "Unexpected overridable method of Object class found",
+ "",
+ "The method "
+ + method
+ + " was not expected to be declared. Either your JVM build offers "
+ + "non-official API or the current functionality is not supported",
+ Platform.describe()));
+ }
+ }
+ RealMethod realMethod;
+ if (invokeDefault == null || Modifier.isAbstract(method.getModifiers())) {
+ realMethod = RealMethod.IsIllegal.INSTANCE;
+ } else {
+ realMethod = new RealDefaultMethod(proxy, method, args);
+ }
+ return handler.get()
+ .handle(
+ createInvocation(
+ proxy, method, args, realMethod, settings, new LocationImpl()));
+ }
+ }
+
+ private class RealDefaultMethod implements RealMethod {
+
+ private final Object proxy;
+ private final SerializableMethod serializableMethod;
+ private final Object[] args;
+
+ private RealDefaultMethod(Object proxy, Method method, Object[] args) {
+ this.proxy = proxy;
+ this.serializableMethod = new SerializableMethod(method);
+ this.args = args;
+ }
+
+ @Override
+ public boolean isInvokable() {
+ return true;
+ }
+
+ @Override
+ public Object invoke() throws Throwable {
+ try {
+ return invokeDefault.invoke(null, proxy, serializableMethod.getJavaMethod(), args);
+ } catch (InvocationTargetException e) {
+ throw e.getTargetException();
+ } catch (IllegalAccessException | IllegalArgumentException e) {
+ throw new MockitoException(
+ join(
+ "Failed to access default method or invoked method with illegal arguments",
+ "",
+ "Method "
+ + serializableMethod.getJavaMethod()
+ + " could not be delegated, this is not supposed to happen",
+ Platform.describe()),
+ e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/mockito/internal/creation/proxy/package-info.java b/src/main/java/org/mockito/internal/creation/proxy/package-info.java
new file mode 100644
index 0000000000..0649d3ba57
--- /dev/null
+++ b/src/main/java/org/mockito/internal/creation/proxy/package-info.java
@@ -0,0 +1,9 @@
+/*
+ * Copyright (c) 2007 Mockito contributors
+ * This program is made available under the terms of the MIT License.
+ */
+
+/**
+ * Mock makers based on the {@link java.lang.reflect.Proxy} utility.
+ */
+package org.mockito.internal.creation.proxy;
diff --git a/src/test/java/org/mockito/internal/creation/AbstractMockMakerTest.java b/src/test/java/org/mockito/internal/creation/AbstractMockMakerTest.java
new file mode 100644
index 0000000000..d59477829a
--- /dev/null
+++ b/src/test/java/org/mockito/internal/creation/AbstractMockMakerTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2017 Mockito contributors
+ * This program is made available under the terms of the MIT License.
+ */
+package org.mockito.internal.creation;
+
+import org.junit.Test;
+import org.mockito.internal.handler.MockHandlerImpl;
+import org.mockito.internal.stubbing.answers.CallsRealMethods;
+import org.mockito.invocation.Invocation;
+import org.mockito.invocation.InvocationContainer;
+import org.mockito.invocation.MockHandler;
+import org.mockito.mock.MockCreationSettings;
+import org.mockito.mock.SerializableMode;
+import org.mockito.plugins.MockMaker;
+import org.mockito.stubbing.Answer;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public abstract class AbstractMockMakerTest {
+
+ protected final MM mockMaker;
+
+ private final Class target;
+
+ protected AbstractMockMakerTest(MM mockMaker, Class target) {
+ this.mockMaker = mockMaker;
+ this.target = target;
+ }
+
+ @Test
+ public void should_mocks_have_different_interceptors() throws Exception {
+ C mockOne = mockMaker.createMock(settingsFor(target), dummyHandler());
+ C mockTwo = mockMaker.createMock(settingsFor(target), dummyHandler());
+
+ MockHandler handlerOne = mockMaker.getHandler(mockOne);
+ MockHandler handlerTwo = mockMaker.getHandler(mockTwo);
+
+ assertThat(handlerOne).isNotSameAs(handlerTwo);
+ }
+
+ @Test
+ public void should_reset_mock_and_set_new_handler() throws Throwable {
+ MockCreationSettings settings = settingsWithSuperCall(target);
+ C proxy = mockMaker.createMock(settings, new MockHandlerImpl(settings));
+
+ MockHandler handler = new MockHandlerImpl(settings);
+ mockMaker.resetMock(proxy, handler, settings);
+ assertThat(mockMaker.getHandler(proxy)).isSameAs(handler);
+ }
+
+ protected static MockCreationSettings settingsFor(
+ Class type, Class>... extraInterfaces) {
+ MockSettingsImpl mockSettings = new MockSettingsImpl();
+ mockSettings.setTypeToMock(type);
+ if (extraInterfaces.length > 0) mockSettings.extraInterfaces(extraInterfaces);
+ return mockSettings;
+ }
+
+ protected static MockCreationSettings serializableSettingsFor(
+ Class type, SerializableMode serializableMode) {
+ MockSettingsImpl mockSettings = new MockSettingsImpl();
+ mockSettings.serializable(serializableMode);
+ mockSettings.setTypeToMock(type);
+ return mockSettings;
+ }
+
+ protected static MockCreationSettings settingsWithConstructorFor(Class type) {
+ MockSettingsImpl mockSettings = new MockSettingsImpl();
+ mockSettings.setTypeToMock(type);
+ return mockSettings;
+ }
+
+ protected static MockCreationSettings settingsWithSuperCall(Class type) {
+ MockSettingsImpl mockSettings = new MockSettingsImpl();
+ mockSettings.setTypeToMock(type);
+ mockSettings.defaultAnswer(new CallsRealMethods());
+ return mockSettings;
+ }
+
+ protected static MockHandler dummyHandler() {
+ return new DummyMockHandler();
+ }
+
+ private static class DummyMockHandler implements MockHandler