diff --git a/docs/src/main/asciidoc/cdi-reference.adoc b/docs/src/main/asciidoc/cdi-reference.adoc index d2ffd27db21a8..0ad8ecc3a46a2 100644 --- a/docs/src/main/asciidoc/cdi-reference.adoc +++ b/docs/src/main/asciidoc/cdi-reference.adoc @@ -391,6 +391,24 @@ This optimization applies to all forms of bean declarations: bean class, produce Users can instruct the container to not remove any of their specific beans (even if they satisfy all the rules specified above) by annotating them with `io.quarkus.arc.Unremovable`. This annotation can be placed on the types, producer methods, and producer fields. +Since this is not always possible, there is an option to achieve the same via `application.properties`. +The `quarkus.arc.unremovable-types` property accepts a list of string values that are used to match beans based on their name or package. + +.Value Examples +|=== +|Value|Description +|`org.acme.Foo`| Match the fully qualified name of the bean class +|`org.acme.*`| Match beans where the package of the bean class is `org.acme` +|`org.acme.**`| Match beans where the package of the bean class starts with `org.acme` +|`Bar`| Match the simple name of the bean class +|=== + +.Example application.properties +[source,properties] +---- +quarkus.arc.unremovable-types=org.acme.Foo,org.acme.*,Bar +---- + Furthermore, extensions can eliminate possible false positives by producing `UnremovableBeanBuildItem`. Finally, Quarkus provides a middle ground for the bean removal optimization where application beans are never removed whether or not they are unused, diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java index 913ad6dc7ea3a..4babe0e9d06f9 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java @@ -112,6 +112,26 @@ public class ArcConfig { @ConfigItem public Optional> excludeTypes; + /** + * List of types that should be considered unremovable regardless of whether they are directly used or not. + * This is a configuration option equivalent to using {@link io.quarkus.arc.Unremovable} annotation. + * + *

+ * An element value can be: + *

+ * If any element value matches a discovered bean, then such a bean is considered unremovable. + * + * @see {@link #removeUnusedBeans} + * @see {@link io.quarkus.arc.Unremovable} + */ + @ConfigItem + public Optional> unremovableTypes; + /** * The artifacts that should be excluded from discovery. These artifacts would be otherwise scanned for beans, i.e. they * contain a Jandex index or a beans.xml descriptor. diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java index 02c15b2acd30a..a762d62ebc4fb 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java @@ -259,6 +259,25 @@ protected DotName getDotName(BeanInfo bean) { for (UnremovableBeanBuildItem exclusion : removalExclusions) { builder.addRemovalExclusion(exclusion.getPredicate()); } + // unremovable beans specified in application.properties + if (arcConfig.unremovableTypes.isPresent()) { + List> classPredicates = initClassPredicates(arcConfig.unremovableTypes.get()); + builder.addRemovalExclusion(new Predicate() { + @Override + public boolean test(BeanInfo beanInfo) { + ClassInfo beanClass = beanInfo.getImplClazz(); + if (beanClass != null) { + // if any of the predicates match, we make the given bean unremovable + for (Predicate predicate : classPredicates) { + if (predicate.test(beanClass)) { + return true; + } + } + } + return false; + } + }); + } if (testClassPredicate.isPresent()) { builder.addRemovalExclusion(new Predicate() { @Override @@ -478,11 +497,11 @@ CustomScopeAnnotationsBuildItem exposeCustomScopeNames(List> initClassPredicates(List selectedAlternatives) { + private List> initClassPredicates(List types) { final String packMatch = ".*"; final String packStarts = ".**"; List> predicates = new ArrayList<>(); - for (String val : selectedAlternatives) { + for (String val : types) { if (val.endsWith(packMatch)) { // Package matches final String pack = val.substring(0, val.length() - packMatch.length()); diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/Charlie.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/Charlie.java new file mode 100644 index 0000000000000..658ff83938c4a --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/Charlie.java @@ -0,0 +1,11 @@ +package io.quarkus.arc.test.unused; + +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class Charlie { + + public String ping() { + return "ok"; + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/Delta.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/Delta.java new file mode 100644 index 0000000000000..e256de932ef52 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/Delta.java @@ -0,0 +1,17 @@ +package io.quarkus.arc.test.unused; + +/** + * Not a bean on its own but has a producer + */ +public class Delta { + + private String s; + + public Delta(String s) { + this.s = s; + } + + public String ping() { + return s; + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/ProducerBean.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/ProducerBean.java new file mode 100644 index 0000000000000..a19e62a9026d5 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/ProducerBean.java @@ -0,0 +1,17 @@ +package io.quarkus.arc.test.unused; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; + +import io.quarkus.arc.Unremovable; + +@ApplicationScoped +@Unremovable +public class ProducerBean { + + @Produces + @ApplicationScoped + Delta produceDelta() { + return new Delta("ok"); + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/UnusedExclusionTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/UnusedExclusionTest.java new file mode 100644 index 0000000000000..22bfca5a9c46e --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/UnusedExclusionTest.java @@ -0,0 +1,59 @@ +package io.quarkus.arc.test.unused; + +import javax.enterprise.context.ApplicationScoped; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.test.unused.subpackage.Beta; +import io.quarkus.test.QuarkusUnitTest; + +public class UnusedExclusionTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(UnusedExclusionTest.class, Alpha.class, Beta.class, Charlie.class, Delta.class, + ProducerBean.class) + .addAsResource(new StringAsset( + "quarkus.arc.unremovable-types=io.quarkus.arc.test.unused.UnusedExclusionTest$Alpha,io.quarkus.arc.test.unused.subpackage.**,io.quarkus.arc.test.unused.Charlie,Delta"), + "application.properties")); + + @Test + public void testBeansWereNotRemoved() { + ArcContainer container = Arc.container(); + String expectedBeanResponse = "ok"; + InstanceHandle alphaInstance = container.instance(Alpha.class); + Assertions.assertTrue(alphaInstance.isAvailable()); + Assertions.assertEquals(expectedBeanResponse, alphaInstance.get().ping()); + + InstanceHandle betaInstance = container.instance(Beta.class); + Assertions.assertTrue(betaInstance.isAvailable()); + Assertions.assertEquals(expectedBeanResponse, betaInstance.get().ping()); + + InstanceHandle charlieInstance = container.instance(Charlie.class); + Assertions.assertTrue(charlieInstance.isAvailable()); + Assertions.assertEquals(expectedBeanResponse, charlieInstance.get().ping()); + + InstanceHandle deltaInstance = container.instance(Delta.class); + Assertions.assertTrue(deltaInstance.isAvailable()); + Assertions.assertEquals(expectedBeanResponse, deltaInstance.get().ping()); + } + + // unused bean, won't be removed + @ApplicationScoped + static class Alpha { + + public String ping() { + return "ok"; + } + + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/subpackage/Beta.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/subpackage/Beta.java new file mode 100644 index 0000000000000..faea7869514c0 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/subpackage/Beta.java @@ -0,0 +1,14 @@ +package io.quarkus.arc.test.unused.subpackage; + +import javax.enterprise.context.ApplicationScoped; + +/** + * Another unused bean that shouldn't be removed. + */ +@ApplicationScoped +public class Beta { + + public String ping() { + return "ok"; + } +}