From f5b2154c5c5b150d84c7f74075d9be0beb308dfd 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 --- docs/src/main/asciidoc/cdi-integration.adoc | 67 ++++++++++++++ .../arc/deployment/StartupBuildSteps.java | 24 ++++- .../deployment/SyntheticBeanBuildItem.java | 34 +++++++- .../deployment/SyntheticBeansProcessor.java | 59 +++++++++---- .../io/quarkus/arc/runtime/ArcRecorder.java | 15 +++- .../arc/processor/BeanConfigurator.java | 3 +- .../arc/processor/BeanConfiguratorBase.java | 43 ++++++++- .../quarkus/arc/processor/BeanGenerator.java | 51 +++++++++++ .../io/quarkus/arc/processor/BeanInfo.java | 34 +++++++- .../arc/processor/InterceptorInfo.java | 2 +- .../arc/processor/MethodDescriptors.java | 11 +++ .../java/io/quarkus/arc/ActiveResult.java | 87 +++++++++++++++++++ .../io/quarkus/arc/InactiveBeanException.java | 12 +++ .../java/io/quarkus/arc/InjectableBean.java | 20 +++++ .../SyntheticBeanWithCheckActiveTest.java | 82 +++++++++++++++++ 15 files changed, 516 insertions(+), 28 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/SyntheticBeanWithCheckActiveTest.java diff --git a/docs/src/main/asciidoc/cdi-integration.adoc b/docs/src/main/asciidoc/cdi-integration.adoc index 39920b6516e60..fe92dd294ba6a 100644 --- a/docs/src/main/asciidoc/cdi-integration.adoc +++ b/docs/src/main/asciidoc/cdi-integration.adoc @@ -419,6 +419,73 @@ public class TestRecorder { ---- <1> Pass a contextual reference of `Bar` to the constructor of `Foo`. +=== Inactive Synthetic Beans + +In the case when one needs to register multiple synthetic beans at build time but only wants a subset of them active at runtime, it is useful to be able to mark a synthetic bean as _inactive_. +This is done by configuring a "check active" procedure, which should be a `Supplier` obtained from a recorder: + +.Inactive Synthetic Bean - Build Step Example +[source,java] +---- +@BuildStep +@Record(RUNTIME_INIT) +SyntheticBeanBuildItem syntheticBean(TestRecorder recorder) { + return SyntheticBeanBuildItem.configure(Foo.class) + .scope(Singleton.class) + .startup() // <1> + .checkActive(recorder.isFooActive()) // <2> + .createWith(recorder.createFoo()) + .done(); +} +---- +<1> A bean that might be inactive is typically initialized eagerly, to make sure that an error is thrown at application startup. + If the bean is in fact inactive, but is not injected into an always-active bean, eager initialization is skipped and no error is thrown. +<2> Configures the "check active" procedure. + +.Inactive Synthetic Bean - Recorder Example +[source,java] +---- +@Recorder +public class TestRecorder { + public Supplier isFooActive() { + return () -> { + if (... should not be active ...) { // <1> + return ActiveResult.inactive("explanation"); // <2> + } + return ActiveResult.active(); + }; + } + + public Function, Foo> createFoo() { + return (context) -> { + return new Foo(); + }; + } +} +---- +<1> The condition when the synthetic bean should be inactive. +<2> Proper explanation of why the bean is inactive. + Another inactive `ActiveResult` may also be provided as a cause, if this bean's inactivity stems from another bean's inactivity. + +If an inactive bean is injected somewhere, or is dynamically looked up, an `InactiveBeanException` is thrown. +The error message contains the reason (from the `ActiveResult`), the cause chain (also from the `ActiveResult`), and possibly also a list of all injection points that resolve to this bean. + +If you want to handle the inactive case gracefully, you should always inject possibly inactive beans using `Instance<>`. +You also need to check before obtaining the actual instance: + +[source,java] +---- +import io.quarkus.arc.InjectableInstance; + +@Inject +InjectableInstance foo; + +if (foo.getHandle().getBean().isActive()) { + Foo foo = foo.get(); + ... +} +---- + [[synthetic_observers]] == Use Case - Synthetic Observers 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 ceaf62879422c..9734dc96ebd9d 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 @@ -37,6 +37,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 +180,27 @@ 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 an always-active bean, skip obtaining the instance + // this means that an inactive bean that is injected into an always-active bean will end up with an error + if (bean.canBeInactive()) { + boolean isInjectedInAlwaysActiveBean = false; + for (InjectionPointInfo ip : observerRegistration.getBeanProcessor().getBeanDeployment().getInjectionPoints()) { + if (bean.equals(ip.getResolvedBean()) && ip.getTargetBean().isPresent() + && !ip.getTargetBean().get().canBeInactive()) { + isInjectedInAlwaysActiveBean = true; + break; + } + } + + if (!isInjectedInAlwaysActiveBean) { + ResultHandle isActive = mc.invokeInterfaceMethod( + MethodDescriptor.ofMethod(InjectableBean.class, "isActive", boolean.class), + beanHandle); + 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 +234,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 3a8dd22f6fb18..0bee6f858918c 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 hasCheckActiveSupplier() { + return configurator.checkActive != 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 checkActive; + ExtendedBeanConfigurator(DotName implClazz) { super(implClazz); this.staticInit = true; @@ -92,7 +100,13 @@ public static class ExtendedBeanConfigurator extends BeanConfiguratorBase} proxy + * returned from a recorder method. + * + * @param checkActive a {@code Supplier} returned from a recorder method + * @return self + * @throws IllegalArgumentException if the {@code checkActive} argument is not a proxy returned from a recorder method + */ + public ExtendedBeanConfigurator checkActive(Supplier checkActive) { + checkReturnedProxy(checkActive); + this.checkActive = Objects.requireNonNull(checkActive); + return this; + } + DotName getImplClazz() { return implClazz; } @@ -213,6 +241,10 @@ Object getRuntimeProxy() { return runtimeProxy; } + Supplier getCheckActive() { + return checkActive; + } + 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 0c105be1d1af3..11c0c8d7b013b 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> checkActiveSuppliers = new HashMap<>(); for (SyntheticBeanBuildItem bean : syntheticBeans) { if (bean.hasRecorderInstance() && bean.isStaticInit()) { - configureSyntheticBean(recorder, functionsMap, beanRegistration, bean); + configureSyntheticBean(recorder, creationFunctions, checkActiveSuppliers, beanRegistration, bean); } } // Init the map of bean instances - recorder.initStaticSupplierBeans(functionsMap); + recorder.initStaticSupplierBeans(creationFunctions, checkActiveSuppliers); } @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> checkActiveSuppliers = new HashMap<>(); for (SyntheticBeanBuildItem bean : syntheticBeans) { if (bean.hasRecorderInstance() && !bean.isStaticInit()) { - configureSyntheticBean(recorder, functionsMap, beanRegistration, bean); + configureSyntheticBean(recorder, creationFunctions, checkActiveSuppliers, beanRegistration, bean); } } - recorder.initRuntimeSupplierBeans(functionsMap); + recorder.initRuntimeSupplierBeans(creationFunctions, checkActiveSuppliers); 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> checkActiveSuppliers, 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.hasCheckActiveSupplier()) { + configurator.checkActive(checkActive(name, bean)); + checkActiveSuppliers.put(name, bean.configurator().getCheckActive()); + } configurator.done(); } @@ -109,7 +118,7 @@ public void accept(MethodCreator m) { m.load(name)); // Throw an exception if no supplier is found m.ifNull(function).trueBranch().throwException(CreationException.class, - createMessage(name, bean)); + createMessage("Synthetic bean instance for ", name, bean)); ResultHandle result = m.invokeInterfaceMethod( MethodDescriptor.ofMethod(Function.class, "apply", Object.class, Object.class), function, m.getMethodParam(0)); @@ -118,9 +127,27 @@ public void accept(MethodCreator m) { }; } - private String createMessage(String name, SyntheticBeanBuildItem bean) { + private Consumer checkActive(String name, SyntheticBeanBuildItem bean) { + return new Consumer() { + @Override + public void accept(MethodCreator mc) { + ResultHandle staticMap = mc.readStaticField( + FieldDescriptor.of(ArcRecorder.class, "syntheticBeanCheckActive", Map.class)); + ResultHandle supplier = mc.invokeInterfaceMethod( + MethodDescriptor.ofMethod(Map.class, "get", Object.class, Object.class), + staticMap, mc.load(name)); + mc.ifNull(supplier).trueBranch().throwException(CreationException.class, + createMessage("ActiveResult of synthetic bean for ", name, bean)); + mc.returnValue(mc.invokeInterfaceMethod( + MethodDescriptor.ofMethod(Supplier.class, "get", Object.class), + supplier)); + } + }; + } + + private String createMessage(String description, String name, SyntheticBeanBuildItem bean) { StringBuilder builder = new StringBuilder(); - builder.append("Synthetic bean instance for "); + builder.append(description); builder.append(bean.configurator().getImplClazz()); builder.append(" not initialized yet: "); builder.append(name); 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 ff8ccc90061d6..c353ac8ff05e1 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> syntheticBeanCheckActive; + 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> checkActiveSuppliers) { + syntheticBeanProviders = new ConcurrentHashMap<>(creationFunctions); + syntheticBeanCheckActive = new ConcurrentHashMap<>(checkActiveSuppliers); } - public void initRuntimeSupplierBeans(Map, ?>> beans) { - syntheticBeanProviders.putAll(beans); + public void initRuntimeSupplierBeans(Map, ?>> creationFunctions, + Map> checkActiveSuppliers) { + syntheticBeanProviders.putAll(creationFunctions); + syntheticBeanCheckActive.putAll(checkActiveSuppliers); } 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 b033a0cdd1837..156df45341604 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) + .checkActive(checkActiveConsumer); 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 102df4d62e6f5..84644292fd52a 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 checkActiveConsumer; 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; + checkActiveConsumer = base.checkActiveConsumer; return self(); } @@ -264,8 +268,11 @@ public THIS addInjectionPoint(Type requiredType, AnnotationInstance... requiredQ /** * Initialize the bean eagerly at application startup. + *

+ * If this bean is not active (see {@link #checkActive(Consumer)}) and is not injected into + * any always active bean, eager initialization is skipped to prevent needless failures. * - * @param priority + * @param priority priority of the generated synthetic observer, to affect eager init ordering * @return self */ public THIS startup(int priority) { @@ -275,6 +282,9 @@ public THIS startup(int priority) { /** * Initialize the bean eagerly at application startup. + *

+ * If this bean is not active (see {@link #checkActive(Consumer)}) and is not injected into + * any always active bean, eager initialization is skipped to prevent needless failures. * * @return self */ @@ -391,6 +401,37 @@ public THIS destroyer(Consumer methodCreatorConsumer) { return cast(this); } + /** + * Configures the class of the "check active" procedure. + * + * @see #checkActive(Consumer) + */ + public THIS checkActive(Class> checkActiveClazz) { + return checkActive(mc -> { + // return new FooActiveResultSupplier().get() + ResultHandle supplierHandle = mc.newInstance(MethodDescriptor.ofConstructor(checkActiveClazz)); + mc.returnValue(mc.invokeInterfaceMethod(MethodDescriptor.ofMethod(Supplier.class, "get", Object.class), + supplierHandle)); + }); + } + + /** + * Configures the procedure that generates the bytecode for checking whether this bean is active or not. + * Usually, this method should not be called, because most beans are always active. However, certain + * synthetic beans may be inactive from time to time -- as determined by this procedure. If a bean + * is inactive, injecting it or looking it up ends with {@code InactiveBeanException}. + *

+ * The procedure is expected to return an {@link io.quarkus.arc.ActiveResult ActiveResult}, which + * for inactive beans must include an explanation (why is this bean not active) and optionally also + * a cause (in case the bean is inactive because another bean is also inactive). + * + * @return the procedure that generates the bytecode for checking whether this bean is active or not + */ + public THIS checkActive(Consumer methodCreatorConsumer) { + this.checkActiveConsumer = 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 7b763b4f47fb3..6d839c1277323 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; @@ -1091,6 +1093,55 @@ private void implementCreateForSyntheticBean(ClassCreator beanCreator, BeanInfo .setModifiers(ACC_PRIVATE); bean.getCreatorConsumer().accept(createSynthetic); + Consumer checkActiveConsumer = bean.getCheckActiveConsumer(); + if (checkActiveConsumer != null) { + MethodCreator checkActive = beanCreator.getMethodCreator("checkActive", ActiveResult.class); + checkActiveConsumer.accept(checkActive); + + List matchingIPs = new ArrayList<>(); + for (InjectionPointInfo injectionPoint : bean.getDeployment().getInjectionPoints()) { + if (bean.equals(injectionPoint.getResolvedBean())) { + matchingIPs.add(injectionPoint); + } + } + + ResultHandle active = doCreate.invokeVirtualMethod(checkActive.getMethodDescriptor(), doCreate.getThis()); + ResultHandle activeBool = doCreate.invokeVirtualMethod(MethodDescriptors.ACTIVE_RESULT_VALUE, active); + BytecodeCreator ifNotActive = doCreate.ifFalse(activeBool).trueBranch(); + StringBuilderGenerator msg = Gizmo.newStringBuilder(ifNotActive); + msg.append("Bean is not active: "); + msg.append(Gizmo.toString(ifNotActive, ifNotActive.getThis())); + msg.append("\nReason: "); + msg.append(ifNotActive.invokeVirtualMethod(MethodDescriptors.ACTIVE_RESULT_REASON, active)); + AssignableResultHandle cause = ifNotActive.createVariable(ActiveResult.class); + ifNotActive.assign(cause, ifNotActive.invokeVirtualMethod(MethodDescriptors.ACTIVE_RESULT_CAUSE, active)); + BytecodeCreator loop = ifNotActive.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 = ifNotActive.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()); + } + } + ifNotActive.throwException(ifNotActive.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 e27ea1d084e2e..5f31655c45579 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 @@ -104,13 +104,17 @@ public class BeanInfo implements InjectionTargetInfo { private final Integer startupPriority; + // used to create the implementation of `InjectableBean.checkActive()`, + // which returns whether this particular bean is active at runtime + private final Consumer checkActiveConsumer; + 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, @@ -118,7 +122,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 checkActiveConsumer) { this.target = Optional.ofNullable(target); if (implClazz == null && target != null) { @@ -152,6 +157,7 @@ public class BeanInfo implements InjectionTargetInfo { this.removable = isRemovable; this.params = params; this.interceptionProxy = interceptionProxy; + this.checkActiveConsumer = checkActiveConsumer; // Identifier must be unique for a specific deployment this.identifier = Hashes.sha1_base64((identifier != null ? identifier : "") + toString() + beanDeployment.toString()); this.interceptedMethods = Collections.emptyMap(); @@ -600,6 +606,21 @@ Consumer getDestroyerConsumer() { return destroyerConsumer; } + Consumer getCheckActiveConsumer() { + return checkActiveConsumer; + } + + /** + * Returns whether this bean can be inactive. Most beans are always active, but certain synthetic beans + * may be inactive from time to time. + * + * @return whether this bean can be inactive + * @see #getCheckActiveConsumer() + */ + public boolean canBeInactive() { + return checkActiveConsumer != null; + } + Map getParams() { return params; } @@ -1136,6 +1157,8 @@ static class Builder { private InterceptionProxyInfo interceptionProxy; + private Consumer checkActiveConsumer; + Builder() { injections = Collections.emptyList(); stereotypes = Collections.emptyList(); @@ -1265,11 +1288,16 @@ Builder interceptionProxy(InterceptionProxyInfo interceptionProxy) { return this; } + Builder checkActive(Consumer checkActiveConsumer) { + this.checkActiveConsumer = checkActiveConsumer; + 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, checkActiveConsumer); } 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 2264fde9a8bd5..a3878f4ee298f 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(); } + /** + * Equivalent to {@code checkActive().value()}. + * + * @see #checkActive() + */ + default boolean isActive() { + return checkActive().value(); + } + + /** + * 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 checkActive() { + return ActiveResult.active(); + } + enum Kind { CLASS, diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticBeanWithCheckActiveTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticBeanWithCheckActiveTest.java new file mode 100644 index 0000000000000..3ea65620921f2 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticBeanWithCheckActiveTest.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 SyntheticBeanWithCheckActiveTest { + @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) + .checkActive(MyBeanIsActive.class) + .unremovable() + .done(); + } + }) + .build(); + + @Test + public void test() { + InjectableInstance myBean = Arc.container().select(MyBean.class); + + MyBeanIsActive.active = true; + + assertTrue(myBean.getHandle().getBean().isActive()); + assertNull(myBean.getHandle().getBean().checkActive().inactiveReason()); + assertNotNull(myBean.get()); + + MyBeanIsActive.active = false; + + assertFalse(myBean.getHandle().getBean().isActive()); + assertNotNull(myBean.getHandle().getBean().checkActive().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")); + } + } + } +}