From 527a44ef4e1f787ebfed4b3ce3431592f426bb76 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 18 Apr 2023 09:10:28 +0200 Subject: [PATCH 1/5] ArC - support multiple interceptor methods in a class hierarchy --- .../arc/processor/InterceptorGenerator.java | 182 +++++++++++----- .../arc/processor/InterceptorInfo.java | 197 ++++++++++++++---- .../arc/processor/MethodDescriptors.java | 8 + .../arc/impl/AbstractInvocationContext.java | 2 +- .../arc/impl/InnerInvocationContext.java | 155 ++++++++++++++ .../quarkus/arc/impl/InvocationContexts.java | 14 ++ .../arc/impl/SuperclassInvocationContext.java | 58 ++++++ .../inheritance/hierarchy/AlphaBinding.java | 19 ++ .../hierarchy/AlphaInterceptor.java | 43 ++++ .../inheritance/hierarchy/Bravo.java | 36 ++++ .../inheritance/hierarchy/Charlie.java | 34 +++ ...rceptorMethodDeclaredOnSuperclassTest.java | 69 ++++++ .../SuperclassInterceptorMethodsTest.java | 54 +++++ 13 files changed, 776 insertions(+), 95 deletions(-) create mode 100644 independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InnerInvocationContext.java create mode 100644 independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/SuperclassInvocationContext.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/AlphaBinding.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/AlphaInterceptor.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/Bravo.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/Charlie.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/MultipleInterceptorMethodDeclaredOnSuperclassTest.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/SuperclassInterceptorMethodsTest.java diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorGenerator.java index 0ce5a09586659..b710f6633ac99 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorGenerator.java @@ -5,12 +5,15 @@ import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Predicate; import java.util.function.Supplier; @@ -34,6 +37,8 @@ import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.FieldCreator; import io.quarkus.gizmo.FieldDescriptor; +import io.quarkus.gizmo.FunctionCreator; +import io.quarkus.gizmo.Gizmo; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; @@ -111,7 +116,7 @@ Collection generate(InterceptorInfo interceptor) { createProviderFields(interceptorCreator, interceptor, injectionPointToProviderField, Collections.emptyMap(), Collections.emptyMap()); createConstructor(classOutput, interceptorCreator, interceptor, injectionPointToProviderField, - bindings.getFieldDescriptor(), reflectionRegistration); + bindings.getFieldDescriptor(), reflectionRegistration, isApplicationClass, providerType); implementGetIdentifier(interceptor, interceptorCreator); implementSupplierGet(interceptorCreator); @@ -139,8 +144,8 @@ Collection generate(InterceptorInfo interceptor) { } protected void createConstructor(ClassOutput classOutput, ClassCreator creator, InterceptorInfo interceptor, - Map injectionPointToProviderField, - FieldDescriptor bindings, ReflectionRegistration reflectionRegistration) { + Map injectionPointToProviderField, FieldDescriptor bindings, + ReflectionRegistration reflectionRegistration, boolean isApplicationClass, ProviderType providerType) { MethodCreator constructor = initConstructor(classOutput, creator, interceptor, injectionPointToProviderField, Collections.emptyMap(), Collections.emptyMap(), annotationLiterals, reflectionRegistration); @@ -156,9 +161,41 @@ protected void createConstructor(ClassOutput classOutput, ClassCreator creator, annotationLiterals.create(constructor, bindingClass, bindingAnnotation)); } constructor.writeInstanceField(bindings, constructor.getThis(), bindingsHandle); + + // Initialize a list of BiFunction for each interception type if multiple interceptor methods are declared in a hierarchy + initInterceptorMethodsField(creator, constructor, InterceptionType.AROUND_INVOKE, interceptor.getAroundInvokes(), + providerType.className(), isApplicationClass); + initInterceptorMethodsField(creator, constructor, InterceptionType.AROUND_CONSTRUCT, interceptor.getAroundConstructs(), + providerType.className(), isApplicationClass); + initInterceptorMethodsField(creator, constructor, InterceptionType.POST_CONSTRUCT, interceptor.getPostConstructs(), + providerType.className(), isApplicationClass); + initInterceptorMethodsField(creator, constructor, InterceptionType.PRE_DESTROY, interceptor.getPreDestroys(), + providerType.className(), isApplicationClass); + constructor.returnValue(null); } + private void initInterceptorMethodsField(ClassCreator creator, MethodCreator constructor, InterceptionType interceptionType, + List methods, String interceptorClass, boolean isApplicationClass) { + if (methods.size() < 2) { + return; + } + FieldCreator field = creator.getFieldCreator(interceptorMethodsField(interceptionType), List.class) + .setModifiers(ACC_PRIVATE); + ResultHandle methodsList = constructor.newInstance(MethodDescriptor.ofConstructor(ArrayList.class)); + for (MethodInfo method : methods) { + // BiFunction + FunctionCreator fun = constructor.createFunction(BiFunction.class); + BytecodeCreator funBytecode = fun.getBytecode(); + ResultHandle ret = invokeInterceptorMethod(funBytecode, interceptorClass, method, + interceptionType, isApplicationClass, funBytecode.getMethodParam(1), + funBytecode.getMethodParam(0)); + funBytecode.returnValue(interceptionType == InterceptionType.AROUND_INVOKE ? ret : funBytecode.loadNull()); + constructor.invokeInterfaceMethod(MethodDescriptors.LIST_ADD, methodsList, fun.getInstance()); + } + constructor.writeInstanceField(field.getFieldDescriptor(), constructor.getThis(), methodsList); + } + /** * * @see InjectableInterceptor#getInterceptorBindings() @@ -214,64 +251,105 @@ protected void implementIntercept(ClassCreator creator, InterceptorInfo intercep .getMethodCreator("intercept", Object.class, InterceptionType.class, Object.class, InvocationContext.class) .setModifiers(ACC_PUBLIC).addException(Exception.class); - addIntercept(intercept, interceptor.getAroundInvoke(), InterceptionType.AROUND_INVOKE, providerType, + addIntercept(creator, intercept, interceptor.getAroundInvokes(), InterceptionType.AROUND_INVOKE, providerType, reflectionRegistration, isApplicationClass); - addIntercept(intercept, interceptor.getPostConstruct(), InterceptionType.POST_CONSTRUCT, providerType, + addIntercept(creator, intercept, interceptor.getPostConstructs(), InterceptionType.POST_CONSTRUCT, providerType, reflectionRegistration, isApplicationClass); - addIntercept(intercept, interceptor.getPreDestroy(), InterceptionType.PRE_DESTROY, providerType, + addIntercept(creator, intercept, interceptor.getPreDestroys(), InterceptionType.PRE_DESTROY, providerType, reflectionRegistration, isApplicationClass); - addIntercept(intercept, interceptor.getAroundConstruct(), InterceptionType.AROUND_CONSTRUCT, providerType, + addIntercept(creator, intercept, interceptor.getAroundConstructs(), InterceptionType.AROUND_CONSTRUCT, providerType, reflectionRegistration, isApplicationClass); intercept.returnValue(intercept.loadNull()); } - private void addIntercept(MethodCreator intercept, MethodInfo interceptorMethod, InterceptionType interceptionType, - ProviderType providerType, - ReflectionRegistration reflectionRegistration, boolean isApplicationClass) { - if (interceptorMethod != null) { - ResultHandle enumValue = intercept - .readStaticField(FieldDescriptor.of(InterceptionType.class.getName(), interceptionType.name(), - InterceptionType.class.getName())); - BranchResult result = intercept.ifNonZero( - intercept.invokeVirtualMethod(MethodDescriptors.OBJECT_EQUALS, enumValue, intercept.getMethodParam(0))); - BytecodeCreator trueBranch = result.trueBranch(); - Class retType = null; - if (InterceptionType.AROUND_INVOKE.equals(interceptionType)) { - retType = Object.class; - } else { - // @PostConstruct, @PreDestroy, @AroundConstruct - retType = interceptorMethod.returnType().kind().equals(Type.Kind.VOID) ? void.class : Object.class; - } - ResultHandle ret; - // Check if interceptor method uses InvocationContext or ArcInvocationContext - Class invocationContextClass; - if (interceptorMethod.parameterType(0).name().equals(DotNames.INVOCATION_CONTEXT)) { - invocationContextClass = InvocationContext.class; - } else { - invocationContextClass = ArcInvocationContext.class; - } - if (Modifier.isPrivate(interceptorMethod.flags())) { - privateMembers.add(isApplicationClass, - String.format("Interceptor method %s#%s()", interceptorMethod.declaringClass().name(), - interceptorMethod.name())); - // Use reflection fallback - ResultHandle paramTypesArray = trueBranch.newArray(Class.class, trueBranch.load(1)); - trueBranch.writeArrayValue(paramTypesArray, 0, trueBranch.loadClass(invocationContextClass)); - ResultHandle argsArray = trueBranch.newArray(Object.class, trueBranch.load(1)); - trueBranch.writeArrayValue(argsArray, 0, intercept.getMethodParam(2)); - reflectionRegistration.registerMethod(interceptorMethod); - ret = trueBranch.invokeStaticMethod(MethodDescriptors.REFLECTIONS_INVOKE_METHOD, - trueBranch.loadClass(interceptorMethod.declaringClass() - .name() - .toString()), - trueBranch.load(interceptorMethod.name()), paramTypesArray, intercept.getMethodParam(1), argsArray); + private void addIntercept(ClassCreator creator, MethodCreator intercept, List interceptorMethods, + InterceptionType interceptionType, ProviderType providerType, ReflectionRegistration reflectionRegistration, + boolean isApplicationClass) { + if (interceptorMethods.isEmpty()) { + return; + } + BranchResult result = intercept + .ifTrue(Gizmo.equals(intercept, intercept.load(interceptionType), intercept.getMethodParam(0))); + BytecodeCreator trueBranch = result.trueBranch(); + ResultHandle ret; + if (interceptorMethods.size() == 1) { + MethodInfo interceptorMethod = interceptorMethods.get(0); + ret = invokeInterceptorMethod(trueBranch, providerType.className(), interceptorMethod, + interceptionType, isApplicationClass, trueBranch.getMethodParam(2), trueBranch.getMethodParam(1)); + } else { + // Multiple interceptor methods found in the hierarchy + ResultHandle methodList = trueBranch.readInstanceField( + FieldDescriptor.of(creator.getClassName(), interceptorMethodsField(interceptionType), List.class), + trueBranch.getThis()); + ResultHandle params; + if (interceptionType == InterceptionType.AROUND_INVOKE) { + params = trueBranch.invokeInterfaceMethod(MethodDescriptors.INVOCATION_CONTEXT_GET_PARAMETERS, + trueBranch.getMethodParam(2)); } else { - ret = trueBranch.invokeVirtualMethod( - MethodDescriptor.ofMethod(providerType.className(), interceptorMethod.name(), retType, - invocationContextClass), - intercept.getMethodParam(1), intercept.getMethodParam(2)); + params = trueBranch.loadNull(); } - trueBranch.returnValue(InterceptionType.AROUND_INVOKE.equals(interceptionType) ? ret : trueBranch.loadNull()); + ret = trueBranch.invokeStaticMethod(MethodDescriptors.INVOCATION_CONTEXTS_PERFORM_SUPERCLASS, + trueBranch.getMethodParam(2), methodList, + trueBranch.getMethodParam(1), params); + + } + trueBranch.returnValue(InterceptionType.AROUND_INVOKE.equals(interceptionType) ? ret : trueBranch.loadNull()); + } + + private String interceptorMethodsField(InterceptionType interceptionType) { + switch (interceptionType) { + case AROUND_INVOKE: + return "aroundInvokes"; + case AROUND_CONSTRUCT: + return "aroundConstructs"; + case POST_CONSTRUCT: + return "postConstructs"; + case PRE_DESTROY: + return "preDestroys"; + default: + throw new IllegalArgumentException("Unsupported interception type: " + interceptionType); + } + } + + private ResultHandle invokeInterceptorMethod(BytecodeCreator creator, String interceptorClass, MethodInfo interceptorMethod, + InterceptionType interceptionType, boolean isApplicationClass, ResultHandle invocationContext, + ResultHandle interceptorInstance) { + Class retType = null; + if (InterceptionType.AROUND_INVOKE.equals(interceptionType)) { + retType = Object.class; + } else { + // @PostConstruct, @PreDestroy, @AroundConstruct + retType = interceptorMethod.returnType().kind().equals(Type.Kind.VOID) ? void.class : Object.class; + } + ResultHandle ret; + // Check if interceptor method uses InvocationContext or ArcInvocationContext + Class invocationContextClass; + if (interceptorMethod.parameterType(0).name().equals(DotNames.INVOCATION_CONTEXT)) { + invocationContextClass = InvocationContext.class; + } else { + invocationContextClass = ArcInvocationContext.class; + } + if (Modifier.isPrivate(interceptorMethod.flags())) { + privateMembers.add(isApplicationClass, + String.format("Interceptor method %s#%s()", interceptorMethod.declaringClass().name(), + interceptorMethod.name())); + // Use reflection fallback + ResultHandle paramTypesArray = creator.newArray(Class.class, creator.load(1)); + creator.writeArrayValue(paramTypesArray, 0, creator.loadClass(invocationContextClass)); + ResultHandle argsArray = creator.newArray(Object.class, creator.load(1)); + creator.writeArrayValue(argsArray, 0, invocationContext); + reflectionRegistration.registerMethod(interceptorMethod); + ret = creator.invokeStaticMethod(MethodDescriptors.REFLECTIONS_INVOKE_METHOD, + creator.loadClass(interceptorMethod.declaringClass() + .name() + .toString()), + creator.load(interceptorMethod.name()), paramTypesArray, interceptorInstance, argsArray); + } else { + ret = creator.invokeVirtualMethod( + MethodDescriptor.ofMethod(interceptorClass, interceptorMethod.name(), retType, + invocationContextClass), + interceptorInstance, invocationContext); } + return ret; } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java index 79b90f70aa15a..21beeb93509ed 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java @@ -33,14 +33,10 @@ public class InterceptorInfo extends BeanInfo implements Comparable bindings; - - private final MethodInfo aroundInvoke; - - private final MethodInfo aroundConstruct; - - private final MethodInfo postConstruct; - - private final MethodInfo preDestroy; + private final List aroundInvokes; + private final List aroundConstructs; + private final List postConstructs; + private final List preDestroys; InterceptorInfo(AnnotationTarget target, BeanDeployment beanDeployment, Set bindings, List injections, int priority) { @@ -54,8 +50,12 @@ public class InterceptorInfo extends BeanInfo implements Comparable postConstructs = new ArrayList<>(); List preDestroys = new ArrayList<>(); + List allMethods = new ArrayList<>(); ClassInfo aClass = target.asClass(); while (aClass != null) { + // Only one interceptor method of a given type may be declared on a given class + int aroundInvokesFound = 0, aroundConstructsFound = 0, postConstructsFound = 0, preDestroysFound = 0; + for (MethodInfo method : aClass.methods()) { if (Modifier.isStatic(method.flags())) { continue; @@ -67,17 +67,34 @@ public class InterceptorInfo extends BeanInfo implements Comparable 1) { + throw new DefinitionException( + "Multiple @AroundInvoke interceptor methods declared on class: " + aClass); + } } if (store.hasAnnotation(method, DotNames.AROUND_CONSTRUCT)) { - aroundConstructs.add(validateSignature(method)); + addInterceptorMethod(allMethods, aroundConstructs, method); + if (++aroundConstructsFound > 1) { + throw new DefinitionException( + "Multiple @AroundConstruct interceptor methods declared on class: " + aClass); + } } if (store.hasAnnotation(method, DotNames.POST_CONSTRUCT)) { - postConstructs.add(validateSignature(method)); + addInterceptorMethod(allMethods, postConstructs, method); + if (++postConstructsFound > 1) { + throw new DefinitionException( + "Multiple @PostConstruct interceptor methods declared on class: " + aClass); + } } if (store.hasAnnotation(method, DotNames.PRE_DESTROY)) { - preDestroys.add(validateSignature(method)); + addInterceptorMethod(allMethods, preDestroys, method); + if (++preDestroysFound > 1) { + throw new DefinitionException( + "Multiple @PreDestroy interceptor methods declared on class: " + aClass); + } } + allMethods.add(method); } for (FieldInfo field : aClass.fields()) { @@ -93,62 +110,123 @@ public class InterceptorInfo extends BeanInfo implements Comparable parameters = method.parameterTypes(); - if (parameters.size() != 1 || !(parameters.get(0).name().equals(DotNames.INVOCATION_CONTEXT) - || parameters.get(0).name().equals(DotNames.ARC_INVOCATION_CONTEXT))) { - throw new IllegalStateException( - "An interceptor method must accept exactly one parameter of type jakarta.interceptor.InvocationContext: " - + method + " declared on " + method.declaringClass()); - } - if (!method.returnType().kind().equals(Type.Kind.VOID) && - !method.returnType().name().equals(DotNames.OBJECT)) { - throw new IllegalStateException( - "The return type of an interceptor method must be java.lang.Object or void: " - + method + " declared on " + method.declaringClass()); + this.aroundInvokes = List.copyOf(aroundInvokes); + this.aroundConstructs = List.copyOf(aroundConstructs); + this.postConstructs = List.copyOf(postConstructs); + this.preDestroys = List.copyOf(preDestroys); + + if (aroundConstructs.isEmpty() && aroundInvokes.isEmpty() && preDestroys.isEmpty() && postConstructs.isEmpty()) { + LOGGER.warnf("%s declares no around-invoke method nor a lifecycle callback!", this); } - return method; } public Set getBindings() { return bindings; } + /** + * Returns all methods annotated with {@link jakarta.interceptor.AroundInvoke} found in the hierarchy of the interceptor + * class. + *

+ * The returned list is sorted. The method declared on the most general superclass is first. The method declared on the + * interceptor class is last. + * + * @return the interceptor methods + */ + public List getAroundInvokes() { + return aroundInvokes; + } + + /** + * Returns all methods annotated with {@link jakarta.interceptor.AroundConstruct} found in the hierarchy of the interceptor + * class. + *

+ * The returned list is sorted. The method declared on the most general superclass is first. The method declared on the + * interceptor class is last. + * + * @return the interceptor methods + */ + public List getAroundConstructs() { + return aroundConstructs; + } + + /** + * Returns all methods annotated with {@link jakarta.annotation.PostConstruct} found in the hierarchy of the interceptor + * class. + *

+ * The returned list is sorted. The method declared on the most general superclass is first. The method declared on the + * interceptor class is last. + * + * @return the interceptor methods + */ + public List getPostConstructs() { + return postConstructs; + } + + /** + * Returns all methods annotated with {@link jakarta.annotation.PreDestroy} found in the hierarchy of the interceptor class. + *

+ * The returned list is sorted. The method declared on the most general superclass is first. The method declared on the + * interceptor class is last. + * + * @return the interceptor methods + */ + public List getPreDestroys() { + return preDestroys; + } + + /** + * + * @deprecated Use {@link #getAroundInvokes()} instead + */ + @Deprecated(since = "3.1", forRemoval = true) public MethodInfo getAroundInvoke() { - return aroundInvoke; + return aroundInvokes.get(aroundInvokes.size() - 1); } + /** + * + * @deprecated Use {@link #getAroundConstructs()} instead + */ + @Deprecated(since = "3.1", forRemoval = true) public MethodInfo getAroundConstruct() { - return aroundConstruct; + return aroundConstructs.get(aroundConstructs.size() - 1); } + /** + * + * @deprecated Use {@link #getPostConstructs()} instead + */ + @Deprecated(since = "3.1", forRemoval = true) public MethodInfo getPostConstruct() { - return postConstruct; + return postConstructs.get(postConstructs.size() - 1); } + /** + * + * @deprecated Use {@link #getPreDestroys()} instead + */ + @Deprecated(since = "3.1", forRemoval = true) public MethodInfo getPreDestroy() { - return preDestroy; + return preDestroys.get(preDestroys.size() - 1); } public boolean intercepts(InterceptionType interceptionType) { switch (interceptionType) { case AROUND_INVOKE: - return aroundInvoke != null; + return !aroundInvokes.isEmpty(); case AROUND_CONSTRUCT: - return aroundConstruct != null; + return !aroundConstructs.isEmpty(); case POST_CONSTRUCT: - return postConstruct != null; + return !postConstructs.isEmpty(); case PRE_DESTROY: - return preDestroy != null; + return !preDestroys.isEmpty(); default: return false; } @@ -169,4 +247,39 @@ public int compareTo(InterceptorInfo other) { return getTarget().toString().compareTo(other.getTarget().toString()); } + private void addInterceptorMethod(List allMethods, List interceptorMethods, MethodInfo method) { + validateSignature(method); + if (!isInterceptorMethodOverriden(allMethods, method)) { + interceptorMethods.add(method); + } + } + + private boolean isInterceptorMethodOverriden(Iterable allMethods, MethodInfo method) { + for (MethodInfo m : allMethods) { + if (m.name().equals(method.name()) && m.parametersCount() == 1 + && (m.parameterType(0).name().equals(DotNames.INVOCATION_CONTEXT) + || m.parameterType(0).name().equals(DotNames.ARC_INVOCATION_CONTEXT))) { + return true; + } + } + return false; + } + + private MethodInfo validateSignature(MethodInfo method) { + List parameters = method.parameterTypes(); + if (parameters.size() != 1 || !(parameters.get(0).name().equals(DotNames.INVOCATION_CONTEXT) + || parameters.get(0).name().equals(DotNames.ARC_INVOCATION_CONTEXT))) { + throw new IllegalStateException( + "An interceptor method must accept exactly one parameter of type jakarta.interceptor.InvocationContext: " + + method + " declared on " + method.declaringClass()); + } + if (!method.returnType().kind().equals(Type.Kind.VOID) && + !method.returnType().name().equals(DotNames.OBJECT)) { + throw new IllegalStateException( + "The return type of an interceptor method must be java.lang.Object or void: " + + method + " declared on " + method.declaringClass()); + } + return method; + } + } 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 c1e33488f6ef7..a3048c9c10de8 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 @@ -179,6 +179,10 @@ public final class MethodDescriptors { public static final MethodDescriptor INVOCATION_CONTEXTS_PRE_DESTROY = MethodDescriptor.ofMethod(InvocationContexts.class, "preDestroy", InvocationContext.class, Object.class, List.class, Set.class); + + public static final MethodDescriptor INVOCATION_CONTEXTS_PERFORM_SUPERCLASS = MethodDescriptor.ofMethod(InvocationContexts.class, + "performSuperclassInterception", + Object.class, InvocationContext.class, List.class, Object.class, Object[].class); public static final MethodDescriptor INVOCATION_CONTEXT_PROCEED = MethodDescriptor.ofMethod(InvocationContext.class, "proceed", @@ -188,6 +192,10 @@ public final class MethodDescriptors { "getTarget", Object.class); + public static final MethodDescriptor INVOCATION_CONTEXT_GET_PARAMETERS = MethodDescriptor.ofMethod(InvocationContext.class, + "getParameters", + Object[].class); + public static final MethodDescriptor CREATIONAL_CTX_ADD_DEP_TO_PARENT = MethodDescriptor.ofMethod( CreationalContextImpl.class, "addDependencyToParent", void.class, diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AbstractInvocationContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AbstractInvocationContext.java index 3feff00e1769e..8a58ee4338c5c 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AbstractInvocationContext.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/AbstractInvocationContext.java @@ -55,7 +55,7 @@ public List findIterceptorBindings(Class annotation return found; } - protected void validateParameters(Executable executable, Object[] params) { + static void validateParameters(Executable executable, Object[] params) { int newParametersCount = Objects.requireNonNull(params).length; Class[] parameterTypes = executable.getParameterTypes(); if (parameterTypes.length != newParametersCount) { diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InnerInvocationContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InnerInvocationContext.java new file mode 100644 index 0000000000000..26b617dcb1869 --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InnerInvocationContext.java @@ -0,0 +1,155 @@ +package io.quarkus.arc.impl; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jakarta.interceptor.InvocationContext; + +import io.quarkus.arc.ArcInvocationContext; + +abstract class InnerInvocationContext implements ArcInvocationContext { + + protected final ArcInvocationContext delegate; + protected Object[] parameters; + + InnerInvocationContext(InvocationContext delegate, Object[] parameters) { + this.delegate = (ArcInvocationContext) delegate; + this.parameters = parameters; + } + + @Override + public Set getInterceptorBindings() { + return delegate.getInterceptorBindings(); + } + + public Method getMethod() { + return delegate.getMethod(); + } + + @Override + public Object[] getParameters() { + if (parameters == null) { + throw new IllegalStateException(); + } + return parameters; + } + + @Override + public void setParameters(Object[] params) { + if (parameters == null) { + throw new IllegalStateException(); + } + AbstractInvocationContext.validateParameters(delegate.getMethod(), params); + this.parameters = params; + } + + @Override + public Object getTarget() { + return delegate.getTarget(); + } + + @Override + public Object getTimer() { + return delegate.getTimer(); + } + + @Override + public Constructor getConstructor() { + return delegate.getConstructor(); + } + + @Override + public Map getContextData() { + return delegate.getContextData(); + } + + @Override + public T findIterceptorBinding(Class annotationType) { + return delegate.findIterceptorBinding(annotationType); + } + + @Override + public List findIterceptorBindings(Class annotationType) { + return delegate.findIterceptorBindings(annotationType); + } + + @Override + public Object proceed() throws Exception { + return proceed(1); + } + + protected abstract Object proceed(int currentPosition) throws Exception; + + class NextInnerInvocationContext implements ArcInvocationContext { + + private final int position; + protected Object[] parameters; + + public NextInnerInvocationContext(int position, Object[] parameters) { + this.position = position; + this.parameters = parameters; + } + + @Override + public Object proceed() throws Exception { + return InnerInvocationContext.this.proceed(position); + } + + @Override + public Object getTarget() { + return InnerInvocationContext.this.getTarget(); + } + + @Override + public Object getTimer() { + return InnerInvocationContext.this.getTimer(); + } + + @Override + public Method getMethod() { + return InnerInvocationContext.this.getMethod(); + } + + @Override + public Constructor getConstructor() { + return InnerInvocationContext.this.getConstructor(); + } + + @Override + public Object[] getParameters() { + return parameters; + } + + @Override + public void setParameters(Object[] params) { + AbstractInvocationContext.validateParameters(InnerInvocationContext.this.delegate.getMethod(), params); + this.parameters = params; + } + + @Override + public Map getContextData() { + return InnerInvocationContext.this.getContextData(); + } + + @Override + public Set getInterceptorBindings() { + return InnerInvocationContext.this.getInterceptorBindings(); + } + + @Override + public T findIterceptorBinding(Class annotationType) { + return InnerInvocationContext.this.findIterceptorBinding(annotationType); + } + + @Override + public List findIterceptorBindings(Class annotationType) { + return InnerInvocationContext.this.findIterceptorBindings(annotationType); + } + + } + +} diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InvocationContexts.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InvocationContexts.java index 51aee7b76d825..9ae287ea3cb46 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InvocationContexts.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InvocationContexts.java @@ -66,4 +66,18 @@ public static InvocationContext aroundConstruct(Constructor constructor, aroundConstructForward); } + /** + * + * @param delegate + * @param methods + * @param interceptorInstance + * @return the return value + * @throws Exception + */ + public static Object performSuperclassInterception(InvocationContext delegate, + List> methods, Object interceptorInstance, Object[] parameters) + throws Exception { + return SuperclassInvocationContext.perform(delegate, methods, interceptorInstance, parameters); + } + } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/SuperclassInvocationContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/SuperclassInvocationContext.java new file mode 100644 index 0000000000000..77d1f8c1cd48a --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/SuperclassInvocationContext.java @@ -0,0 +1,58 @@ +package io.quarkus.arc.impl; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.function.BiFunction; + +import jakarta.interceptor.InvocationContext; + +/** + * A special {@link javax.interceptor.InvocationContext} that is used if multiple interceptor methods are declared in a + * hierarchy of an interceptor class. + *

+ * The interceptor methods defined by the superclasses are invoked before the interceptor method defined by the interceptor + * class, most general superclass first. + */ +class SuperclassInvocationContext extends InnerInvocationContext { + + static Object perform(InvocationContext delegate, List> methods, + Object interceptorInstance, Object[] parameters) + throws Exception { + return methods.get(0).apply(interceptorInstance, + new SuperclassInvocationContext(delegate, methods, interceptorInstance, parameters)); + } + + private final Object interceptorInstance; + private final List> methods; + + SuperclassInvocationContext(InvocationContext delegate, List> methods, + Object interceptorInstance, Object[] parameters) { + super(delegate, parameters); + this.methods = methods; + this.interceptorInstance = interceptorInstance; + } + + protected Object proceed(int currentPosition) throws Exception { + try { + if (currentPosition < methods.size()) { + // Invoke the next interceptor method from the hierarchy + return methods.get(currentPosition) + .apply(interceptorInstance, + new NextInnerInvocationContext(currentPosition + 1, parameters)); + } else { + // Invoke the next interceptor in the chain + return delegate.proceed(); + } + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Error) { + throw (Error) cause; + } + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw new RuntimeException(cause); + } + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/AlphaBinding.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/AlphaBinding.java new file mode 100644 index 0000000000000..85c51ebf7a473 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/AlphaBinding.java @@ -0,0 +1,19 @@ +package io.quarkus.arc.test.interceptors.inheritance.hierarchy; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.interceptor.InterceptorBinding; + +@Target({ TYPE, METHOD }) +@Retention(RUNTIME) +@Documented +@InterceptorBinding +public @interface AlphaBinding { + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/AlphaInterceptor.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/AlphaInterceptor.java new file mode 100644 index 0000000000000..4fdd8b25959a4 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/AlphaInterceptor.java @@ -0,0 +1,43 @@ +package io.quarkus.arc.test.interceptors.inheritance.hierarchy; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundConstruct; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; + +@Priority(1) +@AlphaBinding +@Interceptor +public class AlphaInterceptor extends Bravo { + + @AroundInvoke + public Object intercept(InvocationContext ctx) throws Exception { + return "a/" + ctx.proceed() + "/a"; + } + + @PostConstruct + void init(InvocationContext ctx) throws Exception { + SuperclassInterceptorMethodsTest.LIFECYCLE_CALLBACKS.add("a"); + ctx.proceed(); + } + + @PreDestroy + void destroy(InvocationContext ctx) throws Exception { + SuperclassInterceptorMethodsTest.LIFECYCLE_CALLBACKS.add("a"); + ctx.proceed(); + } + + @AroundConstruct + public void construct(InvocationContext ctx) throws Exception { + SuperclassInterceptorMethodsTest.LIFECYCLE_CALLBACKS.add("A"); + ctx.proceed(); + } + + // "If an interceptor method is overridden by another method (regardless whether that method is itself an interceptor method), it will not be invoked." + void alphaDummyInit(InvocationContext ctx) throws Exception { + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/Bravo.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/Bravo.java new file mode 100644 index 0000000000000..4c3c9240ea07c --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/Bravo.java @@ -0,0 +1,36 @@ +package io.quarkus.arc.test.interceptors.inheritance.hierarchy; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.interceptor.AroundConstruct; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.InvocationContext; + +public class Bravo extends Charlie { + + @AroundInvoke + public Object bravoIntercept(InvocationContext ctx) throws Exception { + return "b/" + ctx.proceed() + "/b"; + } + + // This callback is overriden + @PostConstruct + void alphaDummyInit(InvocationContext ctx) throws Exception { + SuperclassInterceptorMethodsTest.LIFECYCLE_CALLBACKS.add("b"); + ctx.proceed(); + } + + @PreDestroy + void bravoDestroy(InvocationContext ctx) throws Exception { + SuperclassInterceptorMethodsTest.LIFECYCLE_CALLBACKS.add("b"); + ctx.proceed(); + } + + // This callback is overriden + @AroundConstruct + public void construct(InvocationContext ctx) throws Exception { + SuperclassInterceptorMethodsTest.LIFECYCLE_CALLBACKS.add("B"); + ctx.proceed(); + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/Charlie.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/Charlie.java new file mode 100644 index 0000000000000..8289dd573a919 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/Charlie.java @@ -0,0 +1,34 @@ +package io.quarkus.arc.test.interceptors.inheritance.hierarchy; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.interceptor.AroundConstruct; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.InvocationContext; + +public class Charlie { + + @AroundInvoke + public Object charlieIntercept(InvocationContext ctx) throws Exception { + return "c/" + ctx.proceed() + "/c"; + } + + @PostConstruct + void charlieInit(InvocationContext ctx) throws Exception { + SuperclassInterceptorMethodsTest.LIFECYCLE_CALLBACKS.add("c"); + ctx.proceed(); + } + + // This callback is overriden + @PreDestroy + void destroy(InvocationContext ctx) throws Exception { + SuperclassInterceptorMethodsTest.LIFECYCLE_CALLBACKS.add("c"); + ctx.proceed(); + } + + @AroundConstruct + public void charlieConstruct(InvocationContext ctx) throws Exception { + SuperclassInterceptorMethodsTest.LIFECYCLE_CALLBACKS.add("C"); + ctx.proceed(); + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/MultipleInterceptorMethodDeclaredOnSuperclassTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/MultipleInterceptorMethodDeclaredOnSuperclassTest.java new file mode 100644 index 0000000000000..0a2f4ac5ac431 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/MultipleInterceptorMethodDeclaredOnSuperclassTest.java @@ -0,0 +1,69 @@ +package io.quarkus.arc.test.interceptors.inheritance.hierarchy; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import jakarta.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.test.ArcTestContainer; + +public class MultipleInterceptorMethodDeclaredOnSuperclassTest { + + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(AlphaInterceptor.class, Bravo.class, AlphaBinding.class, + Fool.class) + .shouldFail().build(); + + @Test + public void testFailure() { + Throwable error = container.getFailure(); + assertNotNull(error); + assertEquals( + "Multiple @AroundInvoke interceptor methods declared on class: io.quarkus.arc.test.interceptors.inheritance.hierarchy.MultipleInterceptorMethodDeclaredOnSuperclassTest$Bravo", + error.getMessage()); + } + + @Priority(1) + @AlphaBinding + @Interceptor + static class AlphaInterceptor extends Bravo { + + @AroundInvoke + public Object intercept(InvocationContext ctx) throws Exception { + return "a/" + ctx.proceed() + "/a"; + } + + } + + static class Bravo { + + @AroundInvoke + public Object b1(InvocationContext ctx) throws Exception { + return ctx.proceed(); + } + + @AroundInvoke + public Object b2(InvocationContext ctx) throws Exception { + return ctx.proceed(); + } + } + + @AlphaBinding + @ApplicationScoped + public static class Fool { + + String ping() { + return "ping"; + } + + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/SuperclassInterceptorMethodsTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/SuperclassInterceptorMethodsTest.java new file mode 100644 index 0000000000000..c331d0144fd7d --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/inheritance/hierarchy/SuperclassInterceptorMethodsTest.java @@ -0,0 +1,54 @@ +package io.quarkus.arc.test.interceptors.inheritance.hierarchy; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.test.ArcTestContainer; + +public class SuperclassInterceptorMethodsTest { + + static final List LIFECYCLE_CALLBACKS = new CopyOnWriteArrayList<>(); + + @RegisterExtension + public ArcTestContainer container = new ArcTestContainer(AlphaInterceptor.class, Bravo.class, AlphaBinding.class, + Fool.class); + + @Test + public void testInterception() { + LIFECYCLE_CALLBACKS.clear(); + InstanceHandle handle = Arc.container().instance(Fool.class); + Fool fool = handle.get(); + assertEquals("c/b/a/ping/a/b/c", fool.ping()); + assertEquals(4, LIFECYCLE_CALLBACKS.size(), LIFECYCLE_CALLBACKS.toString()); + assertEquals("C", LIFECYCLE_CALLBACKS.get(0)); + assertEquals("A", LIFECYCLE_CALLBACKS.get(1)); + assertEquals("c", LIFECYCLE_CALLBACKS.get(2)); + assertEquals("a", LIFECYCLE_CALLBACKS.get(3)); + + LIFECYCLE_CALLBACKS.clear(); + handle.destroy(); + assertEquals(2, LIFECYCLE_CALLBACKS.size(), LIFECYCLE_CALLBACKS.toString()); + assertEquals("b", LIFECYCLE_CALLBACKS.get(0)); + assertEquals("a", LIFECYCLE_CALLBACKS.get(1)); + } + + @AlphaBinding + @ApplicationScoped + public static class Fool { + + String ping() { + return "ping"; + } + + } + +} From 59916eec3d1ea41eef0d78b0e623d2946011618b Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 18 Apr 2023 13:05:39 +0200 Subject: [PATCH 2/5] ArC - support around invoke method declared on target class - and its superclasses --- .../io/quarkus/arc/processor/BeanInfo.java | 25 +++++- .../quarkus/arc/processor/BeanProcessor.java | 2 +- .../java/io/quarkus/arc/processor/Beans.java | 28 +++++++ .../arc/processor/MethodDescriptors.java | 9 ++- .../io/quarkus/arc/processor/Methods.java | 16 ++-- .../arc/processor/SubclassGenerator.java | 77 ++++++++++++++++++- .../impl/AroundInvokeInvocationContext.java | 11 ++- .../quarkus/arc/impl/InvocationContexts.java | 17 +++- .../TargetAroundInvokeInvocationContext.java | 58 ++++++++++++++ 9 files changed, 223 insertions(+), 20 deletions(-) create mode 100644 independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/TargetAroundInvokeInvocationContext.java diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java index 858b76965aa98..ead53d8a41703 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java @@ -90,6 +90,8 @@ public class BeanInfo implements InjectionTargetInfo { private final String targetPackageName; + private final List aroundInvokes; + BeanInfo(AnnotationTarget target, BeanDeployment beanDeployment, ScopeInfo scope, Set types, Set qualifiers, List injections, BeanInfo declaringBean, DisposerInfo disposer, boolean alternative, List stereotypes, String name, boolean isDefaultBean, String targetPackageName, @@ -143,6 +145,7 @@ public class BeanInfo implements InjectionTargetInfo { this.lifecycleInterceptors = Collections.emptyMap(); this.forceApplicationClass = forceApplicationClass; this.targetPackageName = targetPackageName; + this.aroundInvokes = isInterceptor() || isDecorator() ? List.of() : Beans.getAroundInvokes(implClazz, beanDeployment); } @Override @@ -360,8 +363,10 @@ public boolean hasAroundInvokeInterceptors() { } boolean isSubclassRequired() { - return !interceptedMethods.isEmpty() || !decoratedMethods.isEmpty() - || lifecycleInterceptors.containsKey(InterceptionType.PRE_DESTROY); + return !interceptedMethods.isEmpty() + || !decoratedMethods.isEmpty() + || lifecycleInterceptors.containsKey(InterceptionType.PRE_DESTROY) + || !aroundInvokes.isEmpty(); } /** @@ -445,6 +450,18 @@ public List getBoundDecorators() { return bound; } + /** + * + * @return the list of around invoke interceptor methods declared in the hierarchy of a bean class + */ + List getAroundInvokes() { + return aroundInvokes; + } + + boolean hasAroundInvokes() { + return !aroundInvokes.isEmpty(); + } + public DisposerInfo getDisposer() { return disposer; } @@ -603,7 +620,7 @@ private Map initInterceptedMethods(List } Set finalMethods = Methods.addInterceptedMethodCandidates(beanDeployment, target.get().asClass(), - candidates, classLevelBindings, bytecodeTransformerConsumer, transformUnproxyableClasses); + candidates, classLevelBindings, bytecodeTransformerConsumer, transformUnproxyableClasses, aroundInvokes); if (!finalMethods.isEmpty()) { String additionalError = ""; if (finalMethods.stream().anyMatch(KotlinUtils::isNoninterceptableKotlinMethod)) { @@ -620,7 +637,7 @@ private Map initInterceptedMethods(List for (Entry> entry : candidates.entrySet()) { List interceptors = beanDeployment.getInterceptorResolver() .resolve(InterceptionType.AROUND_INVOKE, entry.getValue()); - if (!interceptors.isEmpty()) { + if (!interceptors.isEmpty() || !aroundInvokes.isEmpty()) { interceptedMethods.put(entry.getKey().method, new InterceptionInfo(interceptors, entry.getValue())); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java index 465dc1447aa21..ba749360e3c54 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java @@ -216,7 +216,7 @@ public List generateResources(ReflectionRegistration reflectionRegistr } SubclassGenerator subclassGenerator = new SubclassGenerator(annotationLiterals, applicationClassPredicate, - generateSources, refReg, existingClasses); + generateSources, refReg, existingClasses, privateMembers); ObserverGenerator observerGenerator = new ObserverGenerator(annotationLiterals, applicationClassPredicate, privateMembers, generateSources, refReg, existingClasses, observerToGeneratedName, diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java index 2cd2e56e3d8b7..141acb7620184 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java @@ -649,6 +649,34 @@ static List getCallbacks(ClassInfo beanClass, DotName annotation, In return callbacks; } + static List getAroundInvokes(ClassInfo beanClass, BeanDeployment deployment) { + List methods = new ArrayList<>(); + AnnotationStore store = deployment.getAnnotationStore(); + Set processed = new HashSet<>(); + + ClassInfo aClass = beanClass; + while (aClass != null) { + int aroundInvokesFound = 0; + for (MethodInfo method : aClass.methods()) { + if (Modifier.isStatic(method.flags())) { + continue; + } + if (store.hasAnnotation(method, DotNames.AROUND_INVOKE) && !processed.contains(method.name())) { + methods.add(method); + if (++aroundInvokesFound > 1) { + throw new DefinitionException( + "Multiple @AroundInvoke interceptor methods declared on class: " + aClass); + } + } + } + DotName superTypeName = aClass.superName(); + aClass = superTypeName == null || DotNames.OBJECT.equals(superTypeName) ? null + : getClassByName(deployment.getBeanArchiveIndex(), superTypeName); + } + Collections.reverse(methods); + return methods.isEmpty() ? List.of() : List.copyOf(methods); + } + static void analyzeType(Type type, BeanDeployment beanDeployment) { if (type.kind() == Type.Kind.PARAMETERIZED_TYPE) { for (Type argument : type.asParameterizedType().arguments()) { 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 a3048c9c10de8..9d14b1dfad991 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 @@ -166,6 +166,10 @@ public final class MethodDescriptors { InvocationContexts.class, "performAroundInvoke", Object.class, Object.class, Object[].class, InterceptedMethodMetadata.class); + public static final MethodDescriptor INVOCATION_CONTEXTS_PERFORM_TARGET_AROUND_INVOKE = MethodDescriptor.ofMethod( + InvocationContexts.class, + "performTargetAroundInvoke", Object.class, InvocationContext.class, List.class, BiFunction.class); + public static final MethodDescriptor INVOCATION_CONTEXTS_AROUND_CONSTRUCT = MethodDescriptor.ofMethod( InvocationContexts.class, "aroundConstruct", @@ -179,8 +183,9 @@ public final class MethodDescriptors { public static final MethodDescriptor INVOCATION_CONTEXTS_PRE_DESTROY = MethodDescriptor.ofMethod(InvocationContexts.class, "preDestroy", InvocationContext.class, Object.class, List.class, Set.class); - - public static final MethodDescriptor INVOCATION_CONTEXTS_PERFORM_SUPERCLASS = MethodDescriptor.ofMethod(InvocationContexts.class, + + public static final MethodDescriptor INVOCATION_CONTEXTS_PERFORM_SUPERCLASS = MethodDescriptor.ofMethod( + InvocationContexts.class, "performSuperclassInterception", Object.class, InvocationContext.class, List.class, Object.class, Object[].class); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Methods.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Methods.java index ad6e073eec531..1dc877ce6a2de 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Methods.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Methods.java @@ -152,12 +152,12 @@ static boolean isObjectToString(MethodInfo method) { static Set addInterceptedMethodCandidates(BeanDeployment beanDeployment, ClassInfo classInfo, Map> candidates, List classLevelBindings, Consumer bytecodeTransformerConsumer, - boolean transformUnproxyableClasses) { + boolean transformUnproxyableClasses, List aroundInvokes) { return addInterceptedMethodCandidates(beanDeployment, classInfo, classInfo, candidates, Set.copyOf(classLevelBindings), bytecodeTransformerConsumer, transformUnproxyableClasses, new SubclassSkipPredicate(beanDeployment.getAssignabilityCheck()::isAssignableFrom, beanDeployment.getBeanArchiveIndex(), beanDeployment.getObserverAndProducerMethods()), - false, new HashSet<>()); + false, new HashSet<>(), aroundInvokes); } private static Set addInterceptedMethodCandidates(BeanDeployment beanDeployment, ClassInfo classInfo, @@ -165,16 +165,20 @@ private static Set addInterceptedMethodCandidates(BeanDeployment bea Map> candidates, Set classLevelBindings, Consumer bytecodeTransformerConsumer, boolean transformUnproxyableClasses, SubclassSkipPredicate skipPredicate, boolean ignoreMethodLevelBindings, - Set noClassInterceptorsMethods) { + Set noClassInterceptorsMethods, List aroundInvokes) { Set methodsFromWhichToRemoveFinal = new HashSet<>(); Set finalMethodsFoundAndNotChanged = new HashSet<>(); skipPredicate.startProcessing(classInfo, originalClassInfo); for (MethodInfo method : classInfo.methods()) { + if (aroundInvokes.contains(method)) { + // Around invoke method declared in the target class hierarchy + continue; + } Set merged = mergeBindings(beanDeployment, originalClassInfo, classLevelBindings, ignoreMethodLevelBindings, method, noClassInterceptorsMethods); - if (merged.isEmpty() || skipPredicate.test(method)) { + if ((merged.isEmpty() && aroundInvokes.isEmpty()) || skipPredicate.test(method)) { continue; } boolean addToCandidates = true; @@ -204,7 +208,7 @@ private static Set addInterceptedMethodCandidates(BeanDeployment bea finalMethodsFoundAndNotChanged .addAll(addInterceptedMethodCandidates(beanDeployment, superClassInfo, classInfo, candidates, classLevelBindings, bytecodeTransformerConsumer, transformUnproxyableClasses, skipPredicate, - ignoreMethodLevelBindings, noClassInterceptorsMethods)); + ignoreMethodLevelBindings, noClassInterceptorsMethods, aroundInvokes)); } } @@ -214,7 +218,7 @@ private static Set addInterceptedMethodCandidates(BeanDeployment bea //interfaces can't have final methods addInterceptedMethodCandidates(beanDeployment, interfaceInfo, originalClassInfo, candidates, classLevelBindings, bytecodeTransformerConsumer, transformUnproxyableClasses, - skipPredicate, true, noClassInterceptorsMethods); + skipPredicate, true, noClassInterceptorsMethods, aroundInvokes); } } return finalMethodsFoundAndNotChanged; 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 ea4aa6ab8b4fb..fcd318c61d779 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 @@ -38,6 +38,7 @@ import org.jboss.jandex.Type.Kind; import org.jboss.jandex.TypeVariable; +import io.quarkus.arc.ArcInvocationContext; import io.quarkus.arc.ArcUndeclaredThrowableException; import io.quarkus.arc.InjectableDecorator; import io.quarkus.arc.InjectableInterceptor; @@ -45,6 +46,7 @@ import io.quarkus.arc.impl.InterceptedMethodMetadata; import io.quarkus.arc.processor.BeanInfo.DecorationInfo; import io.quarkus.arc.processor.BeanInfo.InterceptionInfo; +import io.quarkus.arc.processor.BeanProcessor.PrivateMembersCollector; import io.quarkus.arc.processor.Methods.MethodKey; import io.quarkus.arc.processor.ResourceOutput.Resource; import io.quarkus.arc.processor.ResourceOutput.Resource.SpecialType; @@ -80,6 +82,7 @@ public class SubclassGenerator extends AbstractGenerator { private final Predicate applicationClassPredicate; private final Set existingClasses; + private final PrivateMembersCollector privateMembers; static String generatedName(DotName providerTypeName, String baseName) { String packageName = DotNames.internalPackageNameWithTrailingSlash(providerTypeName); @@ -90,11 +93,12 @@ static String generatedName(DotName providerTypeName, String baseName) { public SubclassGenerator(AnnotationLiteralProcessor annotationLiterals, Predicate applicationClassPredicate, boolean generateSources, ReflectionRegistration reflectionRegistration, - Set existingClasses) { + Set existingClasses, PrivateMembersCollector privateMembers) { super(generateSources, reflectionRegistration); this.applicationClassPredicate = applicationClassPredicate; this.annotationLiterals = annotationLiterals; this.existingClasses = existingClasses; + this.privateMembers = privateMembers; } Collection generate(BeanInfo bean, String beanClassName) { @@ -325,6 +329,25 @@ public String apply(List keys) { } } + // Initialize the "aroundInvokes" field if necessary + if (bean.hasAroundInvokes()) { + FieldCreator field = subclass.getFieldCreator("aroundInvokes", List.class) + .setModifiers(ACC_PRIVATE); + ResultHandle methodsList = constructor.newInstance(MethodDescriptor.ofConstructor(ArrayList.class)); + for (MethodInfo method : bean.getAroundInvokes()) { + // BiFunction + FunctionCreator fun = constructor.createFunction(BiFunction.class); + BytecodeCreator funBytecode = fun.getBytecode(); + ResultHandle ret = invokeInterceptorMethod(funBytecode, method, + applicationClassPredicate.test(bean.getBeanClass()), + funBytecode.getMethodParam(1), + funBytecode.getMethodParam(0)); + funBytecode.returnValue(ret); + constructor.invokeInterfaceMethod(MethodDescriptors.LIST_ADD, methodsList, fun.getInstance()); + } + constructor.writeInstanceField(field.getFieldDescriptor(), constructor.getThis(), methodsList); + } + // Split initialization of InterceptedMethodMetadata into multiple methods int group = 0; int groupLimit = 30; @@ -422,6 +445,7 @@ public String apply(List keys) { superParamHandles[i] = funcBytecode.readArrayValue(ctxParamsHandle, i); } } + // If a decorator is bound then invoke the method upon the decorator instance instead of the generated forwarding method if (decorator != null) { AssignableResultHandle funDecoratorInstance = funcBytecode.createVariable(Object.class); @@ -445,10 +469,28 @@ public String apply(List keys) { funcBytecode.returnValue(superResult != null ? superResult : funcBytecode.loadNull()); } + ResultHandle aroundForwardFun = func.getInstance(); + + if (bean.hasAroundInvokes()) { + // Wrap the forwarding function with a function that calls around invoke methods declared in a hierarchy of the target class first + AssignableResultHandle methodsList = initMetadataMethod.createVariable(List.class); + initMetadataMethod.assign(methodsList, initMetadataMethod.readInstanceField( + FieldDescriptor.of(subclass.getClassName(), "aroundInvokes", List.class), + initMetadataMethod.getThis())); + FunctionCreator targetFun = initMetadataMethod.createFunction(BiFunction.class); + BytecodeCreator targetFunBytecode = targetFun.getBytecode(); + ResultHandle ret = targetFunBytecode.invokeStaticMethod( + MethodDescriptors.INVOCATION_CONTEXTS_PERFORM_TARGET_AROUND_INVOKE, + targetFunBytecode.getMethodParam(1), + methodsList, aroundForwardFun); + targetFunBytecode.returnValue(ret); + aroundForwardFun = targetFun.getInstance(); + } + // Now create metadata for the given intercepted method ResultHandle methodMetadataHandle = initMetadataMethod.newInstance( MethodDescriptors.INTERCEPTED_METHOD_METADATA_CONSTRUCTOR, - chainHandle, methodHandle, bindingsHandle, func.getInstance()); + chainHandle, methodHandle, bindingsHandle, aroundForwardFun); FieldDescriptor metadataField = FieldDescriptor.of(subclass.getClassName(), "arc$" + methodIdx++, InterceptedMethodMetadata.class.getName()); @@ -513,6 +555,37 @@ public String apply(List keys) { return preDestroysField != null ? preDestroysField.getFieldDescriptor() : null; } + private ResultHandle invokeInterceptorMethod(BytecodeCreator creator, MethodInfo interceptorMethod, + boolean isApplicationClass, ResultHandle invocationContext, ResultHandle targetInstance) { + ResultHandle ret; + // Check if interceptor method uses InvocationContext or ArcInvocationContext + Class invocationContextClass; + if (interceptorMethod.parameterType(0).name().equals(DotNames.INVOCATION_CONTEXT)) { + invocationContextClass = InvocationContext.class; + } else { + invocationContextClass = ArcInvocationContext.class; + } + if (Modifier.isPrivate(interceptorMethod.flags())) { + privateMembers.add(isApplicationClass, + String.format("Interceptor method %s#%s()", interceptorMethod.declaringClass().name(), + interceptorMethod.name())); + // Use reflection fallback + ResultHandle paramTypesArray = creator.newArray(Class.class, creator.load(1)); + creator.writeArrayValue(paramTypesArray, 0, creator.loadClass(invocationContextClass)); + ResultHandle argsArray = creator.newArray(Object.class, creator.load(1)); + creator.writeArrayValue(argsArray, 0, invocationContext); + reflectionRegistration.registerMethod(interceptorMethod); + ret = creator.invokeStaticMethod(MethodDescriptors.REFLECTIONS_INVOKE_METHOD, + creator.loadClass(interceptorMethod.declaringClass() + .name() + .toString()), + creator.load(interceptorMethod.name()), paramTypesArray, targetInstance, argsArray); + } else { + ret = creator.invokeVirtualMethod(interceptorMethod, targetInstance, invocationContext); + } + return ret; + } + private void processDecorator(DecoratorInfo decorator, BeanInfo bean, Type providerType, String providerTypeName, ClassCreator subclass, ClassOutput classOutput, MethodCreator subclassConstructor, int paramIndex, Map decoratorToResultHandle, 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 eea2a771e2728..73b87a7649c59 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 @@ -27,6 +27,13 @@ */ class AroundInvokeInvocationContext extends AbstractInvocationContext { + static Object perform(Object target, Object[] args, InterceptedMethodMetadata metadata) throws Exception { + if (metadata.chain.isEmpty()) { + return metadata.aroundInvokeForward.apply(target, new AroundInvokeInvocationContext(target, args, metadata)); + } + return metadata.chain.get(0).invoke(new AroundInvokeInvocationContext(target, args, metadata)); + } + private final InterceptedMethodMetadata metadata; AroundInvokeInvocationContext(Object target, Object[] args, InterceptedMethodMetadata metadata) { @@ -34,10 +41,6 @@ class AroundInvokeInvocationContext extends AbstractInvocationContext { this.metadata = metadata; } - static Object perform(Object target, Object[] args, InterceptedMethodMetadata metadata) throws Exception { - return metadata.chain.get(0).invoke(new AroundInvokeInvocationContext(target, args, metadata)); - } - @Override public Set getInterceptorBindings() { return metadata.bindings; diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InvocationContexts.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InvocationContexts.java index 9ae287ea3cb46..fa137958f4c7c 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InvocationContexts.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InvocationContexts.java @@ -4,6 +4,7 @@ import java.lang.reflect.Constructor; import java.util.List; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Supplier; import jakarta.interceptor.InvocationContext; @@ -26,6 +27,20 @@ public static Object performAroundInvoke(Object target, Object[] args, Intercept return AroundInvokeInvocationContext.perform(target, args, metadata); } + /** + * + * @param delegate + * @param aroundInvokeMethods + * @param aroundInvokeForward + * @return the return value + * @throws Exception + */ + public static Object performTargetAroundInvoke(InvocationContext delegate, + List> aroundInvokeMethods, + BiFunction aroundInvokeForward) throws Exception { + return TargetAroundInvokeInvocationContext.perform(delegate, aroundInvokeMethods, aroundInvokeForward); + } + /** * * @param target @@ -67,7 +82,7 @@ public static InvocationContext aroundConstruct(Constructor constructor, } /** - * + * * @param delegate * @param methods * @param interceptorInstance diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/TargetAroundInvokeInvocationContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/TargetAroundInvokeInvocationContext.java new file mode 100644 index 0000000000000..782f1768b8dfe --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/TargetAroundInvokeInvocationContext.java @@ -0,0 +1,58 @@ +package io.quarkus.arc.impl; + +import java.util.List; +import java.util.function.BiFunction; + +import jakarta.interceptor.InvocationContext; + +/** + * A special {@link javax.interceptor.InvocationContext} that is used for around invoke methods declared in a hierarchy of a + * target class. + *

+ * The interceptor methods defined by the superclasses are invoked before the interceptor method defined by the interceptor + * class, most general superclass first. + */ +class TargetAroundInvokeInvocationContext extends InnerInvocationContext { + + static Object perform(InvocationContext delegate, + List> methods, + BiFunction aroundInvokeForward) + throws Exception { + return methods.get(0).apply(delegate.getTarget(), + new TargetAroundInvokeInvocationContext(delegate, methods, aroundInvokeForward)); + } + + private final List> methods; + private final BiFunction aroundInvokeForward; + + TargetAroundInvokeInvocationContext(InvocationContext delegate, List> methods, + BiFunction aroundInvokeForward) { + super(delegate, delegate.getParameters()); + this.methods = methods; + this.aroundInvokeForward = aroundInvokeForward; + } + + protected Object proceed(int currentPosition) throws Exception { + try { + if (currentPosition < methods.size()) { + // Invoke the next interceptor method from the hierarchy + return methods.get(currentPosition) + .apply(delegate.getTarget(), + new NextInnerInvocationContext(currentPosition + 1, parameters)); + } else { + // Invoke the target method + return aroundInvokeForward.apply(delegate.getTarget(), this); + } + } catch (Exception e) { + Throwable cause = e.getCause(); + if (cause instanceof Error) { + throw (Error) cause; + } + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw new RuntimeException(cause); + } + } + +} From f9bfadb7df95ca46121876bc84c789a45d014ed4 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 18 Apr 2023 16:50:38 +0200 Subject: [PATCH 3/5] ArC TCK runner - update exlude list - removed AroundConstructOrderTest, AroundInvokeOrderTest and PostConstructOrderTest --- .../cdi-tck-runner/src/test/resources/testng.xml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/independent-projects/arc/tcks/cdi-tck-runner/src/test/resources/testng.xml b/independent-projects/arc/tcks/cdi-tck-runner/src/test/resources/testng.xml index de07e790cdc48..c27324111b129 100644 --- a/independent-projects/arc/tcks/cdi-tck-runner/src/test/resources/testng.xml +++ b/independent-projects/arc/tcks/cdi-tck-runner/src/test/resources/testng.xml @@ -158,11 +158,6 @@ - - - - - @@ -363,11 +358,6 @@ - - - - - @@ -401,11 +391,6 @@ - - - - - From f181c27d83eaf03c4081840ef7e5bdcaa572e59a Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Wed, 19 Apr 2023 12:25:32 +0200 Subject: [PATCH 4/5] ArC: improve javadoc for InvocationContext implementations --- .../io/quarkus/arc/impl/AroundInvokeInvocationContext.java | 2 +- .../java/io/quarkus/arc/impl/InnerInvocationContext.java | 5 +++++ .../quarkus/arc/impl/LifecycleCallbackInvocationContext.java | 4 ++-- .../io/quarkus/arc/impl/SuperclassInvocationContext.java | 2 +- .../arc/impl/TargetAroundInvokeInvocationContext.java | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) 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 73b87a7649c59..46739088b3933 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 @@ -11,7 +11,7 @@ import io.quarkus.arc.ArcInvocationContext; /** - * An {@link javax.interceptor.InvocationContext} for {@link javax.interceptor.AroundInvoke} interceptors. + * An {@link jakarta.interceptor.InvocationContext} for {@link jakarta.interceptor.AroundInvoke} interceptors. *

* A new instance is created for the first interceptor in the chain. Furthermore, subsequent interceptors receive a new instance * of {@link NextAroundInvokeInvocationContext}. This does not comply with the spec but allows for "asynchronous continuation" diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InnerInvocationContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InnerInvocationContext.java index 26b617dcb1869..e37c23950c9e6 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InnerInvocationContext.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InnerInvocationContext.java @@ -11,6 +11,11 @@ import io.quarkus.arc.ArcInvocationContext; +/** + * Invocation context for an "inner" invocation chain, consisting of interceptor methods declared + * in one class and its superclasses. It doesn't proceed to other interceptors in the "outer" invocation + * chain (interceptor methods declared in other classes). + */ abstract class InnerInvocationContext implements ArcInvocationContext { protected final ArcInvocationContext delegate; diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/LifecycleCallbackInvocationContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/LifecycleCallbackInvocationContext.java index 3d70c8c910fd7..b84425940266e 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/LifecycleCallbackInvocationContext.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/LifecycleCallbackInvocationContext.java @@ -6,8 +6,8 @@ import java.util.Set; /** - * A simple stateful {@link javax.interceptor.InvocationContext} implementation used for {@link javax.annotation.PostConstruct} - * and {@link javax.annotation.PreDestroy} callbacks. + * A simple stateful {@link jakarta.interceptor.InvocationContext} implementation used for + * {@link jakarta.annotation.PostConstruct} and {@link jakarta.annotation.PreDestroy} callbacks. *

* All lifecycle callback interceptors of a specific chain must be invoked on the same thread. */ diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/SuperclassInvocationContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/SuperclassInvocationContext.java index 77d1f8c1cd48a..af27620fe3132 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/SuperclassInvocationContext.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/SuperclassInvocationContext.java @@ -7,7 +7,7 @@ import jakarta.interceptor.InvocationContext; /** - * A special {@link javax.interceptor.InvocationContext} that is used if multiple interceptor methods are declared in a + * A special {@link jakarta.interceptor.InvocationContext} that is used if multiple interceptor methods are declared in a * hierarchy of an interceptor class. *

* The interceptor methods defined by the superclasses are invoked before the interceptor method defined by the interceptor diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/TargetAroundInvokeInvocationContext.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/TargetAroundInvokeInvocationContext.java index 782f1768b8dfe..7647744e50a25 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/TargetAroundInvokeInvocationContext.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/TargetAroundInvokeInvocationContext.java @@ -6,7 +6,7 @@ import jakarta.interceptor.InvocationContext; /** - * A special {@link javax.interceptor.InvocationContext} that is used for around invoke methods declared in a hierarchy of a + * A special {@link jakarta.interceptor.InvocationContext} that is used for around invoke methods declared in a hierarchy of a * target class. *

* The interceptor methods defined by the superclasses are invoked before the interceptor method defined by the interceptor From 1adf1b7821508380d40eaa96ca9c0beedda203ba Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Wed, 19 Apr 2023 14:40:34 +0200 Subject: [PATCH 5/5] ArC: add tests for business method interceptors on target classes and fix some issues Co-authored-by: Martin Kouba --- .../io/quarkus/arc/processor/BeanInfo.java | 7 +- .../java/io/quarkus/arc/processor/Beans.java | 9 +- .../arc/processor/InterceptorInfo.java | 20 +-- .../io/quarkus/arc/processor/Methods.java | 68 ++++++---- .../processor/SubclassSkipPredicateTest.java | 2 +- ...sAndManySuperclassesWithOverridesTest.java | 64 ++++++++++ ...nvokeOnTargetClassAndSuperclassesTest.java | 45 +++++++ ...ClassAndSuperclassesWithOverridesTest.java | 53 ++++++++ .../AroundInvokeOnTargetClassTest.java | 38 ++++++ ...eAndManySuperclassesWithOverridesTest.java | 118 ++++++++++++++++++ ...getClassAndOutsideAndSuperclassesTest.java | 79 ++++++++++++ ...tsideAndSuperclassesWithOverridesTest.java | 80 ++++++++++++ ...oundInvokeOnTargetClassAndOutsideTest.java | 65 ++++++++++ 13 files changed, 604 insertions(+), 44 deletions(-) create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/AroundInvokeOnTargetClassAndManySuperclassesWithOverridesTest.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/AroundInvokeOnTargetClassAndSuperclassesTest.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/AroundInvokeOnTargetClassAndSuperclassesWithOverridesTest.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/AroundInvokeOnTargetClassTest.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/mixed/AroundInvokeOnTargetClassAndOutsideAndManySuperclassesWithOverridesTest.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/mixed/AroundInvokeOnTargetClassAndOutsideAndSuperclassesTest.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/mixed/AroundInvokeOnTargetClassAndOutsideAndSuperclassesWithOverridesTest.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/mixed/AroundInvokeOnTargetClassAndOutsideTest.java diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java index ead53d8a41703..ffa2a7995a2d6 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java @@ -619,8 +619,8 @@ private Map initInterceptedMethods(List } } - Set finalMethods = Methods.addInterceptedMethodCandidates(beanDeployment, target.get().asClass(), - candidates, classLevelBindings, bytecodeTransformerConsumer, transformUnproxyableClasses, aroundInvokes); + Set finalMethods = Methods.addInterceptedMethodCandidates(this, candidates, classLevelBindings, + bytecodeTransformerConsumer, transformUnproxyableClasses); if (!finalMethods.isEmpty()) { String additionalError = ""; if (finalMethods.stream().anyMatch(KotlinUtils::isNoninterceptableKotlinMethod)) { @@ -668,7 +668,8 @@ private Map initDecoratedMethods() { ClassInfo classInfo = target.get().asClass(); addDecoratedMethods(candidates, classInfo, classInfo, bound, new SubclassSkipPredicate(beanDeployment.getAssignabilityCheck()::isAssignableFrom, - beanDeployment.getBeanArchiveIndex(), beanDeployment.getObserverAndProducerMethods())); + beanDeployment.getBeanArchiveIndex(), beanDeployment.getObserverAndProducerMethods(), + beanDeployment.getAnnotationStore())); Map decoratedMethods = new HashMap<>(candidates.size()); for (Entry entry : candidates.entrySet()) { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java index 141acb7620184..035ffb1ffd091 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java @@ -650,10 +650,10 @@ static List getCallbacks(ClassInfo beanClass, DotName annotation, In } static List getAroundInvokes(ClassInfo beanClass, BeanDeployment deployment) { - List methods = new ArrayList<>(); AnnotationStore store = deployment.getAnnotationStore(); - Set processed = new HashSet<>(); + List methods = new ArrayList<>(); + List allMethods = new ArrayList<>(); ClassInfo aClass = beanClass; while (aClass != null) { int aroundInvokesFound = 0; @@ -661,13 +661,14 @@ static List getAroundInvokes(ClassInfo beanClass, BeanDeployment dep if (Modifier.isStatic(method.flags())) { continue; } - if (store.hasAnnotation(method, DotNames.AROUND_INVOKE) && !processed.contains(method.name())) { - methods.add(method); + if (store.hasAnnotation(method, DotNames.AROUND_INVOKE)) { + InterceptorInfo.addInterceptorMethod(allMethods, methods, method); if (++aroundInvokesFound > 1) { throw new DefinitionException( "Multiple @AroundInvoke interceptor methods declared on class: " + aClass); } } + allMethods.add(method); } DotName superTypeName = aClass.superName(); aClass = superTypeName == null || DotNames.OBJECT.equals(superTypeName) ? null diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java index 21beeb93509ed..06eaf3ea49a58 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java @@ -247,28 +247,30 @@ public int compareTo(InterceptorInfo other) { return getTarget().toString().compareTo(other.getTarget().toString()); } - private void addInterceptorMethod(List allMethods, List interceptorMethods, MethodInfo method) { + static void addInterceptorMethod(List allMethods, List interceptorMethods, MethodInfo method) { validateSignature(method); if (!isInterceptorMethodOverriden(allMethods, method)) { interceptorMethods.add(method); } } - private boolean isInterceptorMethodOverriden(Iterable allMethods, MethodInfo method) { + static boolean isInterceptorMethodOverriden(Iterable allMethods, MethodInfo method) { for (MethodInfo m : allMethods) { - if (m.name().equals(method.name()) && m.parametersCount() == 1 - && (m.parameterType(0).name().equals(DotNames.INVOCATION_CONTEXT) - || m.parameterType(0).name().equals(DotNames.ARC_INVOCATION_CONTEXT))) { + if (m.name().equals(method.name()) && hasInterceptorMethodParameter(m)) { return true; } } return false; } - private MethodInfo validateSignature(MethodInfo method) { - List parameters = method.parameterTypes(); - if (parameters.size() != 1 || !(parameters.get(0).name().equals(DotNames.INVOCATION_CONTEXT) - || parameters.get(0).name().equals(DotNames.ARC_INVOCATION_CONTEXT))) { + static boolean hasInterceptorMethodParameter(MethodInfo method) { + return method.parametersCount() == 1 + && (method.parameterType(0).name().equals(DotNames.INVOCATION_CONTEXT) + || method.parameterType(0).name().equals(DotNames.ARC_INVOCATION_CONTEXT)); + } + + private static MethodInfo validateSignature(MethodInfo method) { + if (!hasInterceptorMethodParameter(method)) { throw new IllegalStateException( "An interceptor method must accept exactly one parameter of type jakarta.interceptor.InvocationContext: " + method + " declared on " + method.declaringClass()); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Methods.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Methods.java index 1dc877ce6a2de..cff90da5a09d2 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Methods.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Methods.java @@ -52,6 +52,8 @@ final class Methods { public static final String TO_STRING = "toString"; static final Set IGNORED_METHODS = Set.of(INIT, CLINIT); + static final List OBSERVER_PRODUCER_ANNOTATIONS = List.of(DotNames.OBSERVES, DotNames.OBSERVES_ASYNC, + DotNames.PRODUCES); private Methods() { } @@ -149,15 +151,18 @@ static boolean isObjectToString(MethodInfo method) { return method.declaringClass().name().equals(DotNames.OBJECT) && method.name().equals(TO_STRING); } - static Set addInterceptedMethodCandidates(BeanDeployment beanDeployment, ClassInfo classInfo, + static Set addInterceptedMethodCandidates(BeanInfo bean, Map> candidates, List classLevelBindings, Consumer bytecodeTransformerConsumer, - boolean transformUnproxyableClasses, List aroundInvokes) { + boolean transformUnproxyableClasses) { + BeanDeployment beanDeployment = bean.getDeployment(); + ClassInfo classInfo = bean.getTarget().get().asClass(); return addInterceptedMethodCandidates(beanDeployment, classInfo, classInfo, candidates, Set.copyOf(classLevelBindings), bytecodeTransformerConsumer, transformUnproxyableClasses, new SubclassSkipPredicate(beanDeployment.getAssignabilityCheck()::isAssignableFrom, - beanDeployment.getBeanArchiveIndex(), beanDeployment.getObserverAndProducerMethods()), - false, new HashSet<>(), aroundInvokes); + beanDeployment.getBeanArchiveIndex(), beanDeployment.getObserverAndProducerMethods(), + beanDeployment.getAnnotationStore()), + false, new HashSet<>(), bean.hasAroundInvokes()); } private static Set addInterceptedMethodCandidates(BeanDeployment beanDeployment, ClassInfo classInfo, @@ -165,20 +170,21 @@ private static Set addInterceptedMethodCandidates(BeanDeployment bea Map> candidates, Set classLevelBindings, Consumer bytecodeTransformerConsumer, boolean transformUnproxyableClasses, SubclassSkipPredicate skipPredicate, boolean ignoreMethodLevelBindings, - Set noClassInterceptorsMethods, List aroundInvokes) { + Set noClassInterceptorsMethods, boolean targetHasAroundInvokes) { Set methodsFromWhichToRemoveFinal = new HashSet<>(); Set finalMethodsFoundAndNotChanged = new HashSet<>(); skipPredicate.startProcessing(classInfo, originalClassInfo); for (MethodInfo method : classInfo.methods()) { - if (aroundInvokes.contains(method)) { - // Around invoke method declared in the target class hierarchy + // Note that we must merge the bindings first + Set bindings = mergeBindings(beanDeployment, originalClassInfo, classLevelBindings, + ignoreMethodLevelBindings, method, noClassInterceptorsMethods); + if (bindings.isEmpty() && !targetHasAroundInvokes) { + // No bindings found and target class does not declare around invoke interceptor methods continue; } - Set merged = mergeBindings(beanDeployment, originalClassInfo, classLevelBindings, - ignoreMethodLevelBindings, method, noClassInterceptorsMethods); - if ((merged.isEmpty() && aroundInvokes.isEmpty()) || skipPredicate.test(method)) { + if (skipPredicate.test(method)) { continue; } boolean addToCandidates = true; @@ -191,7 +197,7 @@ private static Set addInterceptedMethodCandidates(BeanDeployment bea } } if (addToCandidates) { - candidates.computeIfAbsent(new Methods.MethodKey(method), key -> merged); + candidates.computeIfAbsent(new Methods.MethodKey(method), key -> bindings); } } skipPredicate.methodsProcessed(); @@ -208,7 +214,7 @@ private static Set addInterceptedMethodCandidates(BeanDeployment bea finalMethodsFoundAndNotChanged .addAll(addInterceptedMethodCandidates(beanDeployment, superClassInfo, classInfo, candidates, classLevelBindings, bytecodeTransformerConsumer, transformUnproxyableClasses, skipPredicate, - ignoreMethodLevelBindings, noClassInterceptorsMethods, aroundInvokes)); + ignoreMethodLevelBindings, noClassInterceptorsMethods, targetHasAroundInvokes)); } } @@ -218,7 +224,7 @@ private static Set addInterceptedMethodCandidates(BeanDeployment bea //interfaces can't have final methods addInterceptedMethodCandidates(beanDeployment, interfaceInfo, originalClassInfo, candidates, classLevelBindings, bytecodeTransformerConsumer, transformUnproxyableClasses, - skipPredicate, true, noClassInterceptorsMethods, aroundInvokes); + skipPredicate, true, noClassInterceptorsMethods, targetHasAroundInvokes); } } return finalMethodsFoundAndNotChanged; @@ -274,9 +280,7 @@ private static Set mergeBindings(BeanDeployment beanDeployme } if (Modifier.isPrivate(method.flags()) - && !Annotations.contains(methodAnnotations, DotNames.PRODUCES) - && !Annotations.contains(methodAnnotations, DotNames.OBSERVES) - && !Annotations.contains(methodAnnotations, DotNames.OBSERVES_ASYNC)) { + && !Annotations.containsAny(methodAnnotations, OBSERVER_PRODUCER_ANNOTATIONS)) { String message; if (methodLevelBindings.size() == 1) { message = String.format("%s will have no effect on method %s.%s() because the method is private.", @@ -442,24 +446,28 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str * This stateful predicate can be used to skip methods that should not be added to the generated subclass. *

* Don't forget to call {@link SubclassSkipPredicate#startProcessing(ClassInfo, ClassInfo)} before the methods are processed - * and - * {@link SubclassSkipPredicate#methodsProcessed()} afterwards. + * and {@link SubclassSkipPredicate#methodsProcessed()} afterwards. */ static class SubclassSkipPredicate implements Predicate { + private static final List INTERCEPTOR_ANNOTATIONS = List.of(DotNames.AROUND_INVOKE, DotNames.POST_CONSTRUCT, + DotNames.PRE_DESTROY); + private final BiFunction assignableFromFun; private final IndexView beanArchiveIndex; private final Set producersAndObservers; + private final AnnotationStore annotationStore; private ClassInfo clazz; private ClassInfo originalClazz; private List regularMethods; private Set bridgeMethods = new HashSet<>(); public SubclassSkipPredicate(BiFunction assignableFromFun, IndexView beanArchiveIndex, - Set producersAndObservers) { + Set producersAndObservers, AnnotationStore annotationStore) { this.assignableFromFun = assignableFromFun; this.beanArchiveIndex = beanArchiveIndex; this.producersAndObservers = producersAndObservers; + this.annotationStore = annotationStore; } void startProcessing(ClassInfo clazz, ClassInfo originalClazz) { @@ -487,7 +495,8 @@ public boolean test(MethodInfo method) { // Skip bridge methods that have a corresponding "implementation method" on the same class // The algorithm we use to detect these methods is best effort, i.e. there might be use cases where the detection fails return hasImplementation(method); - } else if (method.isSynthetic()) { + } + if (method.isSynthetic()) { // Skip non-bridge synthetic methods return true; } @@ -495,20 +504,25 @@ public boolean test(MethodInfo method) { // Skip a private method that is not and observer or producer return true; } - if (method.hasAnnotation(DotNames.POST_CONSTRUCT) || method.hasAnnotation(DotNames.PRE_DESTROY)) { - // @PreDestroy and @PostConstruct methods declared on the bean are NOT candidates for around invoke interception + if (Modifier.isStatic(method.flags())) { return true; } - if (isOverridenByBridgeMethod(method)) { + if (IGNORED_METHODS.contains(method.name())) { return true; } - if (Modifier.isStatic(method.flags())) { + if (method.declaringClass().name().equals(DotNames.OBJECT)) { return true; } - if (IGNORED_METHODS.contains(method.name())) { + if (annotationStore.hasAnyAnnotation(method, INTERCEPTOR_ANNOTATIONS)) { + // @AroundInvoke, @PreDestroy and @PostConstruct methods declared on the bean are NOT candidates for around invoke interception return true; } - if (method.declaringClass().name().equals(DotNames.OBJECT)) { + if (InterceptorInfo.hasInterceptorMethodParameter(method) + && InterceptorInfo.isInterceptorMethodOverriden(regularMethods, method)) { + // Has exactly one param InvocationContext/ArcInvocationContext and is overriden + return true; + } + if (isOverridenByBridgeMethod(method)) { return true; } if (Modifier.isInterface(clazz.flags()) && Modifier.isInterface(method.declaringClass().flags()) @@ -527,7 +541,7 @@ public boolean test(MethodInfo method) { } DotName typeName = type.name(); if (type.kind() == Kind.ARRAY) { - Type componentType = type.asArrayType().component(); + Type componentType = type.asArrayType().constituent(); if (componentType.kind() == Kind.PRIMITIVE) { continue; } diff --git a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/SubclassSkipPredicateTest.java b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/SubclassSkipPredicateTest.java index 5b73e11ba2f31..0035ce54cdb7b 100644 --- a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/SubclassSkipPredicateTest.java +++ b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/SubclassSkipPredicateTest.java @@ -27,7 +27,7 @@ public void testPredicate() throws IOException { IndexView index = Index.of(Base.class, Submarine.class, Long.class, Number.class); AssignabilityCheck assignabilityCheck = new AssignabilityCheck(index, null); SubclassSkipPredicate predicate = new SubclassSkipPredicate(assignabilityCheck::isAssignableFrom, null, - Collections.emptySet()); + Collections.emptySet(), new AnnotationStore(Collections.emptyList(), null)); ClassInfo submarineClass = index.getClassByName(DotName.createSimple(Submarine.class.getName())); predicate.startProcessing(submarineClass, submarineClass); diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/AroundInvokeOnTargetClassAndManySuperclassesWithOverridesTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/AroundInvokeOnTargetClassAndManySuperclassesWithOverridesTest.java new file mode 100644 index 0000000000000..179cff5fcd517 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/AroundInvokeOnTargetClassAndManySuperclassesWithOverridesTest.java @@ -0,0 +1,64 @@ +package io.quarkus.arc.test.interceptors.targetclass; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Singleton; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.InvocationContext; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.test.ArcTestContainer; + +public class AroundInvokeOnTargetClassAndManySuperclassesWithOverridesTest { + @RegisterExtension + public ArcTestContainer container = new ArcTestContainer(MyBean.class); + + @Test + public void test() { + ArcContainer arc = Arc.container(); + MyBean bean = arc.instance(MyBean.class).get(); + assertEquals("super-intercepted: intercepted: foobar", bean.doSomething(42)); + } + + static class Alpha { + @AroundInvoke + Object intercept(InvocationContext ctx) throws Exception { + return "this should not be called as the method is overridden in MyBean"; + } + } + + static class Bravo extends Alpha { + @AroundInvoke + Object specialIntercept(InvocationContext ctx) throws Exception { + return "this should not be called as the method is overridden in Charlie"; + } + } + + static class Charlie extends Bravo { + @AroundInvoke + Object superIntercept(InvocationContext ctx) throws Exception { + return "super-intercepted: " + ctx.proceed(); + } + + @Override + Object specialIntercept(InvocationContext ctx) { + return "this is not an interceptor method"; + } + } + + @Singleton + static class MyBean extends Charlie { + String doSomething(int param) { + return "foobar"; + } + + @AroundInvoke + Object intercept(InvocationContext ctx) throws Exception { + return "intercepted: " + ctx.proceed(); + } + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/AroundInvokeOnTargetClassAndSuperclassesTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/AroundInvokeOnTargetClassAndSuperclassesTest.java new file mode 100644 index 0000000000000..9f7e6e786b601 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/AroundInvokeOnTargetClassAndSuperclassesTest.java @@ -0,0 +1,45 @@ +package io.quarkus.arc.test.interceptors.targetclass; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Singleton; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.InvocationContext; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.test.ArcTestContainer; + +public class AroundInvokeOnTargetClassAndSuperclassesTest { + @RegisterExtension + public ArcTestContainer container = new ArcTestContainer(MyBean.class); + + @Test + public void test() { + ArcContainer arc = Arc.container(); + MyBean bean = arc.instance(MyBean.class).get(); + assertEquals("super-intercepted: intercepted: foobar", bean.doSomething()); + } + + static class MyBeanSuperclass { + @AroundInvoke + Object superIntercept(InvocationContext ctx) throws Exception { + return "super-intercepted: " + ctx.proceed(); + } + } + + @Singleton + static class MyBean extends MyBeanSuperclass { + String doSomething() { + return "foobar"; + } + + @AroundInvoke + Object intercept(InvocationContext ctx) throws Exception { + return "intercepted: " + ctx.proceed(); + } + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/AroundInvokeOnTargetClassAndSuperclassesWithOverridesTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/AroundInvokeOnTargetClassAndSuperclassesWithOverridesTest.java new file mode 100644 index 0000000000000..a1712db87110a --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/AroundInvokeOnTargetClassAndSuperclassesWithOverridesTest.java @@ -0,0 +1,53 @@ +package io.quarkus.arc.test.interceptors.targetclass; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Singleton; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.InvocationContext; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.test.ArcTestContainer; + +public class AroundInvokeOnTargetClassAndSuperclassesWithOverridesTest { + @RegisterExtension + public ArcTestContainer container = new ArcTestContainer(MyBean.class); + + @Test + public void test() { + ArcContainer arc = Arc.container(); + MyBean bean = arc.instance(MyBean.class).get(); + assertEquals("super-intercepted: intercepted: foobar", bean.doSomething()); + } + + static class Alpha { + @AroundInvoke + Object intercept(InvocationContext ctx) throws Exception { + return "this should not be called as the method is overridden in MyBean"; + } + } + + static class Bravo extends Alpha { + @AroundInvoke + Object superIntercept(InvocationContext ctx) throws Exception { + return "super-intercepted: " + ctx.proceed(); + } + } + + @Singleton + static class MyBean extends Bravo { + String doSomething() { + return "foobar"; + } + + @Override + @AroundInvoke + Object intercept(InvocationContext ctx) throws Exception { + return "intercepted: " + ctx.proceed(); + } + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/AroundInvokeOnTargetClassTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/AroundInvokeOnTargetClassTest.java new file mode 100644 index 0000000000000..89587eab4c3ae --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/AroundInvokeOnTargetClassTest.java @@ -0,0 +1,38 @@ +package io.quarkus.arc.test.interceptors.targetclass; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Singleton; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.InvocationContext; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.test.ArcTestContainer; + +public class AroundInvokeOnTargetClassTest { + @RegisterExtension + public ArcTestContainer container = new ArcTestContainer(MyBean.class); + + @Test + public void test() { + ArcContainer arc = Arc.container(); + MyBean bean = arc.instance(MyBean.class).get(); + assertEquals("intercepted: foobar", bean.doSomething()); + } + + @Singleton + static class MyBean { + String doSomething() { + return "foobar"; + } + + @AroundInvoke + Object intercept(InvocationContext ctx) throws Exception { + return "intercepted: " + ctx.proceed(); + } + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/mixed/AroundInvokeOnTargetClassAndOutsideAndManySuperclassesWithOverridesTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/mixed/AroundInvokeOnTargetClassAndOutsideAndManySuperclassesWithOverridesTest.java new file mode 100644 index 0000000000000..7dc4a90501ad5 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/mixed/AroundInvokeOnTargetClassAndOutsideAndManySuperclassesWithOverridesTest.java @@ -0,0 +1,118 @@ +package io.quarkus.arc.test.interceptors.targetclass.mixed; + +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 io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.test.ArcTestContainer; + +public class AroundInvokeOnTargetClassAndOutsideAndManySuperclassesWithOverridesTest { + @RegisterExtension + public ArcTestContainer container = new ArcTestContainer(MyBean.class, MyInterceptorBinding.class, MyInterceptor.class); + + @Test + public void test() { + ArcContainer arc = Arc.container(); + MyBean bean = arc.instance(MyBean.class).get(); + assertEquals("super-outside: outside: super-target: target: foobar", bean.doSomething()); + } + + static class Alpha { + @AroundInvoke + Object intercept(InvocationContext ctx) throws Exception { + return "this should not be called as the method is overridden in MyBean"; + } + } + + static class Bravo extends Alpha { + @AroundInvoke + Object specialIntercept(InvocationContext ctx) { + return "this should not be called as the method is overridden in Charlie"; + } + } + + static class Charlie extends Bravo { + @AroundInvoke + Object superIntercept(InvocationContext ctx) throws Exception { + return "super-target: " + ctx.proceed(); + } + + @Override + Object specialIntercept(InvocationContext ctx) { + return "this is not an interceptor method"; + } + } + + @Singleton + @MyInterceptorBinding + static class MyBean extends Charlie { + String doSomething() { + return "foobar"; + } + + @Override + @AroundInvoke + Object intercept(InvocationContext ctx) throws Exception { + return "target: " + ctx.proceed(); + } + } + + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @InterceptorBinding + @interface MyInterceptorBinding { + } + + static class Delta { + @AroundInvoke + Object intercept(InvocationContext ctx) throws Exception { + return "this should not be called as the method is overridden in MyInterceptor"; + } + } + + static class Echo extends Delta { + @AroundInvoke + Object specialIntercept(InvocationContext ctx) throws Exception { + return "this should not be called as the method is overridden in Charlie"; + } + } + + static class Foxtrot extends Echo { + @AroundInvoke + Object superIntercept(InvocationContext ctx) throws Exception { + return "super-outside: " + ctx.proceed(); + } + + @Override + Object specialIntercept(InvocationContext ctx) { + return "this is not an interceptor method"; + } + } + + @MyInterceptorBinding + @Interceptor + @Priority(1) + static class MyInterceptor extends Foxtrot { + @AroundInvoke + Object intercept(InvocationContext ctx) throws Exception { + return "outside: " + ctx.proceed(); + } + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/mixed/AroundInvokeOnTargetClassAndOutsideAndSuperclassesTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/mixed/AroundInvokeOnTargetClassAndOutsideAndSuperclassesTest.java new file mode 100644 index 0000000000000..a0dc9d112270a --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/mixed/AroundInvokeOnTargetClassAndOutsideAndSuperclassesTest.java @@ -0,0 +1,79 @@ +package io.quarkus.arc.test.interceptors.targetclass.mixed; + +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 io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.test.ArcTestContainer; + +public class AroundInvokeOnTargetClassAndOutsideAndSuperclassesTest { + @RegisterExtension + public ArcTestContainer container = new ArcTestContainer(MyBean.class, MyInterceptorBinding.class, MyInterceptor.class); + + @Test + public void test() { + ArcContainer arc = Arc.container(); + MyBean bean = arc.instance(MyBean.class).get(); + assertEquals("super-outside: outside: super-target: target: foobar", bean.doSomething()); + } + + static class MyBeanSuperclass { + @AroundInvoke + Object superIntercept(InvocationContext ctx) throws Exception { + return "super-target: " + ctx.proceed(); + } + } + + @Singleton + @MyInterceptorBinding + static class MyBean extends MyBeanSuperclass { + String doSomething() { + return "foobar"; + } + + @AroundInvoke + Object intercept(InvocationContext ctx) throws Exception { + return "target: " + ctx.proceed(); + } + } + + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @InterceptorBinding + @interface MyInterceptorBinding { + } + + static class MyInterceptorSuperclass { + @AroundInvoke + Object superIntercept(InvocationContext ctx) throws Exception { + return "super-outside: " + ctx.proceed(); + } + } + + @MyInterceptorBinding + @Interceptor + @Priority(1) + static class MyInterceptor extends MyInterceptorSuperclass { + @AroundInvoke + Object intercept(InvocationContext ctx) throws Exception { + return "outside: " + ctx.proceed(); + } + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/mixed/AroundInvokeOnTargetClassAndOutsideAndSuperclassesWithOverridesTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/mixed/AroundInvokeOnTargetClassAndOutsideAndSuperclassesWithOverridesTest.java new file mode 100644 index 0000000000000..d5b89eb78cb0c --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/mixed/AroundInvokeOnTargetClassAndOutsideAndSuperclassesWithOverridesTest.java @@ -0,0 +1,80 @@ +package io.quarkus.arc.test.interceptors.targetclass.mixed; + +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 io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.test.ArcTestContainer; + +public class AroundInvokeOnTargetClassAndOutsideAndSuperclassesWithOverridesTest { + @RegisterExtension + public ArcTestContainer container = new ArcTestContainer(MyBean.class, MyInterceptorBinding.class, MyInterceptor.class); + + @Test + public void test() { + ArcContainer arc = Arc.container(); + MyBean bean = arc.instance(MyBean.class).get(); + assertEquals("super-outside: outside: target: foobar", bean.doSomething()); + } + + static class MyBeanSuperclass { + @AroundInvoke + Object intercept(InvocationContext ctx) throws Exception { + return "this should not be called as the method is overridden in MyBean"; + } + } + + @Singleton + @MyInterceptorBinding + static class MyBean extends MyBeanSuperclass { + String doSomething() { + return "foobar"; + } + + @Override + @AroundInvoke + Object intercept(InvocationContext ctx) throws Exception { + return "target: " + ctx.proceed(); + } + } + + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @InterceptorBinding + @interface MyInterceptorBinding { + } + + static class MyInterceptorSuperclass { + @AroundInvoke + Object superIntercept(InvocationContext ctx) throws Exception { + return "super-outside: " + ctx.proceed(); + } + } + + @MyInterceptorBinding + @Interceptor + @Priority(1) + static class MyInterceptor extends MyInterceptorSuperclass { + @AroundInvoke + Object intercept(InvocationContext ctx) throws Exception { + return "outside: " + ctx.proceed(); + } + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/mixed/AroundInvokeOnTargetClassAndOutsideTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/mixed/AroundInvokeOnTargetClassAndOutsideTest.java new file mode 100644 index 0000000000000..a21c8471834ab --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/interceptors/targetclass/mixed/AroundInvokeOnTargetClassAndOutsideTest.java @@ -0,0 +1,65 @@ +package io.quarkus.arc.test.interceptors.targetclass.mixed; + +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 io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.test.ArcTestContainer; + +public class AroundInvokeOnTargetClassAndOutsideTest { + @RegisterExtension + public ArcTestContainer container = new ArcTestContainer(MyBean.class, MyInterceptorBinding.class, MyInterceptor.class); + + @Test + public void test() { + ArcContainer arc = Arc.container(); + MyBean bean = arc.instance(MyBean.class).get(); + assertEquals("outside: target: foobar", bean.doSomething()); + } + + @Singleton + @MyInterceptorBinding + static class MyBean { + String doSomething() { + return "foobar"; + } + + @AroundInvoke + Object intercept(InvocationContext ctx) throws Exception { + return "target: " + ctx.proceed(); + } + } + + @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 "outside: " + ctx.proceed(); + } + } +}