From 4323a1770f9426790ad5c3499e32a7f73e415992 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Tue, 4 Apr 2023 12:46:37 +0200 Subject: [PATCH 1/2] ArC: organize version properties in root POM --- independent-projects/arc/pom.xml | 44 +++++++++----------------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index 7e3bac57e7808..1503cf5669537 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -35,31 +35,32 @@ - 1.0.0 - 5.0.0 UTF-8 11 11 11 - + + 4.0.1 + 2.1.1 + 3.0.0 2.0.1 + + 1.6.0.Final 3.0.5 + 3.5.0.Final + 2.1.0 + + 3.24.2 5.9.2 1.8.10 1.6.4 - 3.8.8 - 3.24.2 - 3.5.0.Final - 2.1.1 - 1.6.0.Final - 3.0.0 - 2.1.0 + 1.7.0.Alpha14 2.0.1 4.0.9 4.13.2 - + 3.11.0 3.2.1 3.0.0 @@ -140,27 +141,6 @@ - - org.apache.maven - maven-plugin-api - ${version.maven} - provided - - - - org.apache.maven.plugin-tools - maven-plugin-annotations - ${version.maven} - provided - - - - org.apache.maven - maven-core - ${version.maven} - provided - - org.jboss.logging jboss-logging From 503ad0d9b12e93f2bd2bf0a5cdc4070ca70c9225 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Tue, 4 Apr 2023 14:59:44 +0200 Subject: [PATCH 2/2] ArC: fix spying on intercepted beans When instantiating a `Subclass` for a bean with intercepted methods, all `InterceptedMethodMetadata` for all intercepted methods are also instantiated. These metadata used to hold a `Function` instance that takes an `InvocationContext` and calls the original method after all interceptors have executed. The `Function` instance used to remember the `this` instance on which the method should be invoked. This is incompatible with the way how Mockito implements spying on objects. To create a spy, Mockito takes an object, obtains its class, creates a subclass, and creates a copy of the original object, except it's an instance of the subclass. When calling an intercepted method on the spy object, the `Function` instance mentioned above diverts the ultimate method call to the object it remembered, instead of using the target object stored in the `InvocationContext`. This commit changes the `InterceptedMethodMetadata` to no longer hold a `Function`; it holds a `BiFunction` instead. The `BiFunction` is completely stateless. Instead, the caller of the `BiFunction` must pass not only the `InvocationContext`, but also the target object. This way, if an intercepted object is spied upon, all intercepted methods are eventually called correctly on the spy, instead of the original instance. --- .../InterceptedStaticMethodsProcessor.java | 7 +- independent-projects/arc/pom.xml | 1 + .../arc/processor/MethodDescriptors.java | 5 +- .../arc/processor/SubclassGenerator.java | 10 ++- .../impl/AroundInvokeInvocationContext.java | 2 +- .../arc/impl/InterceptedMethodMetadata.java | 6 +- independent-projects/arc/tests/pom.xml | 7 ++ .../mocking/SpyOnInterceptedBeanTest.java | 75 +++++++++++++++++++ 8 files changed, 98 insertions(+), 15 deletions(-) create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/mocking/SpyOnInterceptedBeanTest.java diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/staticmethods/InterceptedStaticMethodsProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/staticmethods/InterceptedStaticMethodsProcessor.java index e570438c545ae..568036a05db26 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/staticmethods/InterceptedStaticMethodsProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/staticmethods/InterceptedStaticMethodsProcessor.java @@ -17,7 +17,6 @@ import java.util.Map.Entry; import java.util.Set; import java.util.function.BiFunction; -import java.util.function.Function; import java.util.stream.Collectors; import jakarta.enterprise.context.spi.Contextual; @@ -371,8 +370,8 @@ private ResultHandle createInterceptor(ResultHandle interceptorBean, BytecodeCre private ResultHandle createForwardingFunction(MethodCreator init, ClassInfo target, MethodInfo method) { // Forwarding function - // Function forward = ctx -> Foo.interceptMe_original((java.lang.String)ctx.getParameters()[0]) - FunctionCreator func = init.createFunction(Function.class); + // BiFunction forward = (ignored, ctx) -> Foo.interceptMe_original((java.lang.String)ctx.getParameters()[0]) + FunctionCreator func = init.createFunction(BiFunction.class); BytecodeCreator funcBytecode = func.getBytecode(); List paramTypes = method.parameterTypes(); ResultHandle[] paramHandles; @@ -382,7 +381,7 @@ private ResultHandle createForwardingFunction(MethodCreator init, ClassInfo targ params = new String[0]; } else { paramHandles = new ResultHandle[paramTypes.size()]; - ResultHandle ctxHandle = funcBytecode.getMethodParam(0); + ResultHandle ctxHandle = funcBytecode.getMethodParam(1); ResultHandle ctxParamsHandle = funcBytecode.invokeInterfaceMethod( MethodDescriptor.ofMethod(InvocationContext.class, "getParameters", Object[].class), ctxHandle); diff --git a/independent-projects/arc/pom.xml b/independent-projects/arc/pom.xml index 1503cf5669537..f472eb49de498 100644 --- a/independent-projects/arc/pom.xml +++ b/independent-projects/arc/pom.xml @@ -55,6 +55,7 @@ 5.9.2 1.8.10 1.6.4 + 5.2.0 1.7.0.Alpha14 2.0.1 diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java index 92927d1b7bc20..c1e33488f6ef7 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/MethodDescriptors.java @@ -9,7 +9,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Function; +import java.util.function.BiFunction; import java.util.function.Supplier; import jakarta.enterprise.context.spi.Context; @@ -231,8 +231,7 @@ public final class MethodDescriptors { String.class); public static final MethodDescriptor INTERCEPTED_METHOD_METADATA_CONSTRUCTOR = MethodDescriptor.ofConstructor( - InterceptedMethodMetadata.class, - List.class, Method.class, Set.class, Function.class); + InterceptedMethodMetadata.class, List.class, Method.class, Set.class, BiFunction.class); public static final MethodDescriptor CREATIONAL_CTX_HAS_DEPENDENT_INSTANCES = MethodDescriptor.ofMethod( CreationalContextImpl.class, diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/SubclassGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/SubclassGenerator.java index 4c50b33415d44..ea4aa6ab8b4fb 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/SubclassGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/SubclassGenerator.java @@ -20,6 +20,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -403,10 +404,11 @@ public String apply(List keys) { } // Instantiate the forwarding function - // Function forward = ctx -> super.foo((java.lang.String)ctx.getParameters()[0]) - FunctionCreator func = initMetadataMethod.createFunction(Function.class); + // BiFunction forward = (target, ctx) -> target.foo$$superforward((java.lang.String)ctx.getParameters()[0]) + FunctionCreator func = initMetadataMethod.createFunction(BiFunction.class); BytecodeCreator funcBytecode = func.getBytecode(); - ResultHandle ctxHandle = funcBytecode.getMethodParam(0); + ResultHandle targetHandle = funcBytecode.getMethodParam(0); + ResultHandle ctxHandle = funcBytecode.getMethodParam(1); ResultHandle[] superParamHandles; if (parameters.isEmpty()) { superParamHandles = new ResultHandle[0]; @@ -438,7 +440,7 @@ public String apply(List keys) { .returnValue(funcBytecode.invokeVirtualMethod(virtualMethodDescriptor, funDecoratorInstance, superParamHandles)); } else { - ResultHandle superResult = funcBytecode.invokeVirtualMethod(forwardDescriptor, initMetadataMethod.getThis(), + ResultHandle superResult = funcBytecode.invokeVirtualMethod(forwardDescriptor, targetHandle, superParamHandles); funcBytecode.returnValue(superResult != null ? superResult : funcBytecode.loadNull()); } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AroundInvokeInvocationContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AroundInvokeInvocationContext.java index 4a0fd83943795..eea2a771e2728 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AroundInvokeInvocationContext.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AroundInvokeInvocationContext.java @@ -71,7 +71,7 @@ private Object proceed(int currentPosition) throws Exception { .invoke(new NextAroundInvokeInvocationContext(currentPosition + 1, parameters)); } else { // Invoke the target method - return metadata.aroundInvokeForward.apply(this); + return metadata.aroundInvokeForward.apply(target, this); } } catch (InvocationTargetException e) { Throwable cause = e.getCause(); diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InterceptedMethodMetadata.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InterceptedMethodMetadata.java index fe59b437c7636..005b7b8ab9f40 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InterceptedMethodMetadata.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InterceptedMethodMetadata.java @@ -4,7 +4,7 @@ import java.lang.reflect.Method; import java.util.List; import java.util.Set; -import java.util.function.Function; +import java.util.function.BiFunction; import jakarta.interceptor.InvocationContext; @@ -16,10 +16,10 @@ public class InterceptedMethodMetadata { public final List chain; public final Method method; public final Set bindings; - public final Function aroundInvokeForward; + public final BiFunction aroundInvokeForward; public InterceptedMethodMetadata(List chain, Method method, Set bindings, - Function aroundInvokeForward) { + BiFunction aroundInvokeForward) { this.chain = chain; this.method = method; this.bindings = bindings; diff --git a/independent-projects/arc/tests/pom.xml b/independent-projects/arc/tests/pom.xml index 59013268636db..88eaba2d14366 100644 --- a/independent-projects/arc/tests/pom.xml +++ b/independent-projects/arc/tests/pom.xml @@ -35,6 +35,13 @@ test + + org.mockito + mockito-core + ${version.mockito} + test + + jakarta.persistence jakarta.persistence-api diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/mocking/SpyOnInterceptedBeanTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/mocking/SpyOnInterceptedBeanTest.java new file mode 100644 index 0000000000000..36df13626bf25 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/mocking/SpyOnInterceptedBeanTest.java @@ -0,0 +1,75 @@ +package io.quarkus.arc.test.mocking; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.annotation.Priority; +import jakarta.inject.Singleton; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InterceptorBinding; +import jakarta.interceptor.InvocationContext; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.test.ArcTestContainer; + +public class SpyOnInterceptedBeanTest { + @RegisterExtension + public ArcTestContainer container = new ArcTestContainer(MyBean.class, MyInterceptorBinding.class, MyInterceptor.class); + + @Test + public void test() { + MyBean bean = Arc.container().instance(MyBean.class).get(); + + MyBean spy = Mockito.spy(bean); + Mockito.when(spy.getValue()).thenReturn("quux"); + + assertEquals("intercepted: intercepted: quux42", spy.doSomething(42)); + Mockito.verify(spy).doSomething(Mockito.anyInt()); + Mockito.verify(spy).doSomethingElse(); + Mockito.verify(spy).getValue(); + } + + @Singleton + static class MyBean { + @MyInterceptorBinding + String doSomething(int param) { + return doSomethingElse() + param; + } + + @MyInterceptorBinding + String doSomethingElse() { + return getValue(); + } + + String getValue() { + return "foobar"; + } + } + + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @InterceptorBinding + public @interface MyInterceptorBinding { + } + + @MyInterceptorBinding + @Interceptor + @Priority(1) + public static class MyInterceptor { + @AroundInvoke + Object aroundInvoke(InvocationContext ctx) throws Exception { + return "intercepted: " + ctx.proceed(); + } + } +}