Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support @Decorated bean injection in decorator #43864

Merged
merged 1 commit into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading