From 980947bc4a74668464a034bcedf1df7628d0d7c4 Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Wed, 17 Jul 2024 13:51:34 +0200 Subject: [PATCH] ArC: initial support for inactive synthetic beans --- .../arc/deployment/StartupBuildSteps.java | 28 +++++- .../deployment/SyntheticBeanBuildItem.java | 32 ++++++- .../deployment/SyntheticBeansProcessor.java | 54 +++++++++--- .../io/quarkus/arc/runtime/ArcRecorder.java | 15 +++- .../arc/processor/BeanConfigurator.java | 3 +- .../arc/processor/BeanConfiguratorBase.java | 18 ++++ .../quarkus/arc/processor/BeanGenerator.java | 51 +++++++++++ .../io/quarkus/arc/processor/BeanInfo.java | 25 +++++- .../arc/processor/InterceptorInfo.java | 2 +- .../arc/processor/MethodDescriptors.java | 11 +++ .../java/io/quarkus/arc/ActiveResult.java | 86 +++++++++++++++++++ .../io/quarkus/arc/InactiveBeanException.java | 12 +++ .../java/io/quarkus/arc/InjectableBean.java | 11 +++ .../beans/SyntheticBeanWithIsActiveTest.java | 82 ++++++++++++++++++ 14 files changed, 406 insertions(+), 24 deletions(-) create mode 100644 independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ActiveResult.java create mode 100644 independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InactiveBeanException.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticBeanWithIsActiveTest.java diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java index ceaf62879422cf..24c5e33c71e292 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java @@ -23,6 +23,7 @@ import org.jboss.jandex.MethodInfo; import org.jboss.logging.Logger; +import io.quarkus.arc.ActiveResult; import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.ClientProxy; @@ -37,6 +38,7 @@ import io.quarkus.arc.processor.BuildExtension; import io.quarkus.arc.processor.BuiltinScope; import io.quarkus.arc.processor.DotNames; +import io.quarkus.arc.processor.InjectionPointInfo; import io.quarkus.arc.processor.ObserverConfigurator; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -179,6 +181,30 @@ private void registerStartupObserver(ObserverRegistrationPhaseBuildItem observer ResultHandle containerHandle = mc.invokeStaticMethod(ARC_CONTAINER); ResultHandle beanHandle = mc.invokeInterfaceMethod(ARC_CONTAINER_BEAN, containerHandle, mc.load(bean.getIdentifier())); + + // if the [synthetic] bean is not active and is not injected in a user bean, skip obtaining the instance + // this means that an inactive bean that is injected into a user bean will end up with an error + if (bean.canBeInactive()) { + boolean isInjectedInUserBean = false; + for (InjectionPointInfo ip : observerRegistration.getBeanProcessor().getBeanDeployment().getInjectionPoints()) { + if (bean.equals(ip.getResolvedBean()) && ip.getTargetBean().isPresent() + && !ip.getTargetBean().get().isSynthetic()) { + isInjectedInUserBean = true; + break; + } + } + + if (!isInjectedInUserBean) { + ResultHandle activeResult = mc.invokeInterfaceMethod( + MethodDescriptor.ofMethod(InjectableBean.class, "isActive", ActiveResult.class), + beanHandle); + ResultHandle isActive = mc.invokeVirtualMethod( + MethodDescriptor.ofMethod(ActiveResult.class, "result", boolean.class), + activeResult); + mc.ifFalse(isActive).trueBranch().returnVoid(); + } + } + if (BuiltinScope.DEPENDENT.is(bean.getScope())) { // It does not make a lot of sense to support @Startup dependent beans but it's still a valid use case ResultHandle creationalContext = mc.newInstance( @@ -212,7 +238,7 @@ private void registerStartupObserver(ObserverRegistrationPhaseBuildItem observer mc.invokeInterfaceMethod(CLIENT_PROXY_CONTEXTUAL_INSTANCE, proxyHandle); } } - mc.returnValue(null); + mc.returnVoid(); }); configurator.done(); } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeanBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeanBuildItem.java index 3a8dd22f6fb184..bb34ca634c8db4 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeanBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeanBuildItem.java @@ -2,6 +2,7 @@ import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -9,6 +10,7 @@ import org.jboss.jandex.DotName; import org.jboss.jandex.Type; +import io.quarkus.arc.ActiveResult; import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.arc.processor.BeanConfiguratorBase; import io.quarkus.arc.processor.BeanRegistrar; @@ -68,6 +70,10 @@ boolean hasRecorderInstance() { || configurator.runtimeProxy != null; } + boolean hasIsActiveSupplier() { + return configurator.isActive != null; + } + /** * This construct is not thread-safe and should not be reused. */ @@ -79,6 +85,8 @@ public static class ExtendedBeanConfigurator extends BeanConfiguratorBase, ?> fun; private boolean staticInit; + private Supplier isActive; + ExtendedBeanConfigurator(DotName implClazz) { super(implClazz); this.staticInit = true; @@ -92,7 +100,11 @@ public static class ExtendedBeanConfigurator extends BeanConfiguratorBase} proxy + * returned from a recorder method. + * + * @param isActive a {@code Supplier} returned from a recorder method + * @return self + * @throws IllegalArgumentException if the {@code isActive} argument is not a proxy returned from a recorder method + */ + public ExtendedBeanConfigurator isActive(Supplier isActive) { + checkReturnedProxy(isActive); + this.isActive = Objects.requireNonNull(isActive); + return this; + } + DotName getImplClazz() { return implClazz; } @@ -213,6 +239,10 @@ Object getRuntimeProxy() { return runtimeProxy; } + Supplier getIsActive() { + return isActive; + } + private void checkMultipleCreationMethods() { if (runtimeProxy == null && runtimeValue == null && supplier == null && fun == null) { return; diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeansProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeansProcessor.java index 0c105be1d1af35..989978b2af1c5d 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeansProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeansProcessor.java @@ -5,9 +5,11 @@ import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Supplier; import jakarta.enterprise.inject.CreationException; +import io.quarkus.arc.ActiveResult; import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem.BeanConfiguratorBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator; @@ -32,15 +34,16 @@ public class SyntheticBeansProcessor { void initStatic(ArcRecorder recorder, List syntheticBeans, BeanRegistrationPhaseBuildItem beanRegistration, BuildProducer configurators) { - Map, ?>> functionsMap = new HashMap<>(); + Map, ?>> creationFunctions = new HashMap<>(); + Map> isActiveSuppliers = new HashMap<>(); for (SyntheticBeanBuildItem bean : syntheticBeans) { if (bean.hasRecorderInstance() && bean.isStaticInit()) { - configureSyntheticBean(recorder, functionsMap, beanRegistration, bean); + configureSyntheticBean(recorder, creationFunctions, isActiveSuppliers, beanRegistration, bean); } } // Init the map of bean instances - recorder.initStaticSupplierBeans(functionsMap); + recorder.initStaticSupplierBeans(creationFunctions, isActiveSuppliers); } @Record(ExecutionTime.RUNTIME_INIT) @@ -49,14 +52,15 @@ void initStatic(ArcRecorder recorder, List syntheticBean ServiceStartBuildItem initRuntime(ArcRecorder recorder, List syntheticBeans, BeanRegistrationPhaseBuildItem beanRegistration, BuildProducer configurators) { - Map, ?>> functionsMap = new HashMap<>(); + Map, ?>> creationFunctions = new HashMap<>(); + Map> isActiveSuppliers = new HashMap<>(); for (SyntheticBeanBuildItem bean : syntheticBeans) { if (bean.hasRecorderInstance() && !bean.isStaticInit()) { - configureSyntheticBean(recorder, functionsMap, beanRegistration, bean); + configureSyntheticBean(recorder, creationFunctions, isActiveSuppliers, beanRegistration, bean); } } - recorder.initRuntimeSupplierBeans(functionsMap); + recorder.initRuntimeSupplierBeans(creationFunctions, isActiveSuppliers); return new ServiceStartBuildItem("runtime-bean-init"); } @@ -66,29 +70,34 @@ void initRegular(List syntheticBeans, for (SyntheticBeanBuildItem bean : syntheticBeans) { if (!bean.hasRecorderInstance()) { - configureSyntheticBean(null, null, beanRegistration, bean); + configureSyntheticBean(null, null, null, beanRegistration, bean); } } } private void configureSyntheticBean(ArcRecorder recorder, - Map, ?>> functionsMap, - BeanRegistrationPhaseBuildItem beanRegistration, SyntheticBeanBuildItem bean) { + Map, ?>> creationFunctions, + Map> isActiveSuppliers, BeanRegistrationPhaseBuildItem beanRegistration, + SyntheticBeanBuildItem bean) { String name = createName(bean.configurator()); if (bean.configurator().getRuntimeValue() != null) { - functionsMap.put(name, recorder.createFunction(bean.configurator().getRuntimeValue())); + creationFunctions.put(name, recorder.createFunction(bean.configurator().getRuntimeValue())); } else if (bean.configurator().getSupplier() != null) { - functionsMap.put(name, recorder.createFunction(bean.configurator().getSupplier())); + creationFunctions.put(name, recorder.createFunction(bean.configurator().getSupplier())); } else if (bean.configurator().getFunction() != null) { - functionsMap.put(name, bean.configurator().getFunction()); + creationFunctions.put(name, bean.configurator().getFunction()); } else if (bean.configurator().getRuntimeProxy() != null) { - functionsMap.put(name, recorder.createFunction(bean.configurator().getRuntimeProxy())); + creationFunctions.put(name, recorder.createFunction(bean.configurator().getRuntimeProxy())); } BeanConfigurator configurator = beanRegistration.getContext().configure(bean.configurator().getImplClazz()) .read(bean.configurator()); if (bean.hasRecorderInstance()) { configurator.creator(creator(name, bean)); } + if (bean.hasIsActiveSupplier()) { + configurator.isActive(isActive(name, bean)); + isActiveSuppliers.put(name, bean.configurator().getIsActive()); + } configurator.done(); } @@ -118,6 +127,25 @@ public void accept(MethodCreator m) { }; } + private Consumer isActive(String name, SyntheticBeanBuildItem bean) { + return new Consumer() { + @Override + public void accept(MethodCreator mc) { + ResultHandle staticMap = mc.readStaticField( + FieldDescriptor.of(ArcRecorder.class, "syntheticBeanIsActive", Map.class)); + ResultHandle supplier = mc.invokeInterfaceMethod( + MethodDescriptor.ofMethod(Map.class, "get", Object.class, Object.class), + staticMap, mc.load(name)); + // TODO different message, possibly even different exception + mc.ifNull(supplier).trueBranch().throwException(CreationException.class, + createMessage(name, bean)); + mc.returnValue(mc.invokeInterfaceMethod( + MethodDescriptor.ofMethod(Supplier.class, "get", Object.class), + supplier)); + } + }; + } + private String createMessage(String name, SyntheticBeanBuildItem bean) { StringBuilder builder = new StringBuilder(); builder.append("Synthetic bean instance for "); diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java index ff8ccc90061d63..45d212c5718fd0 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java @@ -13,6 +13,7 @@ import org.jboss.logging.Logger; +import io.quarkus.arc.ActiveResult; import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.ArcInitConfig; @@ -41,6 +42,8 @@ public class ArcRecorder { */ public static volatile Map, ?>> syntheticBeanProviders; + public static volatile Map> syntheticBeanIsActive; + public ArcContainer initContainer(ShutdownContext shutdown, RuntimeValue currentContextFactory, boolean strictCompatibility) throws Exception { ArcInitConfig.Builder builder = ArcInitConfig.builder(); @@ -61,12 +64,16 @@ public void initExecutor(ExecutorService executor) { Arc.setExecutor(executor); } - public void initStaticSupplierBeans(Map, ?>> beans) { - syntheticBeanProviders = new ConcurrentHashMap<>(beans); + public void initStaticSupplierBeans(Map, ?>> creationFunctions, + Map> isActiveSuppliers) { + syntheticBeanProviders = new ConcurrentHashMap<>(creationFunctions); + syntheticBeanIsActive = new ConcurrentHashMap<>(isActiveSuppliers); } - public void initRuntimeSupplierBeans(Map, ?>> beans) { - syntheticBeanProviders.putAll(beans); + public void initRuntimeSupplierBeans(Map, ?>> creationFunctions, + Map> isActiveSuppliers) { + syntheticBeanProviders.putAll(creationFunctions); + syntheticBeanIsActive.putAll(isActiveSuppliers); } public BeanContainer initBeanContainer(ArcContainer container, List listeners) diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfigurator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfigurator.java index b033a0cdd18372..445ef73c169d88 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfigurator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfigurator.java @@ -105,7 +105,8 @@ public void done() { .forceApplicationClass(forceApplicationClass) .targetPackageName(targetPackageName) .startupPriority(startupPriority) - .interceptionProxy(interceptionProxy); + .interceptionProxy(interceptionProxy) + .isActive(isActiveConsumer); if (!injectionPoints.isEmpty()) { builder.injections(Collections.singletonList(Injection.forSyntheticBean(injectionPoints))); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfiguratorBase.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfiguratorBase.java index 102df4d62e6f5d..b85b2d45fd3729 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfiguratorBase.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfiguratorBase.java @@ -9,6 +9,7 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Supplier; import jakarta.enterprise.context.NormalScope; import jakarta.enterprise.context.spi.CreationalContext; @@ -22,6 +23,7 @@ import org.jboss.jandex.Type; import org.jboss.jandex.Type.Kind; +import io.quarkus.arc.ActiveResult; import io.quarkus.arc.BeanCreator; import io.quarkus.arc.BeanDestroyer; import io.quarkus.arc.InjectableBean; @@ -59,6 +61,7 @@ public abstract class BeanConfiguratorBase injectionPoints; protected Integer startupPriority; protected InterceptionProxyInfo interceptionProxy; + protected Consumer isActiveConsumer; protected BeanConfiguratorBase(DotName implClazz) { this.implClazz = implClazz; @@ -101,6 +104,7 @@ public THIS read(BeanConfiguratorBase base) { injectionPoints.addAll(base.injectionPoints); startupPriority = base.startupPriority; interceptionProxy = base.interceptionProxy; + isActiveConsumer = base.isActiveConsumer; return self(); } @@ -391,6 +395,20 @@ public THIS destroyer(Consumer methodCreatorConsumer) { return cast(this); } + public THIS isActive(Class> isActiveClazz) { + return isActive(mc -> { + // return new FooActiveResultSupplier().get() + ResultHandle supplierHandle = mc.newInstance(MethodDescriptor.ofConstructor(isActiveClazz)); + mc.returnValue(mc.invokeInterfaceMethod(MethodDescriptor.ofMethod(Supplier.class, "get", Object.class), + supplierHandle)); + }); + } + + public THIS isActive(Consumer methodCreatorConsumer) { + this.isActiveConsumer = methodCreatorConsumer; + return cast(this); + } + /** * The identifier becomes part of the {@link BeanInfo#getIdentifier()} and {@link InjectableBean#getIdentifier()}. *

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 da197f726362b1..758d57bd2e509c 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 @@ -46,6 +46,8 @@ import org.jboss.jandex.Type; import org.jboss.logging.Logger; +import io.quarkus.arc.ActiveResult; +import io.quarkus.arc.InactiveBeanException; import io.quarkus.arc.InjectableBean; import io.quarkus.arc.InjectableDecorator; import io.quarkus.arc.InjectableInterceptor; @@ -1089,6 +1091,55 @@ private void implementCreateForSyntheticBean(ClassCreator beanCreator, BeanInfo .setModifiers(ACC_PRIVATE); bean.getCreatorConsumer().accept(createSynthetic); + Consumer isActiveConsumer = bean.getIsActiveConsumer(); + if (isActiveConsumer != null) { + MethodCreator isActive = beanCreator.getMethodCreator("isActive", ActiveResult.class); + isActiveConsumer.accept(isActive); + + List matchingIPs = new ArrayList<>(); + for (InjectionPointInfo injectionPoint : bean.getDeployment().getInjectionPoints()) { + if (bean.equals(injectionPoint.getResolvedBean())) { + matchingIPs.add(injectionPoint); + } + } + + ResultHandle activeResult = doCreate.invokeVirtualMethod(isActive.getMethodDescriptor(), doCreate.getThis()); + ResultHandle activeResultBool = doCreate.invokeVirtualMethod(MethodDescriptors.ACTIVE_RESULT_RESULT, activeResult); + BytecodeCreator notActive = doCreate.ifFalse(activeResultBool).trueBranch(); + StringBuilderGenerator msg = Gizmo.newStringBuilder(notActive); + msg.append("Bean is not active: "); + msg.append(Gizmo.toString(notActive, notActive.getThis())); + msg.append("\nReason: "); + msg.append(notActive.invokeVirtualMethod(MethodDescriptors.ACTIVE_RESULT_REASON, activeResult)); + AssignableResultHandle cause = notActive.createVariable(ActiveResult.class); + notActive.assign(cause, notActive.invokeVirtualMethod(MethodDescriptors.ACTIVE_RESULT_CAUSE, activeResult)); + BytecodeCreator loop = notActive.whileLoop(bc -> bc.ifNotNull(cause)).block(); + loop.invokeVirtualMethod(MethodDescriptors.STRING_BUILDER_APPEND, msg.getInstance(), loop.load("\nCause: ")); + ResultHandle causeReason = loop.invokeVirtualMethod(MethodDescriptors.ACTIVE_RESULT_REASON, cause); + loop.invokeVirtualMethod(MethodDescriptors.STRING_BUILDER_APPEND, msg.getInstance(), causeReason); + loop.assign(cause, loop.invokeVirtualMethod(MethodDescriptors.ACTIVE_RESULT_CAUSE, cause)); + msg.append("\nTo avoid this exception while keeping the bean inactive:"); + msg.append("\n\t- Configure all extensions consuming this bean as inactive as well, if they allow it," + + " e.g. 'quarkus.someextension.active=false'"); + msg.append("\n\t- Make sure that custom code only accesses this bean if it is active"); + if (!matchingIPs.isEmpty()) { + ResultHandle implClassName = notActive.load(bean.getImplClazz().name().toString()); + msg.append("\n\t- Inject the bean with 'Instance<") + .append(implClassName) + .append(">' instead of '") + .append(implClassName) + .append("'"); + msg.append("\nThis bean is injected into:"); + for (InjectionPointInfo matchingIP : matchingIPs) { + msg.append("\n\t-"); + msg.append(matchingIP.getTargetInfo()); + } + } + notActive.throwException(notActive.newInstance( + MethodDescriptor.ofConstructor(InactiveBeanException.class, String.class), + msg.callToString())); + } + ResultHandle injectedReferences; if (injectionPointToProviderSupplierField.isEmpty()) { injectedReferences = doCreate.invokeStaticMethod(MethodDescriptors.COLLECTIONS_EMPTY_MAP); 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 8b3d1ceca018de..e08cf39ed509f2 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 @@ -103,13 +103,15 @@ public class BeanInfo implements InjectionTargetInfo { private final Integer startupPriority; + private final Consumer isActiveConsumer; + 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, Integer priority, Set unrestrictedTypes, InterceptionProxyInfo interceptionProxy) { this(null, null, target, beanDeployment, scope, types, qualifiers, injections, declaringBean, disposer, alternative, stereotypes, name, isDefaultBean, null, null, Collections.emptyMap(), true, false, - targetPackageName, priority, null, unrestrictedTypes, null, interceptionProxy); + targetPackageName, priority, null, unrestrictedTypes, null, interceptionProxy, null); } BeanInfo(ClassInfo implClazz, Type providerType, AnnotationTarget target, BeanDeployment beanDeployment, ScopeInfo scope, @@ -117,7 +119,8 @@ public class BeanInfo implements InjectionTargetInfo { DisposerInfo disposer, boolean alternative, List stereotypes, String name, boolean isDefaultBean, Consumer creatorConsumer, Consumer destroyerConsumer, Map params, boolean isRemovable, boolean forceApplicationClass, String targetPackageName, Integer priority, String identifier, - Set unrestrictedTypes, Integer startupPriority, InterceptionProxyInfo interceptionProxy) { + Set unrestrictedTypes, Integer startupPriority, InterceptionProxyInfo interceptionProxy, + Consumer isActiveConsumer) { this.target = Optional.ofNullable(target); if (implClazz == null && target != null) { @@ -151,6 +154,7 @@ public class BeanInfo implements InjectionTargetInfo { this.removable = isRemovable; this.params = params; this.interceptionProxy = interceptionProxy; + this.isActiveConsumer = isActiveConsumer; // Identifier must be unique for a specific deployment this.identifier = Hashes.sha1_base64((identifier != null ? identifier : "") + toString() + beanDeployment.toString()); this.interceptedMethods = Collections.emptyMap(); @@ -590,6 +594,14 @@ Consumer getDestroyerConsumer() { return destroyerConsumer; } + Consumer getIsActiveConsumer() { + return isActiveConsumer; + } + + public boolean canBeInactive() { + return isActiveConsumer != null; + } + Map getParams() { return params; } @@ -1130,6 +1142,8 @@ static class Builder { private InterceptionProxyInfo interceptionProxy; + private Consumer isActiveConsumer; + Builder() { injections = Collections.emptyList(); stereotypes = Collections.emptyList(); @@ -1259,11 +1273,16 @@ Builder interceptionProxy(InterceptionProxyInfo interceptionProxy) { return this; } + Builder isActive(Consumer isActiveConsumer) { + this.isActiveConsumer = isActiveConsumer; + return this; + } + BeanInfo build() { return new BeanInfo(implClazz, providerType, target, beanDeployment, scope, types, qualifiers, injections, declaringBean, disposer, alternative, stereotypes, name, isDefaultBean, creatorConsumer, destroyerConsumer, params, removable, forceApplicationClass, targetPackageName, priority, - identifier, null, startupPriority, interceptionProxy); + identifier, null, startupPriority, interceptionProxy, isActiveConsumer); } public Builder forceApplicationClass(boolean forceApplicationClass) { 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 2264fde9a8bd5a..a3878f4ee298f6 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 @@ -66,7 +66,7 @@ public class InterceptorInfo extends BeanInfo implements Comparable getImplementationClass() { return getBeanClass(); } + /** + * Returns whether this bean is active and if not, the reason why. Certain + * synthetic beans may be inactive from time to time. Attempting to inject + * or lookup such an inactive bean leads to {@link InactiveBeanException}. + * + * @return whether this bean is active and if not, the reason why + */ + default ActiveResult isActive() { + return ActiveResult.active(); + } + enum Kind { CLASS, diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticBeanWithIsActiveTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticBeanWithIsActiveTest.java new file mode 100644 index 00000000000000..be6c7147948892 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticBeanWithIsActiveTest.java @@ -0,0 +1,82 @@ +package io.quarkus.arc.test.buildextension.beans; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.function.Supplier; + +import org.jboss.jandex.ClassType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.ActiveResult; +import io.quarkus.arc.Arc; +import io.quarkus.arc.BeanCreator; +import io.quarkus.arc.InactiveBeanException; +import io.quarkus.arc.InjectableInstance; +import io.quarkus.arc.SyntheticCreationalContext; +import io.quarkus.arc.processor.BeanRegistrar; +import io.quarkus.arc.test.ArcTestContainer; + +public class SyntheticBeanWithIsActiveTest { + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanRegistrars(new BeanRegistrar() { + @Override + public void register(RegistrationContext context) { + context.configure(MyBean.class) + .addType(ClassType.create(MyBean.class)) + .creator(MyBeanCreator.class) + .isActive(MyBeanIsActive.class) + .unremovable() + .done(); + } + }) + .build(); + + @Test + public void test() { + InjectableInstance myBean = Arc.container().select(MyBean.class); + + MyBeanIsActive.active = true; + + assertTrue(myBean.getHandle().getBean().isActive().result()); + assertNull(myBean.getHandle().getBean().isActive().inactiveReason()); + assertNotNull(myBean.get()); + + MyBeanIsActive.active = false; + + assertFalse(myBean.getHandle().getBean().isActive().result()); + assertNotNull(myBean.getHandle().getBean().isActive().inactiveReason()); + InactiveBeanException e = assertThrows(InactiveBeanException.class, myBean::get); + assertTrue(e.getMessage().contains("Bean is not active")); + assertTrue(e.getMessage().contains("MyBean not active")); + assertTrue(e.getMessage().contains("Deeper reason")); + } + + public static class MyBean { + } + + public static class MyBeanCreator implements BeanCreator { + @Override + public MyBean create(SyntheticCreationalContext context) { + return new MyBean(); + } + } + + public static class MyBeanIsActive implements Supplier { + public static boolean active; + + @Override + public ActiveResult get() { + if (active) { + return ActiveResult.active(); + } else { + return ActiveResult.inactive("MyBean not active", ActiveResult.inactive("Deeper reason")); + } + } + } +}