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 16, 2024
1 parent b46c8ef commit 36c636c
Show file tree
Hide file tree
Showing 11 changed files with 283 additions and 11 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<Account> 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`.
<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.
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 @@ -680,7 +680,6 @@ void validate(List<Throwable> errors, Consumer<BytecodeTransformer> bytecodeTran
}

void validateInterceptorDecorator(List<Throwable> errors, Consumer<BytecodeTransformer> bytecodeTransformerConsumer) {
// no actual validations done at the moment, but we still want the transformation
Beans.validateInterceptorDecorator(this, errors, bytecodeTransformerConsumer);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -816,6 +817,25 @@ static void validateInterceptorDecorator(BeanInfo bean, List<Throwable> 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<Throwable> errors, Consumer<BytecodeTransformer> bytecodeTransformerConsumer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
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,22 @@ private static void validateInjections(InjectionPointInfo injectionPointInfo, Be
"but was detected in: " + injectionPointInfo.getTargetInfo());
}

// If a Decorator<T> 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<T> 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<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 Expand Up @@ -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<T> 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<Injection> injections, BeanType beanType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Contextual<?>> {
public class InterceptedDecoratedBeanMetadataProvider implements InjectableReferenceProvider<Contextual<?>> {

@Override
public Contextual<?> get(CreationalContext<Contextual<?>> creationalContext) {
Expand Down
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 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<T>, can only be injected into decorators but was detected in: "
+ InvalidBean.class.getName() + "#decorated"));
}

@ApplicationScoped
static class InvalidBean {

@Inject
@Decorated
Bean<?> decorated;

}
}
Original file line number Diff line number Diff line change
@@ -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> {
T convert(T value);
}

@ApplicationScoped
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();
}
}
}
Loading

0 comments on commit 36c636c

Please sign in to comment.