Skip to content

Commit

Permalink
ArC: fix in-application build compatible extensions
Browse files Browse the repository at this point in the history
In case the _application_ contains a build compatible extension that
registers a synthetic bean for a _non-application_ class, or a synthetic
observer declared "as if" in a _non-application_ class, the generated
classes must be forced to also be application classes. If the generated
classes were non-application classes (as they were prior to this commit),
they would not see the creator/disposer/observer classes in Quarkus
dev mode and test mode (where application classes are in a different
class loader than non-application classes).

Before this commit, it has already been possible to force ArC to generate
synthetic bean classes as application classes, but this was not possible
for synthetic observers. This commit adds that, too.
  • Loading branch information
Ladicek committed Nov 29, 2023
1 parent d458399 commit 36b9976
Show file tree
Hide file tree
Showing 14 changed files with 263 additions and 14 deletions.
5 changes: 5 additions & 0 deletions build-parent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,11 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc-test-supplement</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
Expand Down
4 changes: 4 additions & 0 deletions extensions/arc/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc-test-supplement</artifactId>
</dependency>
<!-- Used to test wrong @Singleton detection -->
<dependency>
<groupId>jakarta.ejb</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SomeClassInExternalLibrary> 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<SomeClassInExternalLibrary> {
public static boolean created;

public SomeClassInExternalLibrary create(Instance<Object> lookup, Parameters params) {
SomeClassInExternalLibrary result = new SomeClassInExternalLibrary();
created = true;
return result;
}
}

public static class MySyntheticBeanDisposer implements SyntheticBeanDisposer<SomeClassInExternalLibrary> {
public static boolean disposed;

@Override
public void dispose(SomeClassInExternalLibrary instance, Instance<Object> lookup, Parameters params) {
disposed = true;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> {
public static boolean notified;

@Override
public void observe(EventContext<String> event, Parameters params) {
notified = true;
}
}
}
1 change: 1 addition & 0 deletions extensions/arc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<modules>
<module>deployment</module>
<module>runtime</module>
<module>test-supplement</module>
</modules>

</project>
16 changes: 16 additions & 0 deletions extensions/arc/test-supplement/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>quarkus-arc-parent</artifactId>
<groupId>io.quarkus</groupId>
<version>999-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>quarkus-arc-test-supplement</artifactId>
<name>Quarkus - ArC - Test Supplement</name>
<description>Supplement archive for ArC tests</description>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.quarkus.arc.test.supplement;

public class SomeClassInExternalLibrary {
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ public class BeanDeployment {
private final IndexView beanArchiveImmutableIndex;
private final IndexView applicationIndex;

private final Predicate<DotName> applicationClassPredicate;

private final Map<DotName, ClassInfo> qualifiers;
private final Map<DotName, ClassInfo> repeatingQualifierAnnotations;
private final Map<DotName, Set<String>> qualifierNonbindingMembers;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -1384,7 +1387,7 @@ private RegistrationContext registerSyntheticBeans(List<BeanRegistrar> beanRegis
}
if (buildCompatibleExtensions != null) {
buildCompatibleExtensions.runSynthesis(beanArchiveComputingIndex);
buildCompatibleExtensions.registerSyntheticBeans(context);
buildCompatibleExtensions.registerSyntheticBeans(context, applicationClassPredicate);
}
this.injectionPoints.addAll(context.syntheticInjectionPoints);
return context;
Expand All @@ -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;
Expand Down Expand Up @@ -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<Throwable> errors) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ Collection<Resource> 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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ private Collection<Resource> 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);

Expand Down Expand Up @@ -168,7 +168,8 @@ private Collection<Resource> 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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public final class ObserverConfigurator extends ConfiguratorBase<ObserverConfigu
boolean isAsync;
TransactionPhase transactionPhase;
Consumer<MethodCreator> notifyConsumer;
boolean forceApplicationClass;

public ObserverConfigurator(Consumer<ObserverConfigurator> consumer) {
this.consumer = consumer;
Expand Down Expand Up @@ -118,6 +119,15 @@ public ObserverConfigurator notify(Consumer<MethodCreator> 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!");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ Collection<Resource> 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);

Expand Down
Loading

0 comments on commit 36b9976

Please sign in to comment.