diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java index f5fb1364f1109..8af6b4fefca8d 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java @@ -104,6 +104,8 @@ public class BeanDeployment { private final Set removedBeans; + private final Set beansWithRuntimeDeferredUnproxyableError; + private final Map> customContexts; private final Map beanDefiningAnnotations; @@ -149,6 +151,7 @@ public class BeanDeployment { this.removeUnusedBeans = builder.removeUnusedBeans; this.unusedExclusions = removeUnusedBeans ? new ArrayList<>(builder.removalExclusions) : null; this.removedBeans = removeUnusedBeans ? new CopyOnWriteArraySet<>() : Collections.emptySet(); + this.beansWithRuntimeDeferredUnproxyableError = Collections.newSetFromMap(new ConcurrentHashMap<>()); this.customContexts = new ConcurrentHashMap<>(); this.excludeTypes = builder.excludeTypes != null ? new ArrayList<>(builder.excludeTypes) : Collections.emptyList(); @@ -506,6 +509,14 @@ public Collection getRemovedBeans() { return Collections.unmodifiableSet(removedBeans); } + boolean hasRuntimeDeferredUnproxyableError(BeanInfo bean) { + return beansWithRuntimeDeferredUnproxyableError.contains(bean); + } + + void deferUnproxyableErrorToRuntime(BeanInfo bean) { + beansWithRuntimeDeferredUnproxyableError.add(bean); + } + public Collection getQualifiers() { return Collections.unmodifiableCollection(qualifiers.values()); } @@ -1522,6 +1533,17 @@ private void validateBeans(List errors, Consumer Map> namedBeans = new HashMap<>(); Set classesReceivingNoArgsCtor = new HashSet<>(); + // this set is only used in strict compatible mode (see `Beans.validateBean()`), + // so no need to initialize it otherwise + Set injectedBeans = new HashSet<>(); + if (strictCompatibility) { + for (InjectionPointInfo injectionPoint : this.injectionPoints) { + if (injectionPoint.hasResolvedBean()) { + injectedBeans.add(injectionPoint.getResolvedBean()); + } + } + } + for (BeanInfo bean : beans) { if (bean.getName() != null) { List named = namedBeans.get(bean.getName()); @@ -1532,7 +1554,7 @@ private void validateBeans(List errors, Consumer named.add(bean); findNamespaces(bean, namespaces); } - bean.validate(errors, bytecodeTransformerConsumer, classesReceivingNoArgsCtor); + bean.validate(errors, bytecodeTransformerConsumer, classesReceivingNoArgsCtor, injectedBeans); } if (!namedBeans.isEmpty()) { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java index 15af009794e8a..40f5469aa85b4 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java @@ -30,6 +30,7 @@ import jakarta.enterprise.context.spi.CreationalContext; import jakarta.enterprise.inject.CreationException; import jakarta.enterprise.inject.IllegalProductException; +import jakarta.enterprise.inject.UnproxyableResolutionException; import jakarta.enterprise.inject.literal.InjectLiteral; import jakarta.enterprise.inject.spi.InterceptionType; import jakarta.interceptor.InvocationContext; @@ -1927,7 +1928,10 @@ protected void implementGet(BeanInfo bean, ClassCreator beanCreator, ProviderTyp MethodCreator get = beanCreator.getMethodCreator("get", providerType.descriptorName(), CreationalContext.class) .setModifiers(ACC_PUBLIC); - if (BuiltinScope.DEPENDENT.is(bean.getScope())) { + if (bean.getDeployment().hasRuntimeDeferredUnproxyableError(bean)) { + get.throwException(UnproxyableResolutionException.class, "Bean not proxyable: " + bean); + get.returnValue(get.loadNull()); + } else if (BuiltinScope.DEPENDENT.is(bean.getScope())) { // @Dependent pseudo-scope // Foo instance = create(ctx) ResultHandle instance = get.invokeVirtualMethod( @@ -2214,6 +2218,10 @@ private ResultHandle wrapCurrentInjectionPoint(BeanInfo bean, } private void initializeProxy(BeanInfo bean, String baseName, ClassCreator beanCreator) { + if (bean.getDeployment().hasRuntimeDeferredUnproxyableError(bean)) { + return; + } + // Add proxy volatile field String proxyTypeName = getProxyTypeName(bean, baseName); beanCreator.getFieldCreator(FIELD_NAME_PROXY, proxyTypeName) 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 9e904309f8882..94b5567a1df75 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 @@ -582,8 +582,8 @@ public String getClientProxyPackageName() { } void validate(List errors, Consumer bytecodeTransformerConsumer, - Set classesReceivingNoArgsCtor) { - Beans.validateBean(this, errors, bytecodeTransformerConsumer, classesReceivingNoArgsCtor); + Set classesReceivingNoArgsCtor, Set injectedBeans) { + Beans.validateBean(this, errors, bytecodeTransformerConsumer, classesReceivingNoArgsCtor, injectedBeans); } void validateInterceptorDecorator(List errors, Consumer 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 f6200b874084f..31ba850b8f231 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 @@ -711,25 +711,34 @@ static void validateInterceptorDecorator(BeanInfo bean, List errors, } static void validateBean(BeanInfo bean, List errors, Consumer bytecodeTransformerConsumer, - Set classesReceivingNoArgsCtor) { + Set classesReceivingNoArgsCtor, Set injectedBeans) { + + // by default, we fail deployment due to unproxyability for all beans, but in strict mode, + // we only do that for beans that are injected somewhere -- and defer the error to runtime otherwise, + // due to CDI spec requirements + boolean failIfNotProxyable = bean.getDeployment().strictCompatibility ? injectedBeans.contains(bean) : true; + if (bean.isClassBean()) { ClassInfo beanClass = bean.getTarget().get().asClass(); String classifier = bean.getScope().isNormal() ? "Normal scoped" : null; if (classifier == null && bean.isSubclassRequired()) { classifier = "Intercepted"; + failIfNotProxyable = true; } if (Modifier.isFinal(beanClass.flags()) && classifier != null) { // Client proxies and subclasses require a non-final class if (bean.getDeployment().transformUnproxyableClasses) { bytecodeTransformerConsumer .accept(new BytecodeTransformer(beanClass.name().toString(), new FinalClassTransformFunction())); - } else { + } else if (failIfNotProxyable) { errors.add(new DeploymentException(String.format("%s bean must not be final: %s", classifier, bean))); + } else { + bean.getDeployment().deferUnproxyableErrorToRuntime(bean); } } if (bean.getDeployment().strictCompatibility && classifier != null) { - validateNonStaticFinalMethods(beanClass, bean.getDeployment().getBeanArchiveIndex(), - classifier, errors); + validateNonStaticFinalMethods(bean, beanClass, bean.getDeployment().getBeanArchiveIndex(), + classifier, errors, failIfNotProxyable); } MethodInfo noArgsConstructor = beanClass.method(Methods.INIT); @@ -754,13 +763,16 @@ static void validateBean(BeanInfo bean, List errors, Consumer errors, Consumer errors, Consumer errors, Consumer errors, Consumer errors) { + private static void validateNonStaticFinalMethods(BeanInfo bean, ClassInfo clazz, IndexView beanArchiveIndex, + String classifier, List errors, boolean failIfNotProxyable) { // see also Methods.skipForClientProxy() while (!clazz.name().equals(DotNames.OBJECT)) { for (MethodInfo method : clazz.methods()) { @@ -920,9 +936,13 @@ private static void validateNonStaticFinalMethods(ClassInfo clazz, IndexView bea } if (Modifier.isFinal(method.flags())) { - errors.add(new DeploymentException(String.format( - "%s bean must not declare non-static final methods with public, protected or default visibility: %s", - classifier, method))); + if (failIfNotProxyable) { + errors.add(new DeploymentException(String.format( + "%s bean must not declare non-static final methods with public, protected or default visibility: %s", + classifier, method))); + } else { + bean.getDeployment().deferUnproxyableErrorToRuntime(bean); + } } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java index ea0277af08f00..8421c9aed59ee 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java @@ -78,6 +78,12 @@ public ClientProxyGenerator(Predicate applicationClassPredicate, boolea Collection generate(BeanInfo bean, String beanClassName, Consumer bytecodeTransformerConsumer, boolean transformUnproxyableClasses) { + // see `BeanGenerator` -- if this bean is unproxyable and that error is deferred to runtime, + // we don't need to (and cannot, in fact) generate the client proxy class + if (bean.getDeployment().hasRuntimeDeferredUnproxyableError(bean)) { + return Collections.emptySet(); + } + ProviderType providerType = new ProviderType(bean.getProviderType()); ClassInfo providerClass = getClassByName(bean.getDeployment().getBeanArchiveIndex(), providerType.name()); String baseName = getBaseName(bean, beanClassName); diff --git a/independent-projects/arc/tcks/cdi-tck-runner/src/test/resources/testng.xml b/independent-projects/arc/tcks/cdi-tck-runner/src/test/resources/testng.xml index 60e5d372194dc..87e459298438e 100644 --- a/independent-projects/arc/tcks/cdi-tck-runner/src/test/resources/testng.xml +++ b/independent-projects/arc/tcks/cdi-tck-runner/src/test/resources/testng.xml @@ -81,15 +81,6 @@ - - - - - - - - - diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/finalmethod/FinalMethodIllegalTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/finalmethod/FinalMethodIllegalWhenInjectedTest.java similarity index 76% rename from independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/finalmethod/FinalMethodIllegalTest.java rename to independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/finalmethod/FinalMethodIllegalWhenInjectedTest.java index fc7da12739ff8..748735b30dbfe 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/finalmethod/FinalMethodIllegalTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/finalmethod/FinalMethodIllegalWhenInjectedTest.java @@ -5,17 +5,19 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.spi.DeploymentException; +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 FinalMethodIllegalTest { +public class FinalMethodIllegalWhenInjectedTest { @RegisterExtension public ArcTestContainer container = ArcTestContainer.builder() - .beanClasses(Moo.class) + .beanClasses(Moo.class, MooConsumer.class) .strictCompatibility(true) .shouldFail() .build(); @@ -34,4 +36,11 @@ final int getVal() { return -1; } } + + // to trigger deployment-time error (in strict compatible mode) + @Dependent + static class MooConsumer { + @Inject + Moo moo; + } } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/finalmethod/FinalMethodIllegalWhenNotInjectedTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/finalmethod/FinalMethodIllegalWhenNotInjectedTest.java new file mode 100644 index 0000000000000..d82d2a136e437 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/finalmethod/FinalMethodIllegalWhenNotInjectedTest.java @@ -0,0 +1,35 @@ +package io.quarkus.arc.test.clientproxy.finalmethod; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.UnproxyableResolutionException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.test.ArcTestContainer; + +public class FinalMethodIllegalWhenNotInjectedTest { + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(Moo.class) + .strictCompatibility(true) + .shouldFail() + .build(); + + @Test + public void test() { + assertThrows(UnproxyableResolutionException.class, () -> { + Arc.container().instance(Moo.class).get(); + }); + } + + @ApplicationScoped + static class Moo { + final int getVal() { + return -1; + } + } +}