From f5994eb9b971c37bb1e8c06afc2f952b8e7266e3 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 16 Jun 2023 15:11:05 +0200 Subject: [PATCH] ArC: add synthetic interceptors API --- .../quarkus/arc/processor/BeanDeployment.java | 19 ++ .../quarkus/arc/processor/BeanGenerator.java | 7 +- .../quarkus/arc/processor/BeanRegistrar.java | 11 ++ .../io/quarkus/arc/processor/Injection.java | 22 ++- .../processor/InterceptorConfigurator.java | 59 ++++++ .../arc/processor/InterceptorGenerator.java | 145 ++++++++++++--- .../arc/processor/InterceptorInfo.java | 49 +++++ .../arc/processor/MethodDescriptors.java | 5 + .../io/quarkus/arc/InterceptorCreator.java | 29 +++ .../beans/SyntheticInterceptorTest.java | 173 ++++++++++++++++++ 10 files changed, 485 insertions(+), 34 deletions(-) create mode 100644 independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorConfigurator.java create mode 100644 independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InterceptorCreator.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticInterceptorTest.java diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java index f2300ebf40ca8..b990a86c41b75 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java @@ -29,6 +29,7 @@ import jakarta.enterprise.event.Reception; import jakarta.enterprise.inject.spi.DefinitionException; import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.enterprise.inject.spi.InterceptionType; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; @@ -1414,6 +1415,10 @@ private void addSyntheticBean(BeanInfo bean) { beans.add(bean); } + void addSyntheticInterceptor(InterceptorInfo interceptor) { + interceptors.add(interceptor); + } + private void addSyntheticObserver(ObserverConfigurator configurator) { observers.add(ObserverInfo.create(configurator.id, this, configurator.beanClass, null, null, null, null, configurator.observedType, @@ -1688,6 +1693,20 @@ public BeanConfigurator configure(DotName beanClassName) { return new BeanConfigurator(beanClassName, beanDeployment, this); } + @Override + public InterceptorConfigurator configureInterceptor(InterceptionType interceptionType) { + switch (Objects.requireNonNull(interceptionType)) { + case AROUND_INVOKE: + case POST_CONSTRUCT: + case PRE_DESTROY: + case AROUND_CONSTRUCT: + return new InterceptorConfigurator(beanDeployment, interceptionType); + default: + throw new IllegalArgumentException("Unsuppored interception type: " + interceptionType); + } + + } + @Override public void accept(BeanInfo bean) { beanDeployment.addSyntheticBean(bean); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java index 40f5469aa85b4..db25b5409e0ce 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java @@ -126,7 +126,9 @@ public BeanGenerator(AnnotationLiteralProcessor annotationLiterals, Predicate generate(BeanInfo bean) { - if (bean.getTarget().isPresent()) { + if (bean.isSynthetic()) { + return generateSyntheticBean(bean); + } else { AnnotationTarget target = bean.getTarget().get(); switch (target.kind()) { case CLASS: @@ -138,9 +140,6 @@ Collection generate(BeanInfo bean) { default: throw new IllegalArgumentException("Unsupported bean type"); } - } else { - // Synthetic beans - return generateSyntheticBean(bean); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanRegistrar.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanRegistrar.java index e66eb84dd4be3..25915ed724cc2 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanRegistrar.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanRegistrar.java @@ -2,6 +2,8 @@ import java.util.Collection; +import jakarta.enterprise.inject.spi.InterceptionType; + import org.jboss.jandex.DotName; /** @@ -39,6 +41,15 @@ default BeanConfigurator configure(Class beanClass) { return configure(DotName.createSimple(beanClass.getName())); } + /** + * Configure a new synthetic interceptor. The interceptor is not added to the deployment unless the + * {@link InterceptorConfigurator#creator(Class)} method is called. + * + * @param interceptionType + * @return a new synthetic interceptor configurator + */ + InterceptorConfigurator configureInterceptor(InterceptionType interceptionType); + /** * The returned stream contains all non-synthetic beans (beans derived from classes) and beans * registered by other {@link BeanRegistrar}s before the stream is created. diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Injection.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Injection.java index 47f5e780daa52..baf0e95c00a96 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Injection.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Injection.java @@ -44,13 +44,21 @@ public class Injection { private static final Logger LOGGER = Logger.getLogger(Injection.class); static Injection forSyntheticBean(Iterable injectionPoints) { - List ips = new ArrayList<>(); + return forSynthetic(injectionPoints, BeanType.SYNTHETIC_BEAN); + } + + static Injection forSyntheticInterceptor(Iterable injectionPoints) { + return forSynthetic(injectionPoints, BeanType.SYNTHETIC_INTERCEPTOR); + } + + private static Injection forSynthetic(Iterable injectionPoints, BeanType beanType) { + List ret = new ArrayList<>(); for (TypeAndQualifiers injectionPoint : injectionPoints) { - InjectionPointInfo injectionPointInfo = InjectionPointInfo.fromSyntheticInjectionPoint(injectionPoint); - validateInjections(injectionPointInfo, BeanType.SYNTHETIC_BEAN); - ips.add(injectionPointInfo); + InjectionPointInfo ip = InjectionPointInfo.fromSyntheticInjectionPoint(injectionPoint); + validateInjections(ip, beanType); + ret.add(ip); } - return new Injection(null, ips); + return new Injection(null, ret); } private static void validateInjections(InjectionPointInfo injectionPointInfo, BeanType beanType) { @@ -78,7 +86,8 @@ private static void validateInjections(InjectionPointInfo injectionPointInfo, Be // declaring the injection point if (injectionPointInfo.getRequiredType().name().equals(DotNames.BEAN) && injectionPointInfo.getRequiredType().kind() == Type.Kind.PARAMETERIZED_TYPE - && injectionPointInfo.getRequiredType().asParameterizedType().arguments().size() == 1) { + && injectionPointInfo.getRequiredType().asParameterizedType().arguments().size() == 1 + && injectionPointInfo.hasDefaultedQualifier()) { Type actualType = injectionPointInfo.getRequiredType().asParameterizedType().arguments().get(0); AnnotationTarget ipTarget = injectionPointInfo.getTarget(); DotName expectedType = null; @@ -255,6 +264,7 @@ static enum BeanType { PRODUCER_METHOD, SYNTHETIC_BEAN, INTERCEPTOR, + SYNTHETIC_INTERCEPTOR, DECORATOR } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorConfigurator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorConfigurator.java new file mode 100644 index 0000000000000..278bcaf369f03 --- /dev/null +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorConfigurator.java @@ -0,0 +1,59 @@ +package io.quarkus.arc.processor; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.inject.spi.InterceptionType; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.Type; + +import io.quarkus.arc.InterceptorCreator; +import io.quarkus.arc.processor.InjectionPointInfo.TypeAndQualifiers; + +/** + * This construct is not thread-safe. + */ +public final class InterceptorConfigurator extends ConfiguratorBase { + + private final BeanDeployment beanDeployment; + + final InterceptionType type; + final Set injectionPoints; + final Set bindings; + int priority; + + InterceptorConfigurator(BeanDeployment beanDeployment, InterceptionType type) { + this.beanDeployment = beanDeployment; + this.type = type; + this.injectionPoints = new HashSet<>(); + this.bindings = new HashSet<>(); + this.priority = 1; + } + + public InterceptorConfigurator priority(int priority) { + this.priority = priority; + return this; + } + + public InterceptorConfigurator bindings(AnnotationInstance... bindings) { + Collections.addAll(this.bindings, bindings); + return this; + } + + public InterceptorConfigurator addInjectionPoint(Type requiredType, AnnotationInstance... requiredQualifiers) { + this.injectionPoints.add(new TypeAndQualifiers(requiredType, + requiredQualifiers.length == 0 ? Set.of(AnnotationInstance.builder(Default.class).build()) + : Set.of(requiredQualifiers))); + return this; + } + + public void creator(Class creatorClass) { + beanDeployment.addSyntheticInterceptor(new InterceptorInfo(creatorClass, beanDeployment, bindings, + List.of(Injection.forSyntheticInterceptor(injectionPoints)), priority, type, params)); + } + +} 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 b710f6633ac99..57295af6ab5fc 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 @@ -28,6 +28,7 @@ import io.quarkus.arc.ArcInvocationContext; import io.quarkus.arc.InjectableInterceptor; +import io.quarkus.arc.InterceptorCreator.InterceptFunction; import io.quarkus.arc.processor.BeanProcessor.PrivateMembersCollector; import io.quarkus.arc.processor.ResourceOutput.Resource; import io.quarkus.arc.processor.ResourceOutput.Resource.SpecialType; @@ -67,15 +68,22 @@ public InterceptorGenerator(AnnotationLiteralProcessor annotationLiterals, Predi */ void precomputeGeneratedName(InterceptorInfo interceptor) { ProviderType providerType = new ProviderType(interceptor.getProviderType()); - ClassInfo interceptorClass = interceptor.getTarget().get().asClass(); String baseName; - if (interceptorClass.enclosingClass() != null) { - baseName = DotNames.simpleName(interceptorClass.enclosingClass()) + "_" + DotNames.simpleName(interceptorClass); + String targetPackage; + if (interceptor.isSynthetic()) { + DotName creatorClassName = DotName.createSimple(interceptor.getCreatorClass()); + baseName = InterceptFunction.class.getSimpleName() + "_" + interceptor.getIdentifier(); + targetPackage = DotNames.packageName(creatorClassName); } else { - baseName = DotNames.simpleName(interceptorClass); + ClassInfo interceptorClass = interceptor.getTarget().get().asClass(); + if (interceptorClass.enclosingClass() != null) { + baseName = DotNames.simpleName(interceptorClass.enclosingClass()) + "_" + DotNames.simpleName(interceptorClass); + } else { + baseName = DotNames.simpleName(interceptorClass); + } + targetPackage = DotNames.packageName(providerType.name()); } beanToGeneratedBaseName.put(interceptor, baseName); - String targetPackage = DotNames.packageName(providerType.name()); String generatedName = generatedNameFromTarget(targetPackage, baseName, BEAN_SUFFIX); beanToGeneratedName.put(interceptor, generatedName); } @@ -86,6 +94,71 @@ void precomputeGeneratedName(InterceptorInfo interceptor) { * @return a collection of resources */ Collection generate(InterceptorInfo interceptor) { + return interceptor.isSynthetic() ? generateSyntheticInterceptor(interceptor) : generateClassInterceptor(interceptor); + } + + private Collection generateSyntheticInterceptor(InterceptorInfo interceptor) { + ProviderType providerType = new ProviderType(interceptor.getProviderType()); + DotName creatorClassName = DotName.createSimple(interceptor.getCreatorClass()); + String baseName = beanToGeneratedBaseName.get(interceptor); + String targetPackage = DotNames.packageName(creatorClassName); + String generatedName = beanToGeneratedName.get(interceptor); + + if (existingClasses.contains(generatedName)) { + return Collections.emptyList(); + } + + boolean isApplicationClass = applicationClassPredicate.test(creatorClassName); + ResourceClassOutput classOutput = new ResourceClassOutput(isApplicationClass, + name -> name.equals(generatedName) ? SpecialType.INTERCEPTOR_BEAN : null, generateSources); + + // MyInterceptor_Bean implements InjectableInterceptor + ClassCreator interceptorBean = ClassCreator.builder().classOutput(classOutput).className(generatedName) + .interfaces(InjectableInterceptor.class, Supplier.class) + .build(); + + // Fields + FieldCreator beanTypes = interceptorBean.getFieldCreator(FIELD_NAME_BEAN_TYPES, Set.class) + .setModifiers(ACC_PRIVATE | ACC_FINAL); + FieldCreator bindings = interceptorBean.getFieldCreator(FIELD_NAME_BINDINGS, Set.class) + .setModifiers(ACC_PRIVATE | ACC_FINAL); + + Map injectionPointToProviderField = new HashMap<>(); + initMaps(interceptor, injectionPointToProviderField, Collections.emptyMap(), Collections.emptyMap()); + + createProviderFields(interceptorBean, interceptor, injectionPointToProviderField, Collections.emptyMap(), + Collections.emptyMap()); + MethodCreator constructor = createConstructor(classOutput, interceptorBean, interceptor, injectionPointToProviderField, + bindings.getFieldDescriptor(), reflectionRegistration, isApplicationClass, providerType); + SyntheticComponentsUtil.addParamsFieldAndInit(interceptorBean, constructor, interceptor.getParams(), annotationLiterals, + interceptor.getDeployment().getBeanArchiveIndex()); + constructor.returnValue(null); + + implementGetIdentifier(interceptor, interceptorBean); + implementSupplierGet(interceptorBean); + implementCreate(classOutput, interceptorBean, interceptor, providerType, baseName, + injectionPointToProviderField, + Collections.emptyMap(), Collections.emptyMap(), + targetPackage, isApplicationClass); + implementGet(interceptor, interceptorBean, providerType, baseName); + implementGetTypes(interceptorBean, beanTypes.getFieldDescriptor()); + implementGetBeanClass(interceptor, interceptorBean); + // Interceptors are always @Dependent and have always default qualifiers + + // InjectableInterceptor methods + implementGetInterceptorBindings(interceptorBean, bindings.getFieldDescriptor()); + implementIntercepts(interceptorBean, interceptor); + implementIntercept(interceptorBean, interceptor, providerType, reflectionRegistration, isApplicationClass); + implementGetPriority(interceptorBean, interceptor); + + implementEquals(interceptor, interceptorBean); + implementHashCode(interceptor, interceptorBean); + + interceptorBean.close(); + return classOutput.getResources(); + } + + private Collection generateClassInterceptor(InterceptorInfo interceptor) { ProviderType providerType = new ProviderType(interceptor.getProviderType()); String baseName = beanToGeneratedBaseName.get(interceptor); String targetPackage = DotNames.packageName(providerType.name()); @@ -116,7 +189,7 @@ Collection generate(InterceptorInfo interceptor) { createProviderFields(interceptorCreator, interceptor, injectionPointToProviderField, Collections.emptyMap(), Collections.emptyMap()); createConstructor(classOutput, interceptorCreator, interceptor, injectionPointToProviderField, - bindings.getFieldDescriptor(), reflectionRegistration, isApplicationClass, providerType); + bindings.getFieldDescriptor(), reflectionRegistration, isApplicationClass, providerType).returnValue(null); implementGetIdentifier(interceptor, interceptorCreator); implementSupplierGet(interceptorCreator); @@ -140,10 +213,9 @@ Collection generate(InterceptorInfo interceptor) { interceptorCreator.close(); return classOutput.getResources(); - } - protected void createConstructor(ClassOutput classOutput, ClassCreator creator, InterceptorInfo interceptor, + protected MethodCreator createConstructor(ClassOutput classOutput, ClassCreator creator, InterceptorInfo interceptor, Map injectionPointToProviderField, FieldDescriptor bindings, ReflectionRegistration reflectionRegistration, boolean isApplicationClass, ProviderType providerType) { @@ -172,7 +244,7 @@ protected void createConstructor(ClassOutput classOutput, ClassCreator creator, initInterceptorMethodsField(creator, constructor, InterceptionType.PRE_DESTROY, interceptor.getPreDestroys(), providerType.className(), isApplicationClass); - constructor.returnValue(null); + return constructor; } private void initInterceptorMethodsField(ClassCreator creator, MethodCreator constructor, InterceptionType interceptionType, @@ -196,6 +268,12 @@ private void initInterceptorMethodsField(ClassCreator creator, MethodCreator con constructor.writeInstanceField(field.getFieldDescriptor(), constructor.getThis(), methodsList); } + protected void implementGetBeanClass(InterceptorInfo interceptor, ClassCreator beanCreator) { + MethodCreator getBeanClass = beanCreator.getMethodCreator("getBeanClass", Class.class).setModifiers(ACC_PUBLIC); + getBeanClass.returnValue(getBeanClass.loadClass( + interceptor.isSynthetic() ? interceptor.getCreatorClass().getName() : interceptor.getBeanClass().toString())); + } + /** * * @see InjectableInterceptor#getInterceptorBindings() @@ -222,10 +300,20 @@ protected void implementGetPriority(ClassCreator creator, InterceptorInfo interc protected void implementIntercepts(ClassCreator creator, InterceptorInfo interceptor) { MethodCreator intercepts = creator.getMethodCreator("intercepts", boolean.class, InterceptionType.class) .setModifiers(ACC_PUBLIC); - addIntercepts(interceptor, InterceptionType.AROUND_INVOKE, intercepts); - addIntercepts(interceptor, InterceptionType.POST_CONSTRUCT, intercepts); - addIntercepts(interceptor, InterceptionType.PRE_DESTROY, intercepts); - addIntercepts(interceptor, InterceptionType.AROUND_CONSTRUCT, intercepts); + if (interceptor.isSynthetic()) { + ResultHandle enumValue = intercepts + .readStaticField( + FieldDescriptor.of(InterceptionType.class.getName(), interceptor.getInterceptionType().name(), + InterceptionType.class.getName())); + BranchResult result = intercepts + .ifTrue(Gizmo.equals(intercepts, enumValue, intercepts.getMethodParam(0))); + result.trueBranch().returnValue(result.trueBranch().load(true)); + } else { + addIntercepts(interceptor, InterceptionType.AROUND_INVOKE, intercepts); + addIntercepts(interceptor, InterceptionType.POST_CONSTRUCT, intercepts); + addIntercepts(interceptor, InterceptionType.PRE_DESTROY, intercepts); + addIntercepts(interceptor, InterceptionType.AROUND_CONSTRUCT, intercepts); + } intercepts.returnValue(intercepts.load(false)); } @@ -235,9 +323,8 @@ private void addIntercepts(InterceptorInfo interceptor, InterceptionType interce .readStaticField(FieldDescriptor.of(InterceptionType.class.getName(), interceptionType.name(), InterceptionType.class.getName())); BranchResult result = intercepts - .ifNonZero(intercepts.invokeVirtualMethod(MethodDescriptors.OBJECT_EQUALS, enumValue, - intercepts.getMethodParam(0))); - result.trueBranch().returnValue(result.trueBranch().load(true)); + .ifTrue(Gizmo.equals(intercepts, enumValue, intercepts.getMethodParam(0))); + result.trueBranch().returnBoolean(true); } } @@ -251,14 +338,24 @@ protected void implementIntercept(ClassCreator creator, InterceptorInfo intercep .getMethodCreator("intercept", Object.class, InterceptionType.class, Object.class, InvocationContext.class) .setModifiers(ACC_PUBLIC).addException(Exception.class); - addIntercept(creator, intercept, interceptor.getAroundInvokes(), InterceptionType.AROUND_INVOKE, providerType, - reflectionRegistration, isApplicationClass); - addIntercept(creator, intercept, interceptor.getPostConstructs(), InterceptionType.POST_CONSTRUCT, providerType, - reflectionRegistration, isApplicationClass); - addIntercept(creator, intercept, interceptor.getPreDestroys(), InterceptionType.PRE_DESTROY, providerType, - reflectionRegistration, isApplicationClass); - addIntercept(creator, intercept, interceptor.getAroundConstructs(), InterceptionType.AROUND_CONSTRUCT, providerType, - reflectionRegistration, isApplicationClass); + if (interceptor.isSynthetic()) { + BranchResult result = intercept + .ifTrue(Gizmo.equals(intercept, intercept.load(interceptor.getInterceptionType()), + intercept.getMethodParam(0))); + BytecodeCreator trueBranch = result.trueBranch(); + ResultHandle interceptFunction = trueBranch.checkCast(trueBranch.getMethodParam(1), InterceptFunction.class); + trueBranch.returnValue(trueBranch.invokeInterfaceMethod(MethodDescriptors.INTERCEPT_FUNCTION_INTERCEPT, + interceptFunction, trueBranch.getMethodParam(2))); + } else { + addIntercept(creator, intercept, interceptor.getAroundInvokes(), InterceptionType.AROUND_INVOKE, providerType, + reflectionRegistration, isApplicationClass); + addIntercept(creator, intercept, interceptor.getPostConstructs(), InterceptionType.POST_CONSTRUCT, providerType, + reflectionRegistration, isApplicationClass); + addIntercept(creator, intercept, interceptor.getPreDestroys(), InterceptionType.PRE_DESTROY, providerType, + reflectionRegistration, isApplicationClass); + addIntercept(creator, intercept, interceptor.getAroundConstructs(), InterceptionType.AROUND_CONSTRUCT, providerType, + reflectionRegistration, isApplicationClass); + } intercept.returnValue(intercept.loadNull()); } 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 06eaf3ea49a58..af1f59b50230a 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 @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import jakarta.enterprise.inject.spi.DefinitionException; @@ -15,6 +16,7 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; import org.jboss.jandex.MethodInfo; @@ -22,7 +24,12 @@ import org.jboss.jandex.Type.Kind; import org.jboss.logging.Logger; +import io.quarkus.arc.InterceptorCreator; +import io.quarkus.arc.InterceptorCreator.InterceptFunction; +import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.arc.impl.Sets; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; /** * @@ -37,6 +44,35 @@ public class InterceptorInfo extends BeanInfo implements Comparable aroundConstructs; private final List postConstructs; private final List preDestroys; + // These fields are only used for synthetic interceptors + private final InterceptionType interceptionType; + private final Class creatorClass; + + InterceptorInfo(Class creatorClass, BeanDeployment beanDeployment, + Set bindings, List injections, int priority, InterceptionType interceptionType, + Map params) { + super(null, ClassType.create(InterceptFunction.class), null, beanDeployment, BuiltinScope.DEPENDENT.getInfo(), + Sets.singletonHashSet(Type.create(DotName.OBJECT_NAME, Kind.CLASS)), new HashSet<>(), injections, null, + null, false, + Collections.emptyList(), null, false, mc -> { + ResultHandle creatorHandle = mc.newInstance(MethodDescriptor.ofConstructor(creatorClass)); + ResultHandle ret = mc.invokeInterfaceMethod( + MethodDescriptor.ofMethod(InterceptorCreator.class, "create", InterceptFunction.class, + SyntheticCreationalContext.class), + creatorHandle, mc.getMethodParam(0)); + mc.ifNull(ret).trueBranch().throwException(IllegalStateException.class, + creatorClass.getName() + "#create() must not return null"); + mc.returnValue(ret); + }, + null, params, true, false, null, priority, creatorClass.getName()); + this.bindings = bindings; + this.interceptionType = interceptionType; + this.creatorClass = creatorClass; + this.aroundInvokes = List.of(); + this.aroundConstructs = List.of(); + this.postConstructs = List.of(); + this.preDestroys = List.of(); + } InterceptorInfo(AnnotationTarget target, BeanDeployment beanDeployment, Set bindings, List injections, int priority) { @@ -44,6 +80,8 @@ public class InterceptorInfo extends BeanInfo implements Comparable(), injections, null, null, false, Collections.emptyList(), null, false, null, priority); this.bindings = bindings; + this.interceptionType = null; + this.creatorClass = null; AnnotationStore store = beanDeployment.getAnnotationStore(); List aroundInvokes = new ArrayList<>(); List aroundConstructs = new ArrayList<>(); @@ -130,6 +168,14 @@ public Set getBindings() { return bindings; } + InterceptionType getInterceptionType() { + return interceptionType; + } + + Class getCreatorClass() { + return creatorClass; + } + /** * Returns all methods annotated with {@link jakarta.interceptor.AroundInvoke} found in the hierarchy of the interceptor * class. @@ -218,6 +264,9 @@ public MethodInfo getPreDestroy() { } public boolean intercepts(InterceptionType interceptionType) { + if (isSynthetic()) { + return interceptionType == this.interceptionType; + } switch (interceptionType) { case AROUND_INVOKE: return !aroundInvokes.isEmpty(); 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 b6f3c26ea216b..57b9f53d10775 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 @@ -22,6 +22,7 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.ArcInvocationContext; import io.quarkus.arc.ClientProxy; import io.quarkus.arc.ComponentsProvider; import io.quarkus.arc.InjectableBean; @@ -29,6 +30,7 @@ import io.quarkus.arc.InjectableContext; import io.quarkus.arc.InjectableInterceptor; import io.quarkus.arc.InjectableReferenceProvider; +import io.quarkus.arc.InterceptorCreator.InterceptFunction; import io.quarkus.arc.impl.ClientProxies; import io.quarkus.arc.impl.CreationalContextImpl; import io.quarkus.arc.impl.DecoratorDelegateProvider; @@ -302,6 +304,9 @@ public final class MethodDescriptors { InjectionPointImpl.class, Type.class, Type.class, Set.class, InjectableBean.class, Set.class, Member.class, int.class, boolean.class); + public static final MethodDescriptor INTERCEPT_FUNCTION_INTERCEPT = MethodDescriptor.ofMethod(InterceptFunction.class, + "intercept", Object.class, ArcInvocationContext.class); + private MethodDescriptors() { } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InterceptorCreator.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InterceptorCreator.java new file mode 100644 index 0000000000000..8c0bf8f9d897a --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InterceptorCreator.java @@ -0,0 +1,29 @@ +package io.quarkus.arc; + +/** + * This interface is used by synthetic interceptor to initialize an interceptor instance. + */ +public interface InterceptorCreator { + + /** + * + * @param context + * @return the intercept function + */ + InterceptFunction create(SyntheticCreationalContext context); + + @FunctionalInterface + public interface InterceptFunction { + + /** + * The returned value is ignored by the container when the method is invoked to interpose on a lifecycle event. + * + * @param invocationContext + * @return the return value + * @throws Exception + */ + Object intercept(ArcInvocationContext invocationContext) throws Exception; + + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticInterceptorTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticInterceptorTest.java new file mode 100644 index 0000000000000..43bd80272116a --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticInterceptorTest.java @@ -0,0 +1,173 @@ +package io.quarkus.arc.test.buildextension.beans; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import jakarta.enterprise.inject.Intercepted; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.InterceptionType; +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.enterprise.util.TypeLiteral; +import jakarta.inject.Singleton; +import jakarta.interceptor.InterceptorBinding; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.WildcardType; +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.InterceptorCreator; +import io.quarkus.arc.SyntheticCreationalContext; +import io.quarkus.arc.processor.BeanRegistrar; +import io.quarkus.arc.test.ArcTestContainer; + +public class SyntheticInterceptorTest { + + static final List EVENTS = new CopyOnWriteArrayList<>(); + + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(MyBean.class, SimpleBinding.class, TestAroundInvoke.class) + .beanRegistrars(new TestRegistrar()).build(); + + @Test + public void testSyntheticInterceptor() { + EVENTS.clear(); + InstanceHandle handle = Arc.container().instance(MyBean.class); + assertEquals("ok", handle.get().ping()); + handle.destroy(); + assertEquals(4, EVENTS.size()); + assertEquals(TestAroundConstruct.class.getName(), EVENTS.get(0)); + assertEquals(TestPostConstruct.class.getName(), EVENTS.get(1)); + assertEquals(TestAroundInvoke.class.getName(), EVENTS.get(2)); + assertEquals(TestPreDestroy.class.getName(), EVENTS.get(3)); + } + + @SimpleBinding + @Singleton + static class MyBean { + + String ping() { + return "true"; + } + + } + + @Target({ TYPE, METHOD }) + @Retention(RUNTIME) + @InterceptorBinding + public @interface SimpleBinding { + + } + + static class TestAroundInvoke implements InterceptorCreator { + + @Override + public InterceptFunction create(SyntheticCreationalContext context) { + assertInterceptedBean(context); + return ic -> { + EVENTS.add(TestAroundInvoke.class.getName()); + return Boolean.parseBoolean(ic.proceed().toString()) ? "ok" : "nok"; + }; + } + + } + + static class TestPostConstruct implements InterceptorCreator { + + @Override + public InterceptFunction create(SyntheticCreationalContext context) { + assertInterceptedBean(context); + return ic -> { + EVENTS.add(TestPostConstruct.class.getName()); + return ic.proceed(); + }; + } + + } + + static class TestPreDestroy implements InterceptorCreator { + + @Override + public InterceptFunction create(SyntheticCreationalContext context) { + assertInterceptedBean(context); + return ic -> { + EVENTS.add(TestPreDestroy.class.getName()); + return ic.proceed(); + }; + } + + } + + static class TestAroundConstruct implements InterceptorCreator { + + @Override + public InterceptFunction create(SyntheticCreationalContext context) { + assertInterceptedBean(context); + return ic -> { + EVENTS.add(TestAroundConstruct.class.getName()); + return ic.proceed(); + }; + } + + } + + static void assertInterceptedBean(SyntheticCreationalContext context) { + @SuppressWarnings("serial") + Bean bean = context.getInjectedReference(new TypeLiteral>() { + }, InterceptedLiteral.INSTANCE); + assertNotNull(bean); + assertEquals(Singleton.class, bean.getScope()); + } + + static final class InterceptedLiteral extends AnnotationLiteral implements Intercepted { + + public static final InterceptedLiteral INSTANCE = new InterceptedLiteral(); + + private static final long serialVersionUID = 1L; + + } + + static class TestRegistrar implements BeanRegistrar { + + @Override + public void register(RegistrationContext context) { + context.configureInterceptor(InterceptionType.AROUND_INVOKE) + .bindings(AnnotationInstance.builder(SimpleBinding.class).build()) + .addInjectionPoint(ParameterizedType.create(Bean.class, WildcardType.UNBOUNDED), + AnnotationInstance.builder(Intercepted.class).build()) + .creator(TestAroundInvoke.class); + + context.configureInterceptor(InterceptionType.POST_CONSTRUCT) + .bindings(AnnotationInstance.builder(SimpleBinding.class).build()) + .addInjectionPoint(ParameterizedType.create(Bean.class, WildcardType.UNBOUNDED), + AnnotationInstance.builder(Intercepted.class).build()) + .creator(TestPostConstruct.class); + + context.configureInterceptor(InterceptionType.PRE_DESTROY) + .bindings(AnnotationInstance.builder(SimpleBinding.class).build()) + .addInjectionPoint(ParameterizedType.create(Bean.class, WildcardType.UNBOUNDED), + AnnotationInstance.builder(Intercepted.class).build()) + .creator(TestPreDestroy.class); + + context.configureInterceptor(InterceptionType.AROUND_CONSTRUCT) + .bindings(AnnotationInstance.builder(SimpleBinding.class).build()) + .addInjectionPoint(ParameterizedType.create(Bean.class, WildcardType.UNBOUNDED), + AnnotationInstance.builder(Intercepted.class).build()) + .creator(TestAroundConstruct.class); + } + + } + +}