From c5476065d080f6cb11f62057261e7af666bf4f3a Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Fri, 23 Jul 2021 16:39:00 +0200 Subject: [PATCH] ArC - remove unused interceptors and decorators - and beans injected only by these interceptors and decorators - and beans that are only injected in removed beans - log the full stack trace if DEBUG level is enabled - fix JTA and SR CP integration - - the tx manager is obtained via CDI.current().select(TransactionManager.class) in the JtaContextProvider - fix SmallRye Metrics - MetricRegistries must be unremovable - fix cache and rest-client integration - fix SmallRye OpenTracing - make any bean that has io.opentracing.Tracer in bean types unremovable - update the docs --- docs/src/main/asciidoc/cdi-reference.adoc | 65 ++++--- .../InterceptedStaticMethodsProcessor.java | 7 +- .../cache/deployment/CacheProcessor.java | 42 ++++- .../mailer/deployment/MailerProcessor.java | 17 +- .../mailer/MailTemplateValidationTest.java | 2 + .../jta/deployment/NarayanaJtaProcessor.java | 7 +- .../SmallRyeFaultToleranceProcessor.java | 35 ++-- .../deployment/SmallRyeGraphQLProcessor.java | 10 +- .../deployment/SmallRyeMetricsProcessor.java | 14 +- .../SmallRyeOpenTracingProcessor.java | 7 +- .../quarkus/arc/processor/BeanDeployment.java | 172 +++++++++++------- .../io/quarkus/arc/processor/BeanInfo.java | 3 +- .../arc/processor/InjectionPointInfo.java | 4 + .../io/quarkus/arc/processor/UnusedBeans.java | 94 ++++++++++ .../io/quarkus/arc/impl/ArcContainerImpl.java | 33 ++++ .../io/quarkus/arc/impl/BeanManagerImpl.java | 2 +- .../io/quarkus/arc/impl/RemovedBeanImpl.java | 4 +- .../io/quarkus/arc/test/unused/Alpha.java | 18 ++ .../arc/test/unused/AlphaInterceptor.java | 21 +++ .../io/quarkus/arc/test/unused/Bravo.java | 18 ++ .../arc/test/unused/BravoInterceptor.java | 17 ++ .../io/quarkus/arc/test/unused/Counter.java | 24 +++ .../test/unused/RemoveUnusedBeansTest.java | 46 +++-- .../unused/RemoveUnusedComponentsTest.java | 17 ++ .../unused/RemoveUnusedDecoratorTest.java | 64 +++++++ .../unused/RemoveUnusedInterceptorTest.java | 46 +++++ 26 files changed, 631 insertions(+), 158 deletions(-) create mode 100644 independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/UnusedBeans.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/Alpha.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/AlphaInterceptor.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/Bravo.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/BravoInterceptor.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/Counter.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/RemoveUnusedComponentsTest.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/RemoveUnusedDecoratorTest.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/RemoveUnusedInterceptorTest.java diff --git a/docs/src/main/asciidoc/cdi-reference.adoc b/docs/src/main/asciidoc/cdi-reference.adoc index a76be0370e810..fe5dd280c8998 100644 --- a/docs/src/main/asciidoc/cdi-reference.adoc +++ b/docs/src/main/asciidoc/cdi-reference.adoc @@ -396,23 +396,50 @@ NOTE: We don't generate a no-args constructor automatically if a bean class exte [[remove_unused_beans]] === Removing Unused Beans -The container attempts to remove all unused beans during build by default. -This optimization can be disabled by setting `quarkus.arc.remove-unused-beans` to `none` or `false`. +The container attempts to remove all unused beans, interceptors and decorators during build by default. +This optimization helps to minimize the amount of generated classes, thus conserving memory. +However, Quarkus can't detect the programmatic lookup performed via the `CDI.current()` static method. +Therefore, it is possible that a removal results in a false positive error, i.e. a bean is removed although it's actually used. +In such cases, you'll notice a big warning in the log. +Users and extension authors have several options <>. -An unused bean: +The optimization can be disabled by setting `quarkus.arc.remove-unused-beans` to `none` or `false`. +Quarkus also provides a middle ground where application beans are never removed whether or not they are unused, while the optimization proceeds normally for non application classes. +To use this mode, set `quarkus.arc.remove-unused-beans` to `fwk` or `framework`. -* is not a built-in bean or an interceptor, -* is not eligible for injection to any injection point, -* is not excluded by any extension, -* does not have a name, -* does not declare an observer, -* does not declare any producer which is eligible for injection to any injection point, -* is not directly eligible for injection into any `javax.enterprise.inject.Instance` or `javax.inject.Provider` injection point +==== What's Removed? -This optimization applies to all forms of bean declarations: bean class, producer method, producer field. +Quarkus first identifies so-called _unremovable_ beans that form the roots in the dependency tree. +A good example is a JAX-RS resource class or a bean which declares a `@Scheduled` method. -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. +An _unremovable_ bean: + +* is excluded from removal by an extension, or +* has a name designated via `@Named`, or +* declares an observer method. + +An _unused_ bean: + +* is not _unremovable_, and +* is not eligible for injection to any injection point in the dependency tree, and +* does not declare any producer which is eligible for injection to any injection point in the dependency tree, and +* is not eligible for injection into any `javax.enterprise.inject.Instance` or `javax.inject.Provider` injection point. + +Unused interceptors and decorators are not associated with any bean. + +[TIP] +==== +When using the dev mode (running `./mvnw clean compile quarkus:dev`), you can see more information about which beans are being removed: + +1. In the console - just enable the DEBUG level in your `application.properties`, i.e. `quarkus.log.category."io.quarkus.arc.processor".level=DEBUG` +2. In the relevant Dev UI page +==== + +[[eliminate_false_positives]] +==== How To Eliminate False Positives + +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 declared on a class, a producer method or field. 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. @@ -432,17 +459,7 @@ The `quarkus.arc.unremovable-types` property accepts a list of string values tha 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, -while the optimization proceeds normally for non application classes. To use this mode, set `quarkus.arc.remove-unused-beans` to `fwk` or `framework`. - -When using the dev mode (running `./mvnw clean compile quarkus:dev`), you can see more information about which beans are being removed -by enabling additional logging via the following line in your `application.properties`. - ----- -quarkus.log.category."io.quarkus.arc.processor".level=DEBUG ----- +Furthermore, extensions can eliminate false positives by producing an `UnremovableBeanBuildItem`. [[default_beans]] === Default Beans diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/staticmethods/InterceptedStaticMethodsProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/staticmethods/InterceptedStaticMethodsProcessor.java index d04f45d0b6989..a34cce279184c 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/staticmethods/InterceptedStaticMethodsProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/staticmethods/InterceptedStaticMethodsProcessor.java @@ -18,6 +18,7 @@ import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.stream.Collectors; import javax.enterprise.context.spi.Contextual; import javax.enterprise.inject.spi.InterceptionType; @@ -41,6 +42,7 @@ import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem; import io.quarkus.arc.deployment.InterceptorResolverBuildItem; import io.quarkus.arc.deployment.TransformedAnnotationsBuildItem; +import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.arc.impl.CreationalContextImpl; import io.quarkus.arc.impl.InterceptedMethodMetadata; import io.quarkus.arc.impl.InterceptedStaticMethods; @@ -85,7 +87,8 @@ public class InterceptedStaticMethodsProcessor { @BuildStep void collectInterceptedStaticMethods(BeanArchiveIndexBuildItem beanArchiveIndex, BuildProducer interceptedStaticMethods, - InterceptorResolverBuildItem interceptorResolver, TransformedAnnotationsBuildItem transformedAnnotations) { + InterceptorResolverBuildItem interceptorResolver, TransformedAnnotationsBuildItem transformedAnnotations, + BuildProducer unremovableBeans) { // In this step we collect all intercepted static methods, ie. static methods annotated with interceptor bindings Set interceptorBindings = interceptorResolver.getInterceptorBindings(); @@ -127,6 +130,8 @@ void collectInterceptedStaticMethods(BeanArchiveIndexBuildItem beanArchiveIndex, if (!interceptors.isEmpty()) { LOGGER.debugf("Intercepted static method found on %s: %s", method.declaringClass().name(), method); + unremovableBeans.produce(UnremovableBeanBuildItem.beanClassNames(interceptors.stream() + .map(InterceptorInfo::getBeanClass).map(Object::toString).collect(Collectors.toSet()))); interceptedStaticMethods.produce( new InterceptedStaticMethodBuildItem(method, methodLevelBindings, interceptors)); } diff --git a/extensions/cache/deployment/src/main/java/io/quarkus/cache/deployment/CacheProcessor.java b/extensions/cache/deployment/src/main/java/io/quarkus/cache/deployment/CacheProcessor.java index 8f562a9fc0567..9d867f5322c5f 100644 --- a/extensions/cache/deployment/src/main/java/io/quarkus/cache/deployment/CacheProcessor.java +++ b/extensions/cache/deployment/src/main/java/io/quarkus/cache/deployment/CacheProcessor.java @@ -1,6 +1,8 @@ package io.quarkus.cache.deployment; import static io.quarkus.cache.deployment.CacheDeploymentConstants.CACHE_INVALIDATE; +import static io.quarkus.cache.deployment.CacheDeploymentConstants.CACHE_INVALIDATE_ALL; +import static io.quarkus.cache.deployment.CacheDeploymentConstants.CACHE_INVALIDATE_ALL_LIST; import static io.quarkus.cache.deployment.CacheDeploymentConstants.CACHE_INVALIDATE_LIST; import static io.quarkus.cache.deployment.CacheDeploymentConstants.CACHE_KEY; import static io.quarkus.cache.deployment.CacheDeploymentConstants.CACHE_NAME; @@ -33,6 +35,7 @@ import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; import io.quarkus.arc.deployment.AutoInjectAnnotationBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; import io.quarkus.cache.CacheManager; import io.quarkus.cache.deployment.exception.ClassTargetException; @@ -40,6 +43,9 @@ import io.quarkus.cache.deployment.exception.UnknownCacheNameException; import io.quarkus.cache.deployment.exception.UnsupportedRepeatedAnnotationException; import io.quarkus.cache.deployment.exception.VoidReturnTypeTargetException; +import io.quarkus.cache.runtime.CacheInvalidateAllInterceptor; +import io.quarkus.cache.runtime.CacheInvalidateInterceptor; +import io.quarkus.cache.runtime.CacheResultInterceptor; import io.quarkus.cache.runtime.caffeine.CaffeineCacheBuildRecorder; import io.quarkus.cache.runtime.caffeine.CaffeineCacheInfo; import io.quarkus.cache.runtime.noop.NoOpCacheBuildRecorder; @@ -209,14 +215,32 @@ SyntheticBeanBuildItem configureCacheManagerSyntheticBean(CacheNamesBuildItem ca } @BuildStep - List enhanceRestClientMethods(CombinedIndexBuildItem combinedIndex) { + List enhanceRestClientMethods(CombinedIndexBuildItem combinedIndex, + BuildProducer unremovableBeans) { List bytecodeTransformers = new ArrayList<>(); + boolean cacheInvalidate = false; + boolean cacheResult = false; + boolean cacheInvalidateAll = false; + for (AnnotationInstance registerRestClientAnnotation : combinedIndex.getIndex().getAnnotations(REGISTER_REST_CLIENT)) { if (registerRestClientAnnotation.target().kind() == Kind.CLASS) { ClassInfo classInfo = registerRestClientAnnotation.target().asClass(); for (MethodInfo methodInfo : classInfo.methods()) { - if (methodInfo.hasAnnotation(CACHE_INVALIDATE) || methodInfo.hasAnnotation(CACHE_INVALIDATE_LIST) - || methodInfo.hasAnnotation(CACHE_RESULT)) { + boolean transform = false; + + if (methodInfo.hasAnnotation(CACHE_INVALIDATE) || methodInfo.hasAnnotation(CACHE_INVALIDATE_LIST)) { + transform = true; + cacheInvalidate = true; + } + if (methodInfo.hasAnnotation(CACHE_RESULT)) { + transform = true; + cacheResult = true; + } + if (methodInfo.hasAnnotation(CACHE_INVALIDATE_ALL) || methodInfo.hasAnnotation(CACHE_INVALIDATE_ALL_LIST)) { + cacheInvalidateAll = true; + } + + if (transform) { short[] cacheKeyParameterPositions = getCacheKeyParameterPositions(methodInfo); /* * The bytecode transformation is always performed even if `cacheKeyParameterPositions` is empty because @@ -228,6 +252,18 @@ List enhanceRestClientMethods(CombinedIndexBuildIt } } } + + // Interceptors need to be registered as unremovable due to the rest-client integration - interceptors + // are currently resolved dynamically at runtime because per the spec interceptor bindings cannot be declared on interfaces + if (cacheResult) { + unremovableBeans.produce(UnremovableBeanBuildItem.beanClassNames(CacheResultInterceptor.class.getName())); + } + if (cacheInvalidate) { + unremovableBeans.produce(UnremovableBeanBuildItem.beanClassNames(CacheInvalidateInterceptor.class.getName())); + } + if (cacheInvalidateAll) { + unremovableBeans.produce(UnremovableBeanBuildItem.beanClassNames(CacheInvalidateAllInterceptor.class.getName())); + } return bytecodeTransformers; } diff --git a/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java b/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java index 8d0a67e05c6d1..834ba12f32a26 100644 --- a/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java +++ b/extensions/mailer/deployment/src/main/java/io/quarkus/mailer/deployment/MailerProcessor.java @@ -36,17 +36,14 @@ public class MailerProcessor { private static final DotName MAIL_TEMPLATE = DotName.createSimple(MailTemplate.class.getName()); @BuildStep - void unremoveableBeans(BuildProducer additionalBeans) { - additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(MailClientProducer.class)); - additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(MailerSupportProducer.class)); - } - - @BuildStep - AdditionalBeanBuildItem registerMailers() { - return AdditionalBeanBuildItem.builder() + void registerBeans(BuildProducer beans) { + beans.produce(AdditionalBeanBuildItem.builder().setUnremovable() .addBeanClasses(MutinyMailerImpl.class, BlockingMailerImpl.class, - MockMailboxImpl.class, MailTemplateProducer.class) - .build(); + MailClientProducer.class, MailerSupportProducer.class) + .build()); + beans.produce(AdditionalBeanBuildItem.builder() + .addBeanClasses(MockMailboxImpl.class, MailTemplateProducer.class) + .build()); } @BuildStep diff --git a/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/MailTemplateValidationTest.java b/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/MailTemplateValidationTest.java index 20882c49cc874..79114506155fc 100644 --- a/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/MailTemplateValidationTest.java +++ b/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/MailTemplateValidationTest.java @@ -11,6 +11,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.arc.Unremovable; import io.quarkus.test.QuarkusUnitTest; import io.smallrye.mutiny.Uni; @@ -31,6 +32,7 @@ public void testValidationFailed() { Assertions.fail(); } + @Unremovable // Injection points from removed beans are not validated @Singleton static class MailTemplates { diff --git a/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java b/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java index 89636ed839b3f..3f43375a4a2eb 100644 --- a/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java +++ b/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java @@ -6,6 +6,7 @@ import javax.annotation.Priority; import javax.interceptor.Interceptor; +import javax.transaction.TransactionManager; import javax.transaction.TransactionScoped; import com.arjuna.ats.arjuna.common.ObjectStoreEnvironmentBean; @@ -143,9 +144,11 @@ public CustomScopeBuildItem registerScope() { } @BuildStep - UnremovableBeanBuildItem unremovableBean() { + void unremovableBean(BuildProducer unremovableBeans) { // LifecycleManager comes from smallrye-context-propagation-jta and is only used via programmatic lookup in JtaContextProvider - return UnremovableBeanBuildItem.beanClassNames(JtaContextProvider.LifecycleManager.class.getName()); + unremovableBeans.produce(UnremovableBeanBuildItem.beanClassNames(JtaContextProvider.LifecycleManager.class.getName())); + // The tx manager is obtained via CDI.current().select(TransactionManager.class) in the JtaContextProvider + unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(TransactionManager.class)); } } diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java index 0f8b969f90ccc..97fca2a96cfd1 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java @@ -78,7 +78,7 @@ public class SmallRyeFaultToleranceProcessor { @BuildStep public void build(BuildProducer annotationsTransformer, - BuildProducer feature, BuildProducer additionalBean, + BuildProducer feature, BuildProducer beans, BuildProducer serviceProvider, BuildProducer additionalBda, Optional metricsCapability, @@ -110,7 +110,7 @@ public void build(BuildProducer annotationsTran reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, fallbackHandler)); fallbackHandlersBeans.addBeanClass(fallbackHandler); } - additionalBean.produce(fallbackHandlersBeans.build()); + beans.produce(fallbackHandlersBeans.build()); } // Add reflective access to fallback methods for (AnnotationInstance annotation : index.getAnnotations(DotNames.FALLBACK)) { @@ -182,18 +182,25 @@ public void transform(TransformationContext context) { for (DotName ftAnnotation : DotNames.FT_ANNOTATIONS) { builder.addBeanClass(ftAnnotation.toString()); } - builder.addBeanClasses(FaultToleranceInterceptor.class, - ExecutorHolder.class, - StrategyCache.class, - QuarkusFaultToleranceOperationProvider.class, - QuarkusFallbackHandlerProvider.class, - QuarkusExistingCircuitBreakerNames.class, - QuarkusAsyncExecutorProvider.class, - MetricsProvider.class, - CircuitBreakerMaintenanceImpl.class, - RequestContextIntegration.class, - SpecCompatibility.class); - additionalBean.produce(builder.build()); + builder + .addBeanClasses( + ExecutorHolder.class, + StrategyCache.class, + QuarkusFallbackHandlerProvider.class, + QuarkusAsyncExecutorProvider.class, + MetricsProvider.class, + CircuitBreakerMaintenanceImpl.class, + RequestContextIntegration.class, + SpecCompatibility.class); + beans.produce(builder.build()); + + // TODO FT should be smart enough and only initialize the stuff in the recorder if it's really needed + // The FaultToleranceInterceptor needs to be registered as unremovable due to the rest-client integration - interceptors + // are currently resolved dynamically at runtime because per the spec interceptor bindings cannot be declared on interfaces + beans.produce(AdditionalBeanBuildItem.builder().setUnremovable() + .addBeanClasses(FaultToleranceInterceptor.class, QuarkusFaultToleranceOperationProvider.class, + QuarkusExistingCircuitBreakerNames.class) + .build()); if (!metricsCapability.isPresent()) { //disable fault tolerance metrics with the MP sys props diff --git a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java index 607de666f4a30..52ac9f9278bfc 100644 --- a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java +++ b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java @@ -48,7 +48,6 @@ import io.quarkus.deployment.util.WebJarUtil; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.RuntimeValue; -import io.quarkus.runtime.metrics.MetricsFactory; import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLRecorder; import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLRuntimeConfig; import io.quarkus.vertx.http.deployment.BodyHandlerBuildItem; @@ -373,8 +372,7 @@ private Set getAllReferenceClasses(Reference reference) { void activateMetrics(Capabilities capabilities, Optional metricsCapability, SmallRyeGraphQLConfig graphQLConfig, - BuildProducer systemProperties, - BuildProducer unremovableBeans) { + BuildProducer systemProperties) { boolean activate = shouldActivateService(graphQLConfig.metricsEnabled, metricsCapability.isPresent(), @@ -383,9 +381,6 @@ void activateMetrics(Capabilities capabilities, "quarkus.smallrye-graphql.metrics.enabled", false); if (activate) { - if (metricsCapability.isPresent() && metricsCapability.get().metricsSupported(MetricsFactory.MP_METRICS)) { - unremovableBeans.produce(UnremovableBeanBuildItem.beanClassNames("io.smallrye.metrics.MetricRegistries")); - } systemProperties.produce(new SystemPropertyBuildItem(ConfigKey.ENABLE_METRICS, TRUE)); } else { systemProperties.produce(new SystemPropertyBuildItem(ConfigKey.ENABLE_METRICS, FALSE)); @@ -395,7 +390,8 @@ void activateMetrics(Capabilities capabilities, @BuildStep void activateTracing(Capabilities capabilities, SmallRyeGraphQLConfig graphQLConfig, - BuildProducer systemProperties) { + BuildProducer systemProperties, + BuildProducer unremovableBeans) { boolean activate = shouldActivateService(graphQLConfig.tracingEnabled, capabilities.isPresent(Capability.OPENTRACING), diff --git a/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java b/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java index ab9328ea24bc1..4673ada2d6022 100644 --- a/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java +++ b/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java @@ -175,20 +175,18 @@ void createRoute(BuildProducer routes, } @BuildStep - void beans(BuildProducer additionalBeans, - BuildProducer unremovableBeans) { - additionalBeans.produce(new AdditionalBeanBuildItem(MetricProducer.class, + void registerBeans(BuildProducer beans) { + beans.produce(new AdditionalBeanBuildItem(MetricProducer.class, MetricNameFactory.class, - MetricRegistries.class, GaugeRegistrationInterceptor.class, MeteredInterceptor.class, ConcurrentGaugeInterceptor.class, CountedInterceptor.class, TimedInterceptor.class, - SimplyTimedInterceptor.class, - MetricsRequestHandler.class)); - unremovableBeans.produce(new UnremovableBeanBuildItem( - new UnremovableBeanBuildItem.BeanClassNameExclusion(MetricsRequestHandler.class.getName()))); + SimplyTimedInterceptor.class)); + // MetricsRequestHandler and MetricRegistries are looked up programmatically in the recorder + beans.produce(AdditionalBeanBuildItem.builder().setUnremovable() + .addBeanClasses(MetricsRequestHandler.class, MetricRegistries.class).build()); } @BuildStep diff --git a/extensions/smallrye-opentracing/deployment/src/main/java/io/quarkus/smallrye/opentracing/deployment/SmallRyeOpenTracingProcessor.java b/extensions/smallrye-opentracing/deployment/src/main/java/io/quarkus/smallrye/opentracing/deployment/SmallRyeOpenTracingProcessor.java index 94fdcd11e4cbb..bf1dd1f87ae8f 100644 --- a/extensions/smallrye-opentracing/deployment/src/main/java/io/quarkus/smallrye/opentracing/deployment/SmallRyeOpenTracingProcessor.java +++ b/extensions/smallrye-opentracing/deployment/src/main/java/io/quarkus/smallrye/opentracing/deployment/SmallRyeOpenTracingProcessor.java @@ -5,9 +5,11 @@ import javax.enterprise.inject.spi.ObserverMethod; import javax.servlet.DispatcherType; +import io.opentracing.Tracer; import io.opentracing.contrib.interceptors.OpenTracingInterceptor; import io.opentracing.contrib.jaxrs2.server.SpanFinishingFilter; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.Feature; @@ -28,7 +30,10 @@ public class SmallRyeOpenTracingProcessor { @BuildStep - AdditionalBeanBuildItem registerBeans() { + AdditionalBeanBuildItem registerBeans(BuildProducer unremovableBeans) { + // Some components obtain the tracer via CDI.current().select(Tracer.class) + // E.g. io.quarkus.smallrye.opentracing.runtime.QuarkusSmallRyeTracingDynamicFeature and io.smallrye.graphql.cdi.tracing.TracingService + unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(Tracer.class)); return new AdditionalBeanBuildItem(OpenTracingInterceptor.class, TracerProducer.class); } 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 6f98786227eca..092d0b22a4e5d 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 @@ -25,6 +25,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -245,7 +246,7 @@ BeanRegistrar.RegistrationContext registerBeans(List beanRegistra void init(Consumer bytecodeTransformerConsumer, List> additionalUnusedBeanExclusions) { - long start = System.currentTimeMillis(); + long start = System.nanoTime(); // Collect dependency resolution errors List errors = new ArrayList<>(); @@ -269,85 +270,120 @@ void init(Consumer bytecodeTransformerConsumer, } if (removeUnusedBeans) { - long removalStart = System.currentTimeMillis(); - Set removable = new HashSet<>(); - Set unusedProducers = new HashSet<>(); - Set unusedButDeclaresProducer = new HashSet<>(); - List producers = beans.stream().filter(b -> b.isProducerMethod() || b.isProducerField()) - .collect(Collectors.toList()); - List instanceInjectionPoints = injectionPoints.stream() - .filter(BuiltinBean.INSTANCE::matches) - .collect(Collectors.toList()); - Set injected = injectionPoints.stream().map(InjectionPointInfo::getResolvedBean) - .collect(Collectors.toSet()); - Set declaresProducer = producers.stream().map(BeanInfo::getDeclaringBean).collect(Collectors.toSet()); + long removalStart = System.nanoTime(); Set declaresObserver = observers.stream().map(ObserverInfo::getDeclaringBean).collect(Collectors.toSet()); - test: for (BeanInfo bean : beans) { - // Named beans can be used in templates and expressions - if (bean.getName() != null) { - continue test; - } - // Unremovable synthetic beans - if (!bean.isRemovable()) { - continue test; - } - // Custom exclusions - for (Predicate exclusion : allUnusedExclusions) { - if (exclusion.test(bean)) { - continue test; - } - } - // Is injected - if (injected.contains(bean)) { - continue test; - } - // Declares an observer method - if (declaresObserver.contains(bean)) { - continue test; + Set removedDecorators = new HashSet<>(); + Set removedInterceptors = new HashSet<>(); + removeUnusedComponents(declaresObserver, allUnusedExclusions, removedDecorators, removedInterceptors); + + LOGGER.debugf("Removed %s beans, %s interceptors and %s decorators in %s ms", removedBeans.size(), + removedInterceptors.size(), removedDecorators.size(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - removalStart)); + } + buildContext.putInternal(BuildExtension.Key.REMOVED_BEANS.asString(), Collections.unmodifiableSet(removedBeans)); + LOGGER.debugf("Bean deployment initialized in %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); + } + + private void removeUnusedComponents(Set declaresObserver, + List> allUnusedExclusions, Set removedDecorators, + Set removedInterceptors) { + int removed; + do { + removed = 0; + removed += removeUnusedBeans(declaresObserver, allUnusedExclusions).size(); + removed += removeUnusedInterceptors(removedInterceptors, allUnusedExclusions).size(); + removed += removeUnusedDecorators(removedDecorators, allUnusedExclusions).size(); + } while (removed > 0); + } + + private Set removeUnusedInterceptors(Set removedInterceptors, + List> allUnusedExclusions) { + Set removableInterceptors = new HashSet<>(); + for (InterceptorInfo interceptor : this.interceptors) { + boolean removable = true; + for (Predicate exclusion : allUnusedExclusions) { + if (exclusion.test(interceptor)) { + removable = false; + break; } - // Instance - for (InjectionPointInfo injectionPoint : instanceInjectionPoints) { - if (Beans.hasQualifiers(bean, injectionPoint.getRequiredQualifiers()) && Beans.matchesType(bean, - injectionPoint.getType().asParameterizedType().arguments().get(0))) { - continue test; + } + if (removable) { + for (BeanInfo bean : this.beans) { + if (bean.getBoundInterceptors().contains(interceptor)) { + removable = false; + break; } } - // Declares a producer - see also second pass - if (declaresProducer.contains(bean)) { - unusedButDeclaresProducer.add(bean); - continue test; - } - if (bean.isProducerField() || bean.isProducerMethod()) { - // This bean is very likely an unused producer - unusedProducers.add(bean); + } + if (removable) { + removableInterceptors.add(interceptor); + } + } + if (!removableInterceptors.isEmpty()) { + removedInterceptors.addAll(removableInterceptors); + this.interceptors.removeAll(removableInterceptors); + List removableInjectionPoints = removableInterceptors.stream() + .flatMap(d -> d.getAllInjectionPoints().stream()).collect(Collectors.toList()); + injectionPoints.removeAll(removableInjectionPoints); + if (LOGGER.isDebugEnabled()) { + LOGGER.debugf(removableInterceptors.stream().map(i -> "Removed unused interceptor " + i) + .collect(Collectors.joining("\n"))); + } + } + return removableInterceptors; + } + + private Set removeUnusedDecorators(Set removedDecorators, + List> allUnusedExclusions) { + Set removableDecorators = new HashSet<>(); + for (DecoratorInfo decorator : this.decorators) { + boolean removable = true; + for (Predicate exclusion : allUnusedExclusions) { + if (exclusion.test(decorator)) { + removable = false; + break; } - removable.add(bean); } - if (!unusedProducers.isEmpty()) { - // Second pass to find beans which themselves are unused and declare only unused producers - Map> declaringMap = producers.stream() - .collect(Collectors.groupingBy(BeanInfo::getDeclaringBean)); - for (Entry> entry : declaringMap.entrySet()) { - BeanInfo declaringBean = entry.getKey(); - if (unusedButDeclaresProducer.contains(declaringBean) && unusedProducers.containsAll(entry.getValue())) { - // All producers declared by this bean are unused - removable.add(declaringBean); + if (removable) { + for (BeanInfo bean : this.beans) { + if (bean.getBoundDecorators().contains(decorator)) { + removable = false; + break; } } } - if (!removable.isEmpty()) { - beans.removeAll(removable); - removedBeans.addAll(removable); - if (LOGGER.isDebugEnabled()) { - LOGGER.debugf(removedBeans.stream().map(b -> "Removed unused " + b).collect(Collectors.joining("\n"))); - } + if (removable) { + removableDecorators.add(decorator); } - LOGGER.debugf("Removed %s unused beans in %s ms", removable.size(), System.currentTimeMillis() - removalStart); } + if (!removableDecorators.isEmpty()) { + removedDecorators.addAll(removableDecorators); + this.decorators.removeAll(removableDecorators); + List removableInjectionPoints = removableDecorators.stream() + .flatMap(d -> d.getAllInjectionPoints().stream()).collect(Collectors.toList()); + injectionPoints.removeAll(removableInjectionPoints); + if (LOGGER.isDebugEnabled()) { + LOGGER.debugf(removableDecorators.stream().map(i -> "Removed unused decorator " + i) + .collect(Collectors.joining("\n"))); + } + } + return removableDecorators; + } - buildContext.putInternal(BuildExtension.Key.REMOVED_BEANS.asString(), Collections.unmodifiableSet(removedBeans)); - - LOGGER.debugf("Bean deployment initialized in %s ms", System.currentTimeMillis() - start); + private Set removeUnusedBeans(Set declaresObserver, List> allUnusedExclusions) { + Set removableBeans = UnusedBeans.findRemovableBeans(this.beans, this.injectionPoints, declaresObserver, + allUnusedExclusions); + if (!removableBeans.isEmpty()) { + this.beans.removeAll(removableBeans); + this.removedBeans.addAll(removableBeans); + List removableInjectionPoints = removableBeans.stream() + .flatMap(d -> d.getAllInjectionPoints().stream()).collect(Collectors.toList()); + injectionPoints.removeAll(removableInjectionPoints); + if (LOGGER.isDebugEnabled()) { + LOGGER.debugf(removedBeans.stream().map(b -> "Removed unused " + b).collect(Collectors.joining("\n"))); + } + } + return removableBeans; } ValidationContext validate(List validators, 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 dde1653ea735e..d2ce5ac749774 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 @@ -12,7 +12,6 @@ import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -535,7 +534,7 @@ private Map initDecoratedMethods() { return Collections.emptyMap(); } // A decorator is bound to a bean if the bean is assignable to the delegate injection point - List bound = new LinkedList<>(); + List bound = new ArrayList<>(); for (DecoratorInfo decorator : decorators) { if (Beans.matches(this, decorator.getDelegateInjectionPoint().getTypeAndQualifiers())) { bound.add(decorator); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InjectionPointInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InjectionPointInfo.java index 71fed5c07feab..fb8193ddc7779 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InjectionPointInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InjectionPointInfo.java @@ -209,6 +209,10 @@ public boolean isDelegate() { return isDelegate; } + public boolean hasResolvedBean() { + return resolvedBean.get() != null; + } + /** * @return the parameter position or {@code -1} for a field injection point */ diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/UnusedBeans.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/UnusedBeans.java new file mode 100644 index 0000000000000..b7e7910af39c9 --- /dev/null +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/UnusedBeans.java @@ -0,0 +1,94 @@ +package io.quarkus.arc.processor; + +import static java.util.function.Predicate.not; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +final class UnusedBeans { + + private UnusedBeans() { + } + + static Set findRemovableBeans(Collection beans, Collection injectionPoints, + Set declaresObserver, List> allUnusedExclusions) { + Set removableBeans = new HashSet<>(); + + Set unusedProducers = new HashSet<>(); + Set unusedButDeclaresProducer = new HashSet<>(); + List producers = beans.stream().filter(b -> b.isProducerMethod() || b.isProducerField()) + .collect(Collectors.toList()); + List instanceInjectionPoints = injectionPoints.stream() + .filter(InjectionPointInfo::isProgrammaticLookup) + .collect(Collectors.toList()); + // Collect all injected beans; skip delegate injection points and injection points that resolve to a built-in bean + Set injected = injectionPoints.stream() + .filter(not(InjectionPointInfo::isDelegate).and(InjectionPointInfo::hasResolvedBean)) + .map(InjectionPointInfo::getResolvedBean) + .collect(Collectors.toSet()); + Set declaresProducer = producers.stream().map(BeanInfo::getDeclaringBean).collect(Collectors.toSet()); + + // Beans - first pass to find unused beans that do not declare a producer + test: for (BeanInfo bean : beans) { + // Named beans can be used in templates and expressions + if (bean.getName() != null) { + continue test; + } + // Unremovable synthetic beans + if (!bean.isRemovable()) { + continue test; + } + // Custom exclusions + for (Predicate exclusion : allUnusedExclusions) { + if (exclusion.test(bean)) { + continue test; + } + } + // Is injected + if (injected.contains(bean)) { + continue test; + } + // Declares an observer method + if (declaresObserver.contains(bean)) { + continue test; + } + // Instance + for (InjectionPointInfo injectionPoint : instanceInjectionPoints) { + if (Beans.hasQualifiers(bean, injectionPoint.getRequiredQualifiers()) && Beans.matchesType(bean, + injectionPoint.getType().asParameterizedType().arguments().get(0))) { + continue test; + } + } + // Declares a producer - see also second pass + if (declaresProducer.contains(bean)) { + unusedButDeclaresProducer.add(bean); + continue test; + } + if (bean.isProducerField() || bean.isProducerMethod()) { + // This bean is very likely an unused producer + unusedProducers.add(bean); + } + removableBeans.add(bean); + } + if (!unusedProducers.isEmpty()) { + // Beans - second pass to find beans which themselves are unused and declare only unused producers + Map> declaringMap = producers.stream() + .collect(Collectors.groupingBy(BeanInfo::getDeclaringBean)); + for (Entry> entry : declaringMap.entrySet()) { + BeanInfo declaringBean = entry.getKey(); + if (unusedButDeclaresProducer.contains(declaringBean) && unusedProducers.containsAll(entry.getValue())) { + // All producers declared by this bean are unused + removableBeans.add(declaringBean); + } + } + } + return removableBeans; + } + +} diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java index 919a98cab9252..11b52ed2909a6 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java @@ -8,6 +8,7 @@ import io.quarkus.arc.ComponentsProvider; import io.quarkus.arc.InjectableBean; import io.quarkus.arc.InjectableContext; +import io.quarkus.arc.InjectableDecorator; import io.quarkus.arc.InjectableInstance; import io.quarkus.arc.InjectableInterceptor; import io.quarkus.arc.InjectableObserverMethod; @@ -51,6 +52,7 @@ import javax.enterprise.inject.spi.Bean; import javax.enterprise.inject.spi.BeanManager; import javax.enterprise.inject.spi.CDI; +import javax.enterprise.inject.spi.Decorator; import javax.enterprise.inject.spi.InjectionPoint; import javax.enterprise.inject.spi.InterceptionType; import javax.enterprise.inject.spi.Interceptor; @@ -71,6 +73,7 @@ public class ArcContainerImpl implements ArcContainer { private final ArrayList> beans; private final ArrayList removedBeans; private final List> interceptors; + private final List> decorators; private final List> observers; private final Map, Set> transitiveInterceptorBindings; private final Map> qualifierNonbindingMembers; @@ -96,6 +99,7 @@ public ArcContainerImpl() { beans = new ArrayList<>(); removedBeans = new ArrayList<>(); interceptors = new ArrayList<>(); + decorators = new ArrayList<>(); observers = new ArrayList<>(); transitiveInterceptorBindings = new HashMap<>(); qualifierNonbindingMembers = new HashMap<>(); @@ -113,6 +117,8 @@ public ArcContainerImpl() { for (InjectableBean bean : components.getBeans()) { if (bean instanceof InjectableInterceptor) { interceptors.add((InjectableInterceptor) bean); + } else if (bean instanceof InjectableDecorator) { + decorators.add((InjectableDecorator) bean); } else { beans.add(bean); } @@ -626,10 +632,20 @@ List> getMatchingBeans(Resolvable resolvable) { + "\t- Application developers can eliminate false positives via the @Unremovable annotation\n" + "\t- Extensions can eliminate false positives via build items, e.g. using the UnremovableBeanBuildItem\n" + "\t- See also https://quarkus.io/guides/cdi-reference#remove_unused_beans\n" + + "\t- Enable the DEBUG log level to see the full stack trace to identify the method that performed the lookup\n" + "%1$s%1$s%1$s%1$s\n"; LOGGER.warnf(msg, separator, removedMatching.stream().map(Object::toString).collect(Collectors.joining("\n\t- ")), resolvable.requiredType, Arrays.toString(resolvable.qualifiers)); + if (LOGGER.isDebugEnabled()) { + StringBuilder stack = new StringBuilder("\nCDI: programmatic lookup stack trace:\n"); + for (StackTraceElement e : Thread.currentThread().getStackTrace()) { + stack.append("\t"); + stack.append(e.toString()); + stack.append("\n"); + } + LOGGER.debug(stack); + } } } return matching; @@ -703,6 +719,23 @@ List> resolveInterceptors(InterceptionType type, Annotation... in return interceptors; } + List> resolveDecorators(Set types, Annotation... qualifiers) { + if (decorators.isEmpty()) { + return Collections.emptyList(); + } + if (Objects.requireNonNull(types).isEmpty()) { + throw new IllegalArgumentException("The set of bean types must not be empty"); + } + List> decorators = new ArrayList<>(); + for (InjectableDecorator decorator : this.decorators) { + if (matches(types, Set.of(qualifiers), decorator.getDelegateType(), + decorator.getDelegateQualifiers().toArray(new Annotation[] {}))) { + decorators.add(decorator); + } + } + return decorators; + } + private boolean hasAllInterceptionBindings(InjectableInterceptor interceptor, Iterable bindings) { // The method or constructor has all the interceptor bindings of the interceptor for (Annotation binding : interceptor.getInterceptorBindings()) { diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/BeanManagerImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/BeanManagerImpl.java index bcf8dbd020443..beaa0d46e8df8 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/BeanManagerImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/BeanManagerImpl.java @@ -134,7 +134,7 @@ public Set> resolveObserverMethods(T event, Annota @Override public List> resolveDecorators(Set types, Annotation... qualifiers) { - throw new UnsupportedOperationException(); + return ArcContainerImpl.instance().resolveDecorators(types, qualifiers); } @Override diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/RemovedBeanImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/RemovedBeanImpl.java index 6917d2a00256b..6d24ee55856cf 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/RemovedBeanImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/RemovedBeanImpl.java @@ -27,7 +27,7 @@ public Kind getKind() { @Override public String getDescription() { - return description; + return description != null ? description : ""; } @Override @@ -43,7 +43,7 @@ public Set getQualifiers() { @Override public String toString() { StringBuilder builder = new StringBuilder(); - builder.append(kind).append(" bean ").append(description).append(" [types=") + builder.append(getKind()).append(" bean ").append(getDescription()).append(" [types=") .append(types).append(", qualifiers=").append(qualifiers).append("]"); return builder.toString(); } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/Alpha.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/Alpha.java new file mode 100644 index 0000000000000..40b27f2570366 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/Alpha.java @@ -0,0 +1,18 @@ +package io.quarkus.arc.test.unused; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import javax.interceptor.InterceptorBinding; + +@Target({ TYPE, METHOD }) +@Retention(RUNTIME) +@Documented +@InterceptorBinding +public @interface Alpha { + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/AlphaInterceptor.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/AlphaInterceptor.java new file mode 100644 index 0000000000000..45356b22e6375 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/AlphaInterceptor.java @@ -0,0 +1,21 @@ +package io.quarkus.arc.test.unused; + +import javax.annotation.Priority; +import javax.inject.Inject; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; + +@Alpha +@Priority(1) +@Interceptor +public class AlphaInterceptor { + + @Inject + Counter counter; + + @AroundInvoke + Object mySuperCoolAroundInvoke(InvocationContext ctx) throws Exception { + return "" + counter.get() + ctx.proceed() + counter.incrementAndGet(); + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/Bravo.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/Bravo.java new file mode 100644 index 0000000000000..00f2ff4514088 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/Bravo.java @@ -0,0 +1,18 @@ +package io.quarkus.arc.test.unused; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import javax.interceptor.InterceptorBinding; + +@Target({ TYPE, METHOD }) +@Retention(RUNTIME) +@Documented +@InterceptorBinding +public @interface Bravo { + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/BravoInterceptor.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/BravoInterceptor.java new file mode 100644 index 0000000000000..f1fa2e03e5393 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/BravoInterceptor.java @@ -0,0 +1,17 @@ +package io.quarkus.arc.test.unused; + +import javax.annotation.Priority; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; + +@Alpha +@Priority(1) +@Interceptor +public class BravoInterceptor { + + @AroundInvoke + Object mySuperCoolAroundInvoke(InvocationContext ctx) throws Exception { + return ctx.proceed(); + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/Counter.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/Counter.java new file mode 100644 index 0000000000000..d719794680733 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/Counter.java @@ -0,0 +1,24 @@ +package io.quarkus.arc.test.unused; + +import java.util.concurrent.atomic.AtomicInteger; +import javax.inject.Singleton; + +@Bravo // Intercepted by Bravo, injected in Alpha! +@Singleton +public class Counter { + + private AtomicInteger counter = new AtomicInteger(); + + public int incrementAndGet() { + return counter.incrementAndGet(); + } + + public void reset() { + counter.set(0); + } + + public int get() { + return counter.get(); + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/RemoveUnusedBeansTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/RemoveUnusedBeansTest.java index 3d021bda5413e..3890ce4777fef 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/RemoveUnusedBeansTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/RemoveUnusedBeansTest.java @@ -24,7 +24,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -public class RemoveUnusedBeansTest { +public class RemoveUnusedBeansTest extends RemoveUnusedComponentsTest { @RegisterExtension public ArcTestContainer container = ArcTestContainer.builder() @@ -39,17 +39,17 @@ public class RemoveUnusedBeansTest { @Test public void testRemoval() { + assertPresent(HasObserver.class); + assertPresent(HasName.class); + assertPresent(InjectedViaInstance.class); + assertPresent(InjectedViaInstanceWithWildcard.class); + assertPresent(InjectedViaInstanceWithWildcard2.class); + assertPresent(InjectedViaProvider.class); + assertPresent(String.class); + assertPresent(UsedProducers.class); + assertNotPresent(UnusedProducers.class); + assertNotPresent(BigDecimal.class); ArcContainer container = Arc.container(); - assertTrue(container.instance(HasObserver.class).isAvailable()); - assertTrue(container.instance(HasName.class).isAvailable()); - assertTrue(container.instance(InjectedViaInstance.class).isAvailable()); - assertTrue(container.instance(InjectedViaInstanceWithWildcard.class).isAvailable()); - assertTrue(container.instance(InjectedViaInstanceWithWildcard2.class).isAvailable()); - assertTrue(container.instance(InjectedViaProvider.class).isAvailable()); - assertTrue(container.instance(String.class).isAvailable()); - assertTrue(container.instance(UsedProducers.class).isAvailable()); - assertFalse(container.instance(UnusedProducers.class).isAvailable()); - assertFalse(container.instance(BigDecimal.class).isAvailable()); // Foo is injected in HasObserver#observe() Foo foo = container.instance(Foo.class).get(); assertEquals(FooAlternative.class.getName(), foo.ping()); @@ -57,12 +57,14 @@ public void testRemoval() { assertEquals(1, container.beanManager().getBeans(Foo.class).size()); assertEquals("pong", container.instance(Excluded.class).get().ping()); // Producer is unused but declaring bean is injected - assertTrue(container.instance(UnusedProducerButInjected.class).isAvailable()); - assertFalse(container.instance(BigInteger.class).isAvailable()); + assertPresent(UnusedProducerButInjected.class); + assertNotPresent(BigInteger.class); // Producer is unused, declaring bean is only used via Instance - assertTrue(container.instance(UsedViaInstanceWithUnusedProducer.class).isAvailable()); - assertFalse(container.instance(Long.class).isAvailable()); + assertPresent(UsedViaInstanceWithUnusedProducer.class); + assertNotPresent(Long.class); assertFalse(ArcContainerImpl.instance().getRemovedBeans().isEmpty()); + assertNotPresent(UnusedBean.class); + assertNotPresent(OnlyInjectedInUnusedBean.class); } @Dependent @@ -195,6 +197,7 @@ static class UsedViaInstanceWithUnusedProducer { Long unusedLong = Long.valueOf(0); } + @Named // just to make it unremovable @Singleton static class UsesBeanViaInstance { @@ -202,4 +205,17 @@ static class UsesBeanViaInstance { Instance instance; } + @Singleton + static class UnusedBean { + + @Inject + OnlyInjectedInUnusedBean beanB; + + } + + @Singleton + static class OnlyInjectedInUnusedBean { + + } + } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/RemoveUnusedComponentsTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/RemoveUnusedComponentsTest.java new file mode 100644 index 0000000000000..4e1be1a76f062 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/RemoveUnusedComponentsTest.java @@ -0,0 +1,17 @@ +package io.quarkus.arc.test.unused; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.quarkus.arc.Arc; + +public abstract class RemoveUnusedComponentsTest { + + protected void assertPresent(Class beanClass) { + assertTrue(Arc.container().instance(beanClass).isAvailable()); + } + + protected void assertNotPresent(Class beanClass) { + assertEquals(0, Arc.container().beanManager().getBeans(beanClass).size()); + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/RemoveUnusedDecoratorTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/RemoveUnusedDecoratorTest.java new file mode 100644 index 0000000000000..11ab44ce4fed7 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/RemoveUnusedDecoratorTest.java @@ -0,0 +1,64 @@ +package io.quarkus.arc.test.unused; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.test.ArcTestContainer; +import java.util.Set; +import javax.annotation.Priority; +import javax.decorator.Decorator; +import javax.decorator.Delegate; +import javax.enterprise.context.Dependent; +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class RemoveUnusedDecoratorTest extends RemoveUnusedComponentsTest { + + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(HasObserver.class, Converter.class, TrimConverterDecorator.class) + .removeUnusedBeans(true) + .build(); + + @Test + public void testRemoval() { + ArcContainer container = Arc.container(); + assertPresent(HasObserver.class); + assertNotPresent(Converter.class); + assertTrue(container.beanManager().resolveDecorators(Set.of(Converter.class)).isEmpty()); + } + + @Dependent + static class HasObserver { + + void observe(@Observes String event) { + } + + } + + interface Converter { + + T convert(T value); + + } + + @Dependent + @Priority(1) + @Decorator + static class TrimConverterDecorator implements Converter { + + @Inject + @Delegate + Converter delegate; + + @Override + public String convert(String value) { + return delegate.convert(value.trim()); + } + + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/RemoveUnusedInterceptorTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/RemoveUnusedInterceptorTest.java new file mode 100644 index 0000000000000..18fa645b9a0bf --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/unused/RemoveUnusedInterceptorTest.java @@ -0,0 +1,46 @@ +package io.quarkus.arc.test.unused; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.test.ArcTestContainer; +import javax.enterprise.context.Dependent; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.spi.InterceptionType; +import javax.enterprise.util.AnnotationLiteral; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class RemoveUnusedInterceptorTest extends RemoveUnusedComponentsTest { + + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(HasObserver.class, Alpha.class, AlphaInterceptor.class, Counter.class, Bravo.class, + BravoInterceptor.class) + .removeUnusedBeans(true) + .build(); + + @SuppressWarnings("serial") + @Test + public void testRemoval() { + ArcContainer container = Arc.container(); + assertPresent(HasObserver.class); + assertNotPresent(Counter.class); + // Both AlphaInterceptor and BravoInterceptor were removed + assertTrue(container.beanManager().resolveInterceptors(InterceptionType.AROUND_INVOKE, new AnnotationLiteral() { + }).isEmpty()); + assertTrue(container.beanManager().resolveInterceptors(InterceptionType.AROUND_INVOKE, new AnnotationLiteral() { + }).isEmpty()); + + } + + @Dependent + static class HasObserver { + + void observe(@Observes String event) { + } + + } + +}