diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 9c2face438419..e24af91afec8d 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -295,6 +295,11 @@ ${project.version} test + + io.quarkus + quarkus-arc-test-supplement + ${project.version} + org.assertj assertj-core diff --git a/extensions/arc/deployment/pom.xml b/extensions/arc/deployment/pom.xml index 750b73d939433..16ef31d3f3bb0 100644 --- a/extensions/arc/deployment/pom.xml +++ b/extensions/arc/deployment/pom.xml @@ -44,6 +44,10 @@ assertj-core test + + io.quarkus + quarkus-arc-test-supplement + jakarta.ejb diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/cdi/bcextensions/SynthBeanForExternalClass.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/cdi/bcextensions/SynthBeanForExternalClass.java new file mode 100644 index 0000000000000..e973a867e756e --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/cdi/bcextensions/SynthBeanForExternalClass.java @@ -0,0 +1,96 @@ +package io.quarkus.arc.test.cdi.bcextensions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; +import jakarta.enterprise.inject.build.compatible.spi.Parameters; +import jakarta.enterprise.inject.build.compatible.spi.Synthesis; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanCreator; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanDisposer; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticComponents; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.test.supplement.SomeClassInExternalLibrary; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusUnitTest; + +public class SynthBeanForExternalClass { + // the test includes an _application_ that declares a build compatible extension + // (in the Runtime CL), which creates a synthetic bean for a class that is _outside_ + // of the application (in the Base Runtime CL) + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyBean.class, MyExtension.class, MySyntheticBeanCreator.class) + .addAsServiceProvider(BuildCompatibleExtension.class, MyExtension.class)) + // we need a non-application archive, so cannot use `withAdditionalDependency()` + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-arc-test-supplement", Version.getVersion()))); + + @Inject + MyBean bean; + + @Test + public void test() { + assertFalse(MySyntheticBeanCreator.created); + assertFalse(MySyntheticBeanDisposer.disposed); + + assertEquals("OK", bean.doSomething()); + assertTrue(MySyntheticBeanCreator.created); + assertTrue(MySyntheticBeanDisposer.disposed); + } + + @ApplicationScoped + public static class MyBean { + @Inject + Instance someClass; + + public String doSomething() { + SomeClassInExternalLibrary instance = someClass.get(); + instance.toString(); // force instantiating the bean + someClass.destroy(instance); // force destroying the instance + return "OK"; + } + } + + public static class MyExtension implements BuildCompatibleExtension { + @Synthesis + public void synthesis(SyntheticComponents syn) { + syn.addBean(SomeClassInExternalLibrary.class) + .type(SomeClassInExternalLibrary.class) + .scope(Dependent.class) + .createWith(MySyntheticBeanCreator.class) + .disposeWith(MySyntheticBeanDisposer.class); + } + } + + public static class MySyntheticBeanCreator implements SyntheticBeanCreator { + public static boolean created; + + public SomeClassInExternalLibrary create(Instance lookup, Parameters params) { + SomeClassInExternalLibrary result = new SomeClassInExternalLibrary(); + created = true; + return result; + } + } + + public static class MySyntheticBeanDisposer implements SyntheticBeanDisposer { + public static boolean disposed; + + @Override + public void dispose(SomeClassInExternalLibrary instance, Instance lookup, Parameters params) { + disposed = true; + } + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/cdi/bcextensions/SynthObserverAsIfInExternalClass.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/cdi/bcextensions/SynthObserverAsIfInExternalClass.java new file mode 100644 index 0000000000000..b9f0bb7af9193 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/cdi/bcextensions/SynthObserverAsIfInExternalClass.java @@ -0,0 +1,79 @@ +package io.quarkus.arc.test.cdi.bcextensions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Event; +import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; +import jakarta.enterprise.inject.build.compatible.spi.Parameters; +import jakarta.enterprise.inject.build.compatible.spi.Synthesis; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticComponents; +import jakarta.enterprise.inject.build.compatible.spi.SyntheticObserver; +import jakarta.enterprise.inject.spi.EventContext; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.test.supplement.SomeClassInExternalLibrary; +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusUnitTest; + +public class SynthObserverAsIfInExternalClass { + // the test includes an _application_ that declares a build compatible extension + // (in the Runtime CL), which creates a synthetic observer which is "as if" declared + // in a class that is _outside_ of the application (in the Base Runtime CL) + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyBean.class, MyExtension.class, MySyntheticObserver.class) + .addAsServiceProvider(BuildCompatibleExtension.class, MyExtension.class)) + // we need a non-application archive, so cannot use `withAdditionalDependency()` + .setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-arc-test-supplement", Version.getVersion()))); + + @Inject + MyBean bean; + + @Test + public void test() { + assertFalse(MySyntheticObserver.notified); + + assertEquals("OK", bean.doSomething()); + assertTrue(MySyntheticObserver.notified); + } + + @ApplicationScoped + public static class MyBean { + @Inject + Event event; + + public String doSomething() { + event.fire(""); // force notifying the observer + return "OK"; + } + } + + public static class MyExtension implements BuildCompatibleExtension { + @Synthesis + public void synthesis(SyntheticComponents syn) { + syn.addObserver(String.class) + .declaringClass(SomeClassInExternalLibrary.class) + .observeWith(MySyntheticObserver.class); + } + } + + public static class MySyntheticObserver implements SyntheticObserver { + public static boolean notified; + + @Override + public void observe(EventContext event, Parameters params) { + notified = true; + } + } +} diff --git a/extensions/arc/pom.xml b/extensions/arc/pom.xml index 455b95ec65d3a..386df972b8bcb 100644 --- a/extensions/arc/pom.xml +++ b/extensions/arc/pom.xml @@ -16,6 +16,7 @@ deployment runtime + test-supplement diff --git a/extensions/arc/test-supplement/pom.xml b/extensions/arc/test-supplement/pom.xml new file mode 100644 index 0000000000000..38b5974d6a65a --- /dev/null +++ b/extensions/arc/test-supplement/pom.xml @@ -0,0 +1,16 @@ + + + + quarkus-arc-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-arc-test-supplement + Quarkus - ArC - Test Supplement + Supplement archive for ArC tests + + diff --git a/extensions/arc/test-supplement/src/main/java/io/quarkus/arc/test/supplement/SomeClassInExternalLibrary.java b/extensions/arc/test-supplement/src/main/java/io/quarkus/arc/test/supplement/SomeClassInExternalLibrary.java new file mode 100644 index 0000000000000..cb045ffd57fd1 --- /dev/null +++ b/extensions/arc/test-supplement/src/main/java/io/quarkus/arc/test/supplement/SomeClassInExternalLibrary.java @@ -0,0 +1,4 @@ +package io.quarkus.arc.test.supplement; + +public class SomeClassInExternalLibrary { +} 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 4e8cce6ae4c8c..51f76f616be88 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 @@ -64,6 +64,8 @@ public class BeanDeployment { private final IndexView beanArchiveImmutableIndex; private final IndexView applicationIndex; + private final Predicate applicationClassPredicate; + private final Map qualifiers; private final Map repeatingQualifierAnnotations; private final Map> qualifierNonbindingMembers; @@ -142,6 +144,7 @@ public class BeanDeployment { this.beanArchiveComputingIndex = builder.beanArchiveComputingIndex; this.beanArchiveImmutableIndex = Objects.requireNonNull(builder.beanArchiveImmutableIndex); this.applicationIndex = builder.applicationIndex; + this.applicationClassPredicate = builder.applicationClassPredicate; this.annotationStore = new AnnotationStore(initAndSort(builder.annotationTransformers, buildContext), buildContext); buildContext.putInternal(Key.ANNOTATION_STORE, annotationStore); @@ -1384,7 +1387,7 @@ private RegistrationContext registerSyntheticBeans(List beanRegis } if (buildCompatibleExtensions != null) { buildCompatibleExtensions.runSynthesis(beanArchiveComputingIndex); - buildCompatibleExtensions.registerSyntheticBeans(context); + buildCompatibleExtensions.registerSyntheticBeans(context, applicationClassPredicate); } this.injectionPoints.addAll(context.syntheticInjectionPoints); return context; @@ -1399,7 +1402,7 @@ io.quarkus.arc.processor.ObserverRegistrar.RegistrationContext registerSynthetic context.extension = null; } if (buildCompatibleExtensions != null) { - buildCompatibleExtensions.registerSyntheticObservers(context); + buildCompatibleExtensions.registerSyntheticObservers(context, applicationClassPredicate); buildCompatibleExtensions.runRegistrationAgain(beanArchiveComputingIndex, beans, observers); } return context; @@ -1433,7 +1436,7 @@ private void addSyntheticObserver(ObserverConfigurator configurator) { configurator.observedQualifiers, Reception.ALWAYS, configurator.transactionPhase, configurator.isAsync, configurator.priority, observerTransformers, buildContext, - jtaCapabilities, configurator.notifyConsumer, configurator.params)); + jtaCapabilities, configurator.notifyConsumer, configurator.params, configurator.forceApplicationClass)); } static void processErrors(List errors) { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DecoratorGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DecoratorGenerator.java index 6b465ba11a4ec..48c92af011388 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DecoratorGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DecoratorGenerator.java @@ -83,7 +83,8 @@ Collection generate(DecoratorInfo decorator) { return Collections.emptyList(); } - boolean isApplicationClass = applicationClassPredicate.test(decorator.getBeanClass()); + boolean isApplicationClass = applicationClassPredicate.test(decorator.getBeanClass()) + || decorator.isForceApplicationClass(); ResourceClassOutput classOutput = new ResourceClassOutput(isApplicationClass, name -> name.equals(generatedName) ? SpecialType.DECORATOR_BEAN : null, generateSources); 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 57295af6ab5fc..5dfac488240a6 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 @@ -108,7 +108,7 @@ private Collection generateSyntheticInterceptor(InterceptorInfo interc return Collections.emptyList(); } - boolean isApplicationClass = applicationClassPredicate.test(creatorClassName); + boolean isApplicationClass = applicationClassPredicate.test(creatorClassName) || interceptor.isForceApplicationClass(); ResourceClassOutput classOutput = new ResourceClassOutput(isApplicationClass, name -> name.equals(generatedName) ? SpecialType.INTERCEPTOR_BEAN : null, generateSources); @@ -168,7 +168,8 @@ private Collection generateClassInterceptor(InterceptorInfo intercepto return Collections.emptyList(); } - boolean isApplicationClass = applicationClassPredicate.test(interceptor.getBeanClass()); + boolean isApplicationClass = applicationClassPredicate.test(interceptor.getBeanClass()) + || interceptor.isForceApplicationClass(); ResourceClassOutput classOutput = new ResourceClassOutput(isApplicationClass, name -> name.equals(generatedName) ? SpecialType.INTERCEPTOR_BEAN : null, generateSources); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverConfigurator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverConfigurator.java index 4ab900d7b8d24..c65cf58bd845b 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverConfigurator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverConfigurator.java @@ -35,6 +35,7 @@ public final class ObserverConfigurator extends ConfiguratorBase notifyConsumer; + boolean forceApplicationClass; public ObserverConfigurator(Consumer consumer) { this.consumer = consumer; @@ -118,6 +119,15 @@ public ObserverConfigurator notify(Consumer notifyConsumer) { return this; } + /** + * Forces the observer to be considered an 'application class', so it will be defined in the runtime + * ClassLoader and re-created on each redeployment. + */ + public ObserverConfigurator forceApplicationClass() { + this.forceApplicationClass = true; + return this; + } + public void done() { if (beanClass == null) { throw new IllegalStateException("Observer bean class must be set!"); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverGenerator.java index f0783ac0885c0..91b6fd82d202a 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverGenerator.java @@ -181,7 +181,8 @@ Collection generate(ObserverInfo observer) { return Collections.emptyList(); } - boolean isApplicationClass = applicationClassPredicate.test(observer.getBeanClass()); + boolean isApplicationClass = applicationClassPredicate.test(observer.getBeanClass()) + || observer.isForceApplicationClass(); ResourceClassOutput classOutput = new ResourceClassOutput(isApplicationClass, name -> name.equals(generatedName) ? SpecialType.OBSERVER : null, generateSources); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverInfo.java index ec23ce565663d..1c75166c852ed 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverInfo.java @@ -87,7 +87,7 @@ static ObserverInfo create(BeanInfo declaringBean, MethodInfo observerMethod, In initQualifiers(beanDeployment, observerMethod, eventParameter), reception, initTransactionPhase(isAsync, beanDeployment, observerMethod), isAsync, priority, transformers, - buildContext, jtaCapabilities, null, Collections.emptyMap()); + buildContext, jtaCapabilities, null, Collections.emptyMap(), false); } static ObserverInfo create(String id, BeanDeployment beanDeployment, DotName beanClass, BeanInfo declaringBean, @@ -95,7 +95,7 @@ static ObserverInfo create(String id, BeanDeployment beanDeployment, DotName bea MethodParameterInfo eventParameter, Type observedType, Set qualifiers, Reception reception, TransactionPhase transactionPhase, boolean isAsync, int priority, List transformers, BuildContext buildContext, boolean jtaCapabilities, - Consumer notify, Map params) { + Consumer notify, Map params, boolean forceApplicationClass) { if (!transformers.isEmpty()) { // Transform attributes if needed @@ -141,7 +141,8 @@ static ObserverInfo create(String id, BeanDeployment beanDeployment, DotName bea info, transactionPhase); } return new ObserverInfo(id, beanDeployment, beanClass, declaringBean, observerMethod, injection, eventParameter, - isAsync, priority, reception, transactionPhase, observedType, qualifiers, notify, params); + isAsync, priority, reception, transactionPhase, observedType, qualifiers, notify, params, + forceApplicationClass); } private final String id; @@ -178,11 +179,15 @@ static ObserverInfo create(String id, BeanDeployment beanDeployment, DotName bea private final Map params; - ObserverInfo(String id, BeanDeployment beanDeployment, DotName beanClass, BeanInfo declaringBean, MethodInfo observerMethod, + private final boolean forceApplicationClass; + + private ObserverInfo(String id, BeanDeployment beanDeployment, DotName beanClass, BeanInfo declaringBean, + MethodInfo observerMethod, Injection injection, MethodParameterInfo eventParameter, boolean isAsync, int priority, Reception reception, TransactionPhase transactionPhase, - Type observedType, Set qualifiers, Consumer notify, Map params) { + Type observedType, Set qualifiers, Consumer notify, + Map params, boolean forceApplicationClass) { this.id = id; this.beanDeployment = beanDeployment; this.beanClass = beanClass; @@ -199,6 +204,7 @@ static ObserverInfo create(String id, BeanDeployment beanDeployment, DotName bea this.qualifiers = qualifiers; this.notify = notify; this.params = params; + this.forceApplicationClass = forceApplicationClass; } @Override @@ -297,6 +303,10 @@ Map getParams() { return params; } + boolean isForceApplicationClass() { + return forceApplicationClass; + } + void init(List errors) { if (injection != null) { for (InjectionPointInfo injectionPoint : injection.injectionPoints) { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java index cf490e5604ff8..b2ddd4f8829ec 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/bcextensions/ExtensionsEntryPoint.java @@ -12,6 +12,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; import java.util.stream.Collectors; import jakarta.enterprise.context.Dependent; @@ -312,7 +313,7 @@ public void runSynthesis(org.jboss.jandex.IndexView beanArchiveIndex) { *

* It is a no-op if no {@link BuildCompatibleExtension} was found. */ - public void registerSyntheticBeans(BeanRegistrar.RegistrationContext context) { + public void registerSyntheticBeans(BeanRegistrar.RegistrationContext context, Predicate isApplicationClass) { if (invoker.isEmpty()) { return; } @@ -424,6 +425,16 @@ public void registerSyntheticBeans(BeanRegistrar.RegistrationContext context) { mc.returnValue(null); }); } + // the generated classes need to see the `creatorClass` and the `disposerClass`, + // so if they are application classes, the generated classes are forced to also + // be application classes, even if the `implementationClass` possibly isn't + if (isApplicationClass.test(DotName.createSimple(syntheticBean.creatorClass))) { + bean.forceApplicationClass(); + } + if (syntheticBean.disposerClass != null + && isApplicationClass.test(DotName.createSimple(syntheticBean.disposerClass))) { + bean.forceApplicationClass(); + } bean.done(); } } @@ -433,7 +444,8 @@ public void registerSyntheticBeans(BeanRegistrar.RegistrationContext context) { *

* It is a no-op if no {@link BuildCompatibleExtension} was found. */ - public void registerSyntheticObservers(ObserverRegistrar.RegistrationContext context) { + public void registerSyntheticObservers(ObserverRegistrar.RegistrationContext context, + Predicate isApplicationClass) { if (invoker.isEmpty()) { return; } @@ -475,6 +487,12 @@ public void registerSyntheticObservers(ObserverRegistrar.RegistrationContext con // return type is void mc.returnValue(null); }); + // the generated classes need to see the `implementationClass`, so if it is + // an application class, the generated classes are forced to also be application + // classes, even if the `declaringClass` possibly isn't + if (isApplicationClass.test(DotName.createSimple(syntheticObserver.implementationClass))) { + observer.forceApplicationClass(); + } observer.done(); } }