Skip to content

Commit

Permalink
support @decorated bean injection in decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
rmanibus committed Oct 14, 2024
1 parent 8d1764e commit 9bd9d42
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 4 deletions.
14 changes: 10 additions & 4 deletions docs/src/main/asciidoc/cdi.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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`.

Check warning on line 476 in docs/src/main/asciidoc/cdi.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than 'which'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using ', which (non restrictive clause preceded by a comma)' or 'that (restrictive clause without a comma)' rather than 'which'.", "location": {"path": "docs/src/main/asciidoc/cdi.adoc", "range": {"start": {"line": 476, "column": 55}}}, "severity": "INFO"}
<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.

Check warning on line 480 in docs/src/main/asciidoc/cdi.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'might (for possibility)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'might (for possibility)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/cdi.adoc", "range": {"start": {"line": 480, "column": 19}}}, "severity": "WARNING"}
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.

Check failure on line 482 in docs/src/main/asciidoc/cdi.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsErrors] Use 'you' rather than 'i'. Raw Output: {"message": "[Quarkus.TermsErrors] Use 'you' rather than 'i'.", "location": {"path": "docs/src/main/asciidoc/cdi.adoc", "range": {"start": {"line": 482, "column": 90}}}, "severity": "ERROR"}

Check warning on line 482 in docs/src/main/asciidoc/cdi.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'that is' rather than 'i.e.' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'that is' rather than 'i.e.' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/cdi.adoc", "range": {"start": {"line": 482, "column": 90}}}, "severity": "WARNING"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>, 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Contextual<?>> {

@Override
public Contextual<?> get(CreationalContext<Contextual<?>> 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;
}

}
Original file line number Diff line number Diff line change
@@ -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> {

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<String> {

@Override
public String convert(String value) {
return "Replaced by the decorator";
}

}

@Dependent
@Priority(1)
@Decorator
static class TrimConverterDecorator implements Converter<String> {

@Inject
@Any
@Delegate
Converter<String> 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();
}

}
}
Original file line number Diff line number Diff line change
@@ -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<T>, can only be injected into decorators but was detected in: "
+ InvalidBean.class.getName() + "#decorated"));
}

@ApplicationScoped
static class InvalidBean {

@Inject
@Decorated
Bean<?> decorated;

}
}

0 comments on commit 9bd9d42

Please sign in to comment.