From 9bd9d420dd4479f7d88eaa6d2bddeef8ad3ddaaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hermann?= Date: Mon, 14 Oct 2024 11:23:51 -0400 Subject: [PATCH] support @Decorated bean injection in decorator --- docs/src/main/asciidoc/cdi.adoc | 14 +++- .../quarkus/arc/processor/BeanArchives.java | 2 + .../io/quarkus/arc/processor/BuiltinBean.java | 26 +++++++ .../io/quarkus/arc/processor/DotNames.java | 2 + .../io/quarkus/arc/processor/Injection.java | 9 +++ .../impl/DecoratedBeanMetadataProvider.java | 28 +++++++ .../decorators/decorated/DecoratedTest.java | 75 +++++++++++++++++++ .../decorated/InvalidDecoratedTest.java | 40 ++++++++++ 8 files changed, 192 insertions(+), 4 deletions(-) create mode 100644 independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/DecoratedBeanMetadataProvider.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/DecoratedTest.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/InvalidDecoratedTest.java diff --git a/docs/src/main/asciidoc/cdi.adoc b/docs/src/main/asciidoc/cdi.adoc index 2dfba1a962509..a33aa6b3d10e9 100644 --- a/docs/src/main/asciidoc/cdi.adoc +++ b/docs/src/main/asciidoc/cdi.adoc @@ -455,10 +455,15 @@ public class LargeTxAccount implements Account { <3> Account delegate; <4> @Inject - LogService logService; <5> + @Decorated + Bean delegateInfo; <5> + + + @Inject + LogService logService; <6> void withdraw(BigDecimal amount) { - delegate.withdraw(amount); <6> + delegate.withdraw(amount); <7> if (amount.compareTo(1000) > 0) { logService.logWithdrawal(delegate, amount); } @@ -470,8 +475,9 @@ public class LargeTxAccount implements Account { <3> <2> `@Decorator` marks a decorator component. <3> The set of decorated types includes all bean types which are Java interfaces, except for `java.io.Serializable`. <4> Each decorator must declare exactly one _delegate injection point_. The decorator applies to beans that are assignable to this delegate injection point. -<5> Decorators can inject other beans. -<6> The decorator may invoke any method of the delegate object. And the container invokes either the next decorator in the chain or the business method of the intercepted instance. +<5> It is possible to get contextual info about the decorated bean by using the @Decorated qualifier. +<6> Decorators can inject other beans. +<7> The decorator may invoke any method of the delegate object. And the container invokes either the next decorator in the chain or the business method of the intercepted instance. NOTE: Instances of decorators are dependent objects of the bean instance they intercept, i.e. a new decorator instance is created for each intercepted bean. diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanArchives.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanArchives.java index 12c9f10614ccd..53c6548e045c2 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanArchives.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanArchives.java @@ -21,6 +21,7 @@ import jakarta.enterprise.context.Initialized; import jakarta.enterprise.context.control.ActivateRequestContext; import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Decorated; import jakarta.enterprise.inject.Default; import jakarta.enterprise.inject.Intercepted; import jakarta.enterprise.inject.Model; @@ -82,6 +83,7 @@ private static IndexView buildAdditionalIndex() { index(indexer, BeforeDestroyed.class.getName()); index(indexer, Destroyed.class.getName()); index(indexer, Intercepted.class.getName()); + index(indexer, Decorated.class.getName()); index(indexer, Model.class.getName()); index(indexer, Lock.class.getName()); index(indexer, All.class.getName()); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinBean.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinBean.java index 8cc9155f269f7..07385dc3833d3 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinBean.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BuiltinBean.java @@ -23,6 +23,7 @@ import io.quarkus.arc.InjectableBean; import io.quarkus.arc.impl.BeanManagerProvider; import io.quarkus.arc.impl.BeanMetadataProvider; +import io.quarkus.arc.impl.DecoratedBeanMetadataProvider; import io.quarkus.arc.impl.EventProvider; import io.quarkus.arc.impl.InjectionPointProvider; import io.quarkus.arc.impl.InstanceProvider; @@ -57,6 +58,11 @@ public enum BuiltinBean { && ip.getRequiredQualifiers().size() == 1 && ip.getRequiredQualifiers().iterator().next().name().equals(DotNames.INTERCEPTED), BuiltinBean::validateInterceptedBean, DotNames.BEAN), + DECORATED_BEAN(BuiltinBean::generateDecoratedBeanBytecode, + (ip, names) -> cdiAndRawTypeMatches(ip, DotNames.BEAN, DotNames.INJECTABLE_BEAN) && !ip.hasDefaultedQualifier() + && ip.getRequiredQualifiers().size() == 1 + && ip.getRequiredQualifiers().iterator().next().name().equals(DotNames.DECORATED), + BuiltinBean::validateDecoratedBean, DotNames.BEAN), BEAN_MANAGER(BuiltinBean::generateBeanManagerBytecode, DotNames.BEAN_MANAGER, DotNames.BEAN_CONTAINER), EVENT(BuiltinBean::generateEventBytecode, DotNames.EVENT), RESOURCE(BuiltinBean::generateResourceBytecode, (ip, names) -> ip.getKind() == InjectionPointKind.RESOURCE, @@ -321,6 +327,19 @@ private static void generateInterceptedBeanBytecode(GeneratorContext ctx) { interceptedBeanMetadataProviderSupplier); } + private static void generateDecoratedBeanBytecode(GeneratorContext ctx) { + ResultHandle decoratedBeanMetadataProvider = ctx.constructor + .newInstance(MethodDescriptor.ofConstructor(DecoratedBeanMetadataProvider.class)); + + ResultHandle decoratedBeanMetadataProviderSupplier = ctx.constructor.newInstance( + MethodDescriptors.FIXED_VALUE_SUPPLIER_CONSTRUCTOR, decoratedBeanMetadataProvider); + ctx.constructor.writeInstanceField( + FieldDescriptor.of(ctx.clazzCreator.getClassName(), ctx.providerName, + Supplier.class.getName()), + ctx.constructor.getThis(), + decoratedBeanMetadataProviderSupplier); + } + private static void generateBeanManagerBytecode(GeneratorContext ctx) { ResultHandle beanManagerProvider = ctx.constructor.newInstance( MethodDescriptor.ofConstructor(BeanManagerProvider.class)); @@ -515,6 +534,13 @@ private static void validateInterceptedBean(ValidatorContext ctx) { } } + private static void validateDecoratedBean(ValidatorContext ctx) { + if (ctx.injectionTarget.kind() != InjectionTargetInfo.TargetKind.BEAN + || !ctx.injectionTarget.asBean().isDecorator()) { + ctx.errors.accept(new DefinitionException("Only decorators can access intercepted bean metadata")); + } + } + private static void validateEventMetadata(ValidatorContext ctx) { if (ctx.injectionTarget.kind() != TargetKind.OBSERVER) { ctx.errors.accept(new DefinitionException( diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DotNames.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DotNames.java index bdfdd2a990e83..fec0bbc8130d6 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DotNames.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DotNames.java @@ -25,6 +25,7 @@ import jakarta.enterprise.event.TransactionPhase; import jakarta.enterprise.inject.Alternative; import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Decorated; import jakarta.enterprise.inject.Default; import jakarta.enterprise.inject.Disposes; import jakarta.enterprise.inject.Instance; @@ -133,6 +134,7 @@ public final class DotNames { public static final DotName INVOCATION_CONTEXT = create(InvocationContext.class); public static final DotName ARC_INVOCATION_CONTEXT = create(ArcInvocationContext.class); public static final DotName DECORATOR = create(Decorator.class); + public static final DotName DECORATED = create(Decorated.class); public static final DotName DELEGATE = create(Delegate.class); public static final DotName SERIALIZABLE = create(Serializable.class); public static final DotName UNREMOVABLE = create(Unremovable.class); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Injection.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Injection.java index 76daa01ec7985..a7ceea3e1b96b 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Injection.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Injection.java @@ -82,6 +82,15 @@ private static void validateInjections(InjectionPointInfo injectionPointInfo, Be "but was detected in: " + injectionPointInfo.getTargetInfo()); } + // If a Bean instance with qualifier @Decorated is injected into a bean instance other than an decorator + // instance, the container automatically detects the problem and treats it as a definition error. + if (injectionPointInfo.getType().name().equals(DotNames.BEAN) + && injectionPointInfo.getRequiredQualifier(DotNames.DECORATED) != null) { + throw new DefinitionException( + "Invalid injection of @Decorated Bean, can only be injected into decorators " + + "but was detected in: " + injectionPointInfo.getTargetInfo()); + } + // the injection point is a field, an initializer method parameter or a bean constructor, with qualifier // @Default, then the type parameter of the injected Bean, or Interceptor must be the same as the type // declaring the injection point diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/DecoratedBeanMetadataProvider.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/DecoratedBeanMetadataProvider.java new file mode 100644 index 0000000000000..dcdc8598138c9 --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/DecoratedBeanMetadataProvider.java @@ -0,0 +1,28 @@ +package io.quarkus.arc.impl; + +import static io.quarkus.arc.impl.CreationalContextImpl.unwrap; + +import jakarta.enterprise.context.spi.Contextual; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.Decorated; +import jakarta.enterprise.inject.spi.Bean; + +import io.quarkus.arc.InjectableReferenceProvider; + +/** + * {@link Decorated} {@link Bean} metadata provider. + */ +public class DecoratedBeanMetadataProvider implements InjectableReferenceProvider> { + + @Override + public Contextual get(CreationalContext> creationalContext) { + // First attempt to obtain the creational context of the decorator bean and then the creational context of the decorated bean + CreationalContextImpl parent = unwrap(creationalContext).getParent(); + if (parent != null) { + parent = parent.getParent(); + return parent != null ? parent.getContextual() : null; + } + return null; + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/DecoratedTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/DecoratedTest.java new file mode 100644 index 0000000000000..9e4a9050d7cc2 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/DecoratedTest.java @@ -0,0 +1,75 @@ +package io.quarkus.arc.test.decorators.decorated; + +import java.util.Comparator; + +import jakarta.annotation.Priority; +import jakarta.decorator.Decorator; +import jakarta.decorator.Delegate; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Decorated; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.inject.Inject; + +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.test.ArcTestContainer; +import io.quarkus.arc.test.MyQualifier; + +public class DecoratedTest { + + @RegisterExtension + public ArcTestContainer container = new ArcTestContainer(Converter.class, DecoratedBean.class, + TrimConverterDecorator.class, MyQualifier.class); + + interface Converter { + + T convert(T value); + + } + + @Test + public void testDecoration() { + DecoratedBean bean = Arc.container().instance(DecoratedBean.class, new MyQualifier.Literal()).get(); + Assertions.assertEquals( + DecoratedBean.class.getName() + " [@io.quarkus.arc.test.MyQualifier(), @jakarta.enterprise.inject.Any()]", + bean.convert("any value")); + } + + @ApplicationScoped + @MyQualifier + static class DecoratedBean implements Converter { + + @Override + public String convert(String value) { + return "Replaced by the decorator"; + } + + } + + @Dependent + @Priority(1) + @Decorator + static class TrimConverterDecorator implements Converter { + + @Inject + @Any + @Delegate + Converter delegate; + + @Inject + @Decorated + Bean decorated; + + @Override + public String convert(String value) { + return decorated.getBeanClass().getName() + " " + decorated.getQualifiers().stream() + .sorted(Comparator.comparing(a -> a.annotationType().getName())).toList(); + } + + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/InvalidDecoratedTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/InvalidDecoratedTest.java new file mode 100644 index 0000000000000..a67c4ca37b065 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/InvalidDecoratedTest.java @@ -0,0 +1,40 @@ +package io.quarkus.arc.test.decorators.decorated; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Decorated; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.test.ArcTestContainer; + +public class InvalidDecoratedTest { + + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(InvalidBean.class) + .shouldFail() + .build(); + + @Test + public void testDecoration() { + assertNotNull(container.getFailure()); + assertTrue(container.getFailure().getMessage().startsWith( + "Invalid injection of @Decorated Bean, can only be injected into decorators but was detected in: " + + InvalidBean.class.getName() + "#decorated")); + } + + @ApplicationScoped + static class InvalidBean { + + @Inject + @Decorated + Bean decorated; + + } +}