diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java index 202a414b2ee611..6d226f963c521d 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcConfig.java @@ -69,6 +69,15 @@ public class ArcConfig { @ConfigItem(defaultValue = "true") public boolean transformUnproxyableClasses; + /** + * If set to true, if a private method that is neither an observer nor a producer and is annotated with an interceptor + * binding. + * An example of this is the use of {@code Transactional} on a private method of a bean. + * If set to false, Quarkus simply logs a warning that the annotation will be ignored. + */ + @ConfigItem(defaultValue = "false") + public boolean failOnInterceptedPrivateMethod; + /** * The default naming strategy for {@link ConfigProperties.NamingStrategy}. The allowed values are determined * by that enum diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java index 5327ef0b6b3df3..87cfd207fd0706 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java @@ -322,6 +322,7 @@ public boolean test(BeanInfo bean) { }); } builder.setTransformUnproxyableClasses(arcConfig.transformUnproxyableClasses); + builder.setFailOnInterceptedPrivateMethod(arcConfig.failOnInterceptedPrivateMethod); builder.setJtaCapabilities(capabilities.isPresent(Capability.TRANSACTIONS)); builder.setGenerateSources(BootstrapDebug.DEBUG_SOURCES_DIR != null); builder.setAllowMocking(launchModeBuildItem.getLaunchMode() == LaunchMode.TEST); diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/interceptor/FailingPrivateInterceptedMethodTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/interceptor/FailingPrivateInterceptedMethodTest.java new file mode 100644 index 00000000000000..8bc478d83ee847 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/interceptor/FailingPrivateInterceptedMethodTest.java @@ -0,0 +1,68 @@ +package io.quarkus.arc.test.interceptor; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.annotation.Priority; +import javax.enterprise.inject.spi.DeploymentException; +import javax.inject.Singleton; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InterceptorBinding; +import javax.interceptor.InvocationContext; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FailingPrivateInterceptedMethodTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(SimpleBean.class, SimpleInterceptor.class, Simple.class) + .addAsResource(new StringAsset("quarkus.arc.fail-on-intercepted-private-method=true"), + "application.properties")) + .setExpectedException(DeploymentException.class); + + @Test + public void testDeploymentFailed() { + // This method should not be invoked + } + + @Singleton + static class SimpleBean { + + @Simple + private String foo() { + return "foo"; + } + + } + + @Simple + @Priority(1) + @Interceptor + public static class SimpleInterceptor { + + @AroundInvoke + public Object mySuperCoolAroundInvoke(InvocationContext ctx) throws Exception { + return "private" + ctx.proceed(); + } + } + + @Target({ TYPE, METHOD }) + @Retention(RUNTIME) + @Documented + @InterceptorBinding + public @interface Simple { + + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/interceptor/IgnoredPrivateInterceptedMethodTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/interceptor/IgnoredPrivateInterceptedMethodTest.java new file mode 100644 index 00000000000000..f8467967a3d042 --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/interceptor/IgnoredPrivateInterceptedMethodTest.java @@ -0,0 +1,68 @@ +package io.quarkus.arc.test.interceptor; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.annotation.Priority; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InterceptorBinding; +import javax.interceptor.InvocationContext; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class IgnoredPrivateInterceptedMethodTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(SimpleBean.class, SimpleInterceptor.class, Simple.class)); + + @Inject + SimpleBean simpleBean; + + @Test + public void testBeanInvocation() { + assertEquals("foo", simpleBean.foo()); + } + + @Singleton + static class SimpleBean { + + @Simple + private String foo() { + return "foo"; + } + + } + + @Simple + @Priority(1) + @Interceptor + public static class SimpleInterceptor { + + @AroundInvoke + public Object mySuperCoolAroundInvoke(InvocationContext ctx) throws Exception { + return "private" + ctx.proceed(); + } + } + + @Target({ TYPE, METHOD }) + @Retention(RUNTIME) + @Documented + @InterceptorBinding + public @interface Simple { + + } +} 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 f797243500db4d..8c34ecf0969d6a 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 @@ -102,6 +102,8 @@ public class BeanDeployment { final boolean transformUnproxyableClasses; + final boolean failOnInterceptedPrivateMethod; + private final boolean jtaCapabilities; private final AlternativePriorities alternativePriorities; @@ -190,6 +192,7 @@ public class BeanDeployment { this.beanResolver = new BeanResolverImpl(this); this.interceptorResolver = new InterceptorResolver(this); this.transformUnproxyableClasses = builder.transformUnproxyableClasses; + this.failOnInterceptedPrivateMethod = builder.failOnInterceptedPrivateMethod; this.jtaCapabilities = builder.jtaCapabilities; this.alternativePriorities = builder.alternativePriorities; } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java index e2127e754a940b..0760f95f0c034a 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java @@ -69,6 +69,7 @@ public static Builder builder() { private final boolean generateSources; private final boolean allowMocking; private final boolean transformUnproxyableClasses; + private final boolean failOnInterceptedPrivateMethod; private final List>> suppressConditionGenerators; // This predicate is used to filter annotations for InjectionPoint metadata @@ -86,6 +87,7 @@ private BeanProcessor(Builder builder) { this.generateSources = builder.generateSources; this.allowMocking = builder.allowMocking; this.transformUnproxyableClasses = builder.transformUnproxyableClasses; + this.failOnInterceptedPrivateMethod = builder.failOnInterceptedPrivateMethod; this.suppressConditionGenerators = builder.suppressConditionGenerators; // Initialize all build processors @@ -298,6 +300,7 @@ public static class Builder { boolean generateSources; boolean jtaCapabilities; boolean transformUnproxyableClasses; + boolean failOnInterceptedPrivateMethod; boolean allowMocking; AlternativePriorities alternativePriorities; @@ -329,6 +332,7 @@ public Builder() { generateSources = false; jtaCapabilities = false; transformUnproxyableClasses = false; + failOnInterceptedPrivateMethod = false; allowMocking = false; excludeTypes = new ArrayList<>(); @@ -503,6 +507,14 @@ public Builder setTransformUnproxyableClasses(boolean value) { return this; } + /** + * If set to true, the build will fail if an annotation that would result in an interceptor being created (such as + * {@code @Transactional}) + */ + public void setFailOnInterceptedPrivateMethod(boolean failOnInterceptedPrivateMethod) { + this.failOnInterceptedPrivateMethod = failOnInterceptedPrivateMethod; + } + /** * If set to true the will generate source files of all generated classes for debug purposes. The generated source is * not actually a source file but a textual representation of generated code. diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Methods.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Methods.java index e11b57f0694ab5..dc7b71450cff79 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Methods.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Methods.java @@ -17,6 +17,7 @@ import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; +import javax.enterprise.inject.spi.DeploymentException; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; @@ -253,16 +254,24 @@ private static Set mergeBindings(BeanDeployment beanDeployme } if (Modifier.isPrivate(method.flags()) + && !Annotations.contains(methodAnnotations, DotNames.PRODUCES) && !Annotations.contains(methodAnnotations, DotNames.OBSERVES) && !Annotations.contains(methodAnnotations, DotNames.OBSERVES_ASYNC)) { + String message; if (methodLevelBindings.size() == 1) { - LOGGER.warnf("%s will have no effect on method %s.%s() because the method is private", + message = String.format("%s will have no effect on method %s.%s() because the method is private", methodLevelBindings.iterator().next(), classInfo.name(), method.name()); } else { - LOGGER.warnf("Annotations %s will have no effect on method %s.%s() because the method is private", + message = String.format( + "Annotations %s will have no effect on method %s.%s() because the method is private", methodLevelBindings.stream().map(AnnotationInstance::toString).collect(Collectors.joining(",")), classInfo.name(), method.name()); } + if (beanDeployment.failOnInterceptedPrivateMethod) { + throw new DeploymentException(message); + } else { + LOGGER.warn(message); + } } } return merged;