Skip to content

Commit

Permalink
Merge pull request #41810 from Ladicek/arc-inactive-synth-beans
Browse files Browse the repository at this point in the history
ArC: initial support for inactive synthetic beans
  • Loading branch information
Ladicek authored Oct 8, 2024
2 parents 5ca9c60 + 30325f2 commit 6543030
Show file tree
Hide file tree
Showing 15 changed files with 519 additions and 31 deletions.
67 changes: 67 additions & 0 deletions docs/src/main/asciidoc/cdi-integration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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<ActiveResult>` 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<ActiveResult> isFooActive() {
return () -> {
if (... should not be active ...) { // <1>
return ActiveResult.inactive("explanation"); // <2>
}
return ActiveResult.active();
};
}
public Function<SyntheticCreationalContext<Foo>, 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> foo;
if (foo.getHandle().getBean().isActive()) {
Foo foo = foo.get();
...
}
----

[[synthetic_observers]]
== Use Case - Synthetic Observers

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -212,7 +234,7 @@ private void registerStartupObserver(ObserverRegistrationPhaseBuildItem observer
mc.invokeInterfaceMethod(CLIENT_PROXY_CONTEXTUAL_INSTANCE, proxyHandle);
}
}
mc.returnValue(null);
mc.returnVoid();
});
configurator.done();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.jboss.jandex.AnnotationInstance;
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;
Expand Down Expand Up @@ -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.
*/
Expand All @@ -79,6 +85,8 @@ public static class ExtendedBeanConfigurator extends BeanConfiguratorBase<Extend
private Function<SyntheticCreationalContext<?>, ?> fun;
private boolean staticInit;

private Supplier<ActiveResult> checkActive;

ExtendedBeanConfigurator(DotName implClazz) {
super(implClazz);
this.staticInit = true;
Expand All @@ -92,7 +100,13 @@ public static class ExtendedBeanConfigurator extends BeanConfiguratorBase<Extend
public SyntheticBeanBuildItem done() {
if (supplier == null && runtimeValue == null && fun == null && runtimeProxy == null && creatorConsumer == null) {
throw new IllegalStateException(
"Synthetic bean does not provide a creation method, use ExtendedBeanConfigurator#creator(), ExtendedBeanConfigurator#supplier(), ExtendedBeanConfigurator#createWith() or ExtendedBeanConfigurator#runtimeValue()");
"Synthetic bean does not provide a creation method, use ExtendedBeanConfigurator#creator(), ExtendedBeanConfigurator#supplier(), ExtendedBeanConfigurator#createWith() or ExtendedBeanConfigurator#runtimeValue()");
}
if (checkActive != null && supplier == null && runtimeValue == null && fun == null && runtimeProxy == null) {
// "check active" procedure is set via recorder proxy,
// creation function must also be set via recorder proxy
throw new IllegalStateException(
"Synthetic bean has ExtendedBeanConfigurator#checkActive(), but does not have ExtendedBeanConfigurator#supplier() / createWith() / runtimeValue() / runtimeProxy()");
}
return new SyntheticBeanBuildItem(this);
}
Expand Down Expand Up @@ -181,6 +195,20 @@ public ExtendedBeanConfigurator setRuntimeInit() {
return this;
}

/**
* The {@link #checkActive(Consumer)} procedure is a {@code Supplier<ActiveResult>} proxy
* returned from a recorder method.
*
* @param checkActive a {@code Supplier<ActiveResult>} 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<ActiveResult> checkActive) {
checkReturnedProxy(checkActive);
this.checkActive = Objects.requireNonNull(checkActive);
return this;
}

DotName getImplClazz() {
return implClazz;
}
Expand Down Expand Up @@ -213,6 +241,10 @@ Object getRuntimeProxy() {
return runtimeProxy;
}

Supplier<ActiveResult> getCheckActive() {
return checkActive;
}

private void checkMultipleCreationMethods() {
if (runtimeProxy == null && runtimeValue == null && supplier == null && fun == null) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,15 +34,16 @@ public class SyntheticBeansProcessor {
void initStatic(ArcRecorder recorder, List<SyntheticBeanBuildItem> syntheticBeans,
BeanRegistrationPhaseBuildItem beanRegistration, BuildProducer<BeanConfiguratorBuildItem> configurators) {

Map<String, Function<SyntheticCreationalContext<?>, ?>> functionsMap = new HashMap<>();
Map<String, Function<SyntheticCreationalContext<?>, ?>> creationFunctions = new HashMap<>();
Map<String, Supplier<ActiveResult>> 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)
Expand All @@ -49,14 +52,15 @@ void initStatic(ArcRecorder recorder, List<SyntheticBeanBuildItem> syntheticBean
ServiceStartBuildItem initRuntime(ArcRecorder recorder, List<SyntheticBeanBuildItem> syntheticBeans,
BeanRegistrationPhaseBuildItem beanRegistration, BuildProducer<BeanConfiguratorBuildItem> configurators) {

Map<String, Function<SyntheticCreationalContext<?>, ?>> functionsMap = new HashMap<>();
Map<String, Function<SyntheticCreationalContext<?>, ?>> creationFunctions = new HashMap<>();
Map<String, Supplier<ActiveResult>> 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");
}

Expand All @@ -66,29 +70,34 @@ void initRegular(List<SyntheticBeanBuildItem> 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<String, Function<SyntheticCreationalContext<?>, ?>> functionsMap,
BeanRegistrationPhaseBuildItem beanRegistration, SyntheticBeanBuildItem bean) {
Map<String, Function<SyntheticCreationalContext<?>, ?>> creationFunctions,
Map<String, Supplier<ActiveResult>> 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();
}

Expand All @@ -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));
Expand All @@ -118,9 +127,27 @@ public void accept(MethodCreator m) {
};
}

private String createMessage(String name, SyntheticBeanBuildItem bean) {
private Consumer<MethodCreator> checkActive(String name, SyntheticBeanBuildItem bean) {
return new Consumer<MethodCreator>() {
@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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,6 +42,8 @@ public class ArcRecorder {
*/
public static volatile Map<String, Function<SyntheticCreationalContext<?>, ?>> syntheticBeanProviders;

public static volatile Map<String, Supplier<ActiveResult>> syntheticBeanCheckActive;

public ArcContainer initContainer(ShutdownContext shutdown, RuntimeValue<CurrentContextFactory> currentContextFactory,
boolean strictCompatibility) throws Exception {
ArcInitConfig.Builder builder = ArcInitConfig.builder();
Expand All @@ -61,12 +64,16 @@ public void initExecutor(ExecutorService executor) {
Arc.setExecutor(executor);
}

public void initStaticSupplierBeans(Map<String, Function<SyntheticCreationalContext<?>, ?>> beans) {
syntheticBeanProviders = new ConcurrentHashMap<>(beans);
public void initStaticSupplierBeans(Map<String, Function<SyntheticCreationalContext<?>, ?>> creationFunctions,
Map<String, Supplier<ActiveResult>> checkActiveSuppliers) {
syntheticBeanProviders = new ConcurrentHashMap<>(creationFunctions);
syntheticBeanCheckActive = new ConcurrentHashMap<>(checkActiveSuppliers);
}

public void initRuntimeSupplierBeans(Map<String, Function<SyntheticCreationalContext<?>, ?>> beans) {
syntheticBeanProviders.putAll(beans);
public void initRuntimeSupplierBeans(Map<String, Function<SyntheticCreationalContext<?>, ?>> creationFunctions,
Map<String, Supplier<ActiveResult>> checkActiveSuppliers) {
syntheticBeanProviders.putAll(creationFunctions);
syntheticBeanCheckActive.putAll(checkActiveSuppliers);
}

public BeanContainer initBeanContainer(ArcContainer container, List<BeanContainerListener> listeners)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down
Loading

0 comments on commit 6543030

Please sign in to comment.