From 36c636c43d48ac4d15fac8f5ae3c10e572484332 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/BeanInfo.java | 1 - .../java/io/quarkus/arc/processor/Beans.java | 20 +++++ .../io/quarkus/arc/processor/BuiltinBean.java | 20 ++++- .../io/quarkus/arc/processor/DotNames.java | 2 + .../io/quarkus/arc/processor/Injection.java | 46 ++++++++++++ ...rceptedDecoratedBeanMetadataProvider.java} | 5 +- ...coratedBeanInjectedInNonDecoratorTest.java | 40 ++++++++++ ...eanInjectedWithWrongTypeParameterTest.java | 69 +++++++++++++++++ .../decorators/decorated/DecoratedTest.java | 75 +++++++++++++++++++ 11 files changed, 283 insertions(+), 11 deletions(-) rename independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/{InterceptedBeanMetadataProvider.java => InterceptedDecoratedBeanMetadataProvider.java} (78%) create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/DecoratedBeanInjectedInNonDecoratorTest.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/DecoratedBeanInjectedWithWrongTypeParameterTest.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/DecoratedTest.java diff --git a/docs/src/main/asciidoc/cdi.adoc b/docs/src/main/asciidoc/cdi.adoc index 2dfba1a962509..6c06a35c5e2d5 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 obtain information 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/BeanInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java index 5f31655c45579..72764ffe513f8 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 @@ -680,7 +680,6 @@ void validate(List errors, Consumer bytecodeTran } void validateInterceptorDecorator(List errors, Consumer bytecodeTransformerConsumer) { - // no actual validations done at the moment, but we still want the transformation Beans.validateInterceptorDecorator(this, errors, bytecodeTransformerConsumer); } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java index 22b11952cbf7e..2012e58d58724 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java @@ -32,6 +32,7 @@ import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.MethodParameterInfo; +import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; import org.jboss.jandex.Type.Kind; import org.jboss.logging.Logger; @@ -816,6 +817,25 @@ static void validateInterceptorDecorator(BeanInfo bean, List errors, } } } + + if (bean.isDecorator()) { + DecoratorInfo decorator = (DecoratorInfo) bean; + for (InjectionPointInfo injectionPointInfo : bean.getAllInjectionPoints()) { + // the injection point is a field, an initializer method parameter or a bean constructor of a decorator, + // with qualifier @Decorated, then the type parameter of the injected Bean must be the same as the delegate type + if (injectionPointInfo.getRequiredType().name().equals(DotNames.BEAN) + && injectionPointInfo.getRequiredQualifier(DotNames.DECORATED) != null + && injectionPointInfo.getRequiredType().kind() == Type.Kind.PARAMETERIZED_TYPE) { + ParameterizedType parameterizedType = injectionPointInfo.getRequiredType().asParameterizedType(); + if (parameterizedType.arguments().size() != 1 + || !parameterizedType.arguments().get(0).equals(decorator.getDelegateType())) { + throw new DefinitionException( + "Injected @Decorated Bean<> has to use the delegate type as its type parameter. " + + "Problematic injection point: " + injectionPointInfo.getTargetInfo()); + } + } + } + } } static void validateBean(BeanInfo bean, List errors, Consumer bytecodeTransformerConsumer, 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..fe06e3dc1d6ba 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 @@ -26,7 +26,7 @@ import io.quarkus.arc.impl.EventProvider; import io.quarkus.arc.impl.InjectionPointProvider; import io.quarkus.arc.impl.InstanceProvider; -import io.quarkus.arc.impl.InterceptedBeanMetadataProvider; +import io.quarkus.arc.impl.InterceptedDecoratedBeanMetadataProvider; import io.quarkus.arc.impl.ListProvider; import io.quarkus.arc.impl.ResourceProvider; import io.quarkus.arc.processor.InjectionPointInfo.InjectionPointKind; @@ -52,11 +52,16 @@ public enum BuiltinBean { BEAN(BuiltinBean::generateBeanBytecode, (ip, names) -> cdiAndRawTypeMatches(ip, DotNames.BEAN, DotNames.INJECTABLE_BEAN) && ip.hasDefaultedQualifier(), BuiltinBean::validateBean, DotNames.BEAN), - INTERCEPTED_BEAN(BuiltinBean::generateInterceptedBeanBytecode, + INTERCEPTED_BEAN(BuiltinBean::generateInterceptedDecoratedBeanBytecode, (ip, names) -> cdiAndRawTypeMatches(ip, DotNames.BEAN, DotNames.INJECTABLE_BEAN) && !ip.hasDefaultedQualifier() && ip.getRequiredQualifiers().size() == 1 && ip.getRequiredQualifiers().iterator().next().name().equals(DotNames.INTERCEPTED), BuiltinBean::validateInterceptedBean, DotNames.BEAN), + DECORATED_BEAN(BuiltinBean::generateInterceptedDecoratedBeanBytecode, + (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, @@ -308,9 +313,9 @@ private static void generateBeanBytecode(GeneratorContext ctx) { beanProviderSupplier); } - private static void generateInterceptedBeanBytecode(GeneratorContext ctx) { + private static void generateInterceptedDecoratedBeanBytecode(GeneratorContext ctx) { ResultHandle interceptedBeanMetadataProvider = ctx.constructor - .newInstance(MethodDescriptor.ofConstructor(InterceptedBeanMetadataProvider.class)); + .newInstance(MethodDescriptor.ofConstructor(InterceptedDecoratedBeanMetadataProvider.class)); ResultHandle interceptedBeanMetadataProviderSupplier = ctx.constructor.newInstance( MethodDescriptors.FIXED_VALUE_SUPPLIER_CONSTRUCTOR, interceptedBeanMetadataProvider); @@ -515,6 +520,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 decorated 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..25337632031a8 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,22 @@ private static void validateInjections(InjectionPointInfo injectionPointInfo, Be "but was detected in: " + injectionPointInfo.getTargetInfo()); } + // If a Decorator instance is injected into a bean instance other than a decorator instance, + // the container automatically detects the problem and treats it as a definition error. + if (injectionPointInfo.getType().name().equals(DotNames.DECORATOR)) { + throw new DefinitionException("Invalid injection of Decorator bean, can only be used in decorators " + + "but was detected in: " + injectionPointInfo.getTargetInfo()); + } + + // If a Bean instance with qualifier @Decorated is injected into a bean instance other than a 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 @@ -153,6 +169,36 @@ private static void validateInjections(InjectionPointInfo injectionPointInfo, Be } } } + if (beanType == BeanType.DECORATOR) { + // the injection point is a field, an initializer method parameter or a bean constructor, with qualifier + // @Default, then the type parameter of the injected Decorator must be the same as the type + // declaring the injection point + if (injectionPointInfo.getRequiredType().name().equals(DotNames.DECORATOR) + && injectionPointInfo.getRequiredType().kind() == Type.Kind.PARAMETERIZED_TYPE + && injectionPointInfo.getRequiredType().asParameterizedType().arguments().size() == 1) { + Type actualType = injectionPointInfo.getRequiredType().asParameterizedType().arguments().get(0); + AnnotationTarget ipTarget = injectionPointInfo.getAnnotationTarget(); + DotName expectedType = null; + if (ipTarget.kind() == Kind.FIELD) { + expectedType = ipTarget.asField().declaringClass().name(); + } else if (ipTarget.kind() == Kind.METHOD_PARAMETER) { + expectedType = ipTarget.asMethodParameter().method().declaringClass().name(); + } + if (expectedType != null + // This is very rudimentary check, might need to be expanded? + && !expectedType.equals(actualType.name())) { + throw new DefinitionException( + "Type of injected Decorator does not match the type of the bean declaring the " + + "injection point. Problematic injection point: " + injectionPointInfo.getTargetInfo()); + } + } + + // the injection point is a field, an initializer method parameter or a bean constructor of a decorator, + // with qualifier @Decorated, then the type parameter of the injected Bean must be the same as the delegate type + // + // a validation for the specification text above would naturally belong here, but we don't have + // access to the delegate type yet, so this is postponed to `Beans.validateInterceptorDecorator()` + } } private static void validateInjections(List injections, BeanType beanType) { diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InterceptedBeanMetadataProvider.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InterceptedDecoratedBeanMetadataProvider.java similarity index 78% rename from independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InterceptedBeanMetadataProvider.java rename to independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InterceptedDecoratedBeanMetadataProvider.java index 1b919ce10860d..d90fa2580becc 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InterceptedBeanMetadataProvider.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InterceptedDecoratedBeanMetadataProvider.java @@ -4,15 +4,16 @@ import jakarta.enterprise.context.spi.Contextual; import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.Decorated; import jakarta.enterprise.inject.Intercepted; import jakarta.enterprise.inject.spi.Bean; import io.quarkus.arc.InjectableReferenceProvider; /** - * {@link Intercepted} {@link Bean} metadata provider. + * {@link Intercepted}/{@link Decorated} {@link Bean} metadata provider. */ -public class InterceptedBeanMetadataProvider implements InjectableReferenceProvider> { +public class InterceptedDecoratedBeanMetadataProvider implements InjectableReferenceProvider> { @Override public Contextual get(CreationalContext> creationalContext) { diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/DecoratedBeanInjectedInNonDecoratorTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/DecoratedBeanInjectedInNonDecoratorTest.java new file mode 100644 index 0000000000000..96f608b9e65c8 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/DecoratedBeanInjectedInNonDecoratorTest.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 DecoratedBeanInjectedInNonDecoratorTest { + + @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; + + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/DecoratedBeanInjectedWithWrongTypeParameterTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/DecoratedBeanInjectedWithWrongTypeParameterTest.java new file mode 100644 index 0000000000000..1708fdf53b3cb --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/DecoratedBeanInjectedWithWrongTypeParameterTest.java @@ -0,0 +1,69 @@ +package io.quarkus.arc.test.decorators.decorated; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.test.ArcTestContainer; + +public class DecoratedBeanInjectedWithWrongTypeParameterTest { + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(Converter.class, DecoratedBean.class, TrimConverterDecorator.class) + .shouldFail() + .build(); + + @Test + public void testDecoration() { + assertNotNull(container.getFailure()); + assertTrue(container.getFailure().getMessage().startsWith( + "Injected @Decorated Bean<> has to use the delegate type as its type parameter. Problematic injection point: " + + TrimConverterDecorator.class.getName() + "#decorated")); + } + + interface Converter { + T convert(T value); + } + + @ApplicationScoped + 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/DecoratedTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/decorators/decorated/DecoratedTest.java new file mode 100644 index 0000000000000..dd73c8505ed33 --- /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); + + @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")); + } + + interface Converter { + + T convert(T 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(); + } + + } +}