diff --git a/core/deployment/src/main/java/io/quarkus/deployment/execannotations/ExecutionModelAnnotationsAllowedBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/execannotations/ExecutionModelAnnotationsAllowedBuildItem.java
new file mode 100644
index 00000000000000..b11a0c5c30ef06
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/execannotations/ExecutionModelAnnotationsAllowedBuildItem.java
@@ -0,0 +1,28 @@
+package io.quarkus.deployment.execannotations;
+
+import java.util.Objects;
+import java.util.function.Predicate;
+
+import org.jboss.jandex.MethodInfo;
+
+import io.quarkus.builder.item.MultiBuildItem;
+
+/**
+ * Carries a predicate that identifies methods that can have annotations which affect
+ * the execution model ({@code @Blocking}, {@code @NonBlocking}, {@code @RunOnVirtualThread}).
+ *
+ * Used to detect wrong usage of these annotations, as they are implemented directly
+ * by the various frameworks and may only be put on "entrypoint" methods. Placing these
+ * annotations on methods that can only be invoked by application code is always wrong.
+ */
+public final class ExecutionModelAnnotationsAllowedBuildItem extends MultiBuildItem {
+ private final Predicate predicate;
+
+ public ExecutionModelAnnotationsAllowedBuildItem(Predicate predicate) {
+ this.predicate = Objects.requireNonNull(predicate);
+ }
+
+ public boolean matches(MethodInfo method) {
+ return predicate.test(method);
+ }
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/execannotations/ExecutionModelAnnotationsConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/execannotations/ExecutionModelAnnotationsConfig.java
new file mode 100644
index 00000000000000..7c8cd2cb0a1c81
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/execannotations/ExecutionModelAnnotationsConfig.java
@@ -0,0 +1,35 @@
+package io.quarkus.deployment.execannotations;
+
+import io.quarkus.runtime.annotations.ConfigPhase;
+import io.quarkus.runtime.annotations.ConfigRoot;
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+
+@ConfigRoot(phase = ConfigPhase.BUILD_TIME)
+@ConfigMapping(prefix = "quarkus.execution-model-annotations")
+public interface ExecutionModelAnnotationsConfig {
+ /**
+ * Detection mode of invalid usage of execution model annotations.
+ *
+ * An execution model annotation is {@code @Blocking}, {@code @NonBlocking} and {@code @RunOnVirtualThread}.
+ * These annotation may only be used on methods that are invoked by the container; using them on any other
+ * method is invalid.
+ */
+ @WithDefault("fail")
+ Mode detectionMode();
+
+ enum Mode {
+ /**
+ * Invalid usage of execution model annotations causes build failure.
+ */
+ FAIL,
+ /**
+ * Invalid usage of execution model annotations causes warning during build.
+ */
+ WARN,
+ /**
+ * No detection of invalid usage of execution model annotations.
+ */
+ DISABLED,
+ }
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/execannotations/ExecutionModelAnnotationsProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/execannotations/ExecutionModelAnnotationsProcessor.java
new file mode 100644
index 00000000000000..dd592f0cb9989a
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/execannotations/ExecutionModelAnnotationsProcessor.java
@@ -0,0 +1,92 @@
+package io.quarkus.deployment.execannotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringJoiner;
+
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.AnnotationTarget;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.IndexView;
+import org.jboss.jandex.MethodInfo;
+import org.jboss.jandex.Type;
+import org.jboss.logging.Logger;
+
+import io.quarkus.deployment.SuppressForbidden;
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
+import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
+import io.smallrye.common.annotation.Blocking;
+import io.smallrye.common.annotation.NonBlocking;
+import io.smallrye.common.annotation.RunOnVirtualThread;
+
+public class ExecutionModelAnnotationsProcessor {
+ private static final Logger log = Logger.getLogger(ExecutionModelAnnotationsProcessor.class);
+
+ private static final DotName BLOCKING = DotName.createSimple(Blocking.class);
+ private static final DotName NON_BLOCKING = DotName.createSimple(NonBlocking.class);
+ private static final DotName RUN_ON_VIRTUAL_THREAD = DotName.createSimple(RunOnVirtualThread.class);
+
+ @BuildStep
+ void check(BuildProducer ignored, // only to make sure this build step is executed
+ ExecutionModelAnnotationsConfig config, CombinedIndexBuildItem index,
+ List predicates) {
+
+ if (config.detectionMode() == ExecutionModelAnnotationsConfig.Mode.DISABLED) {
+ return;
+ }
+
+ StringBuilder message = new StringBuilder("\n");
+ doCheck(message, index.getIndex(), predicates, BLOCKING);
+ doCheck(message, index.getIndex(), predicates, NON_BLOCKING);
+ doCheck(message, index.getIndex(), predicates, RUN_ON_VIRTUAL_THREAD);
+
+ if (message.length() > 1) {
+ message.append("The @Blocking, @NonBlocking and @RunOnVirtualThread annotations may only be used "
+ + "on methods that are invoked by the container");
+ if (config.detectionMode() == ExecutionModelAnnotationsConfig.Mode.WARN) {
+ log.warn(message);
+ } else {
+ throw new IllegalStateException(message.toString());
+ }
+ }
+ }
+
+ private void doCheck(StringBuilder message, IndexView index,
+ List predicates, DotName annotationName) {
+
+ List badMethods = new ArrayList<>();
+ for (AnnotationInstance annotation : index.getAnnotations(annotationName)) {
+ // these annotations may be put on classes too, but we'll ignore that for now
+ if (annotation.target() != null && annotation.target().kind() == AnnotationTarget.Kind.METHOD) {
+ MethodInfo method = annotation.target().asMethod();
+ for (ExecutionModelAnnotationsAllowedBuildItem predicate : predicates) {
+ if (!predicate.matches(method)) {
+ badMethods.add(methodToString(method));
+ break;
+ }
+ }
+ }
+ }
+
+ if (!badMethods.isEmpty()) {
+ message.append("Wrong usage(s) of @").append(annotationName.withoutPackagePrefix()).append(" found:\n");
+ for (String method : badMethods) {
+ message.append("\t- ").append(method).append("\n");
+ }
+ }
+ }
+
+ @SuppressForbidden(reason = "Using Type.toString() to build an informative message")
+ private String methodToString(MethodInfo method) {
+ StringBuilder result = new StringBuilder();
+ result.append(method.declaringClass().name()).append('.').append(method.name());
+ StringJoiner joiner = new StringJoiner(", ", "(", ")");
+ for (Type parameter : method.parameterTypes()) {
+ joiner.add(parameter.toString());
+ }
+ result.append(joiner);
+ return result.toString();
+ }
+}
diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcMethodsProcessor.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcMethodsProcessor.java
new file mode 100644
index 00000000000000..ceb4ce90d91ab0
--- /dev/null
+++ b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcMethodsProcessor.java
@@ -0,0 +1,20 @@
+package io.quarkus.grpc.deployment;
+
+import java.util.function.Predicate;
+
+import org.jboss.jandex.MethodInfo;
+
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.execannotations.ExecutionModelAnnotationsAllowedBuildItem;
+
+public class GrpcMethodsProcessor {
+ @BuildStep
+ ExecutionModelAnnotationsAllowedBuildItem grpcMethods() {
+ return new ExecutionModelAnnotationsAllowedBuildItem(new Predicate() {
+ @Override
+ public boolean test(MethodInfo method) {
+ return method.declaringClass().hasDeclaredAnnotation(GrpcDotNames.GRPC_SERVICE);
+ }
+ });
+ }
+}
diff --git a/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/ReactiveRoutesMethodsProcessor.java b/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/ReactiveRoutesMethodsProcessor.java
new file mode 100644
index 00000000000000..5bb223f5a0da29
--- /dev/null
+++ b/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/ReactiveRoutesMethodsProcessor.java
@@ -0,0 +1,21 @@
+package io.quarkus.vertx.web.deployment;
+
+import java.util.function.Predicate;
+
+import org.jboss.jandex.MethodInfo;
+
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.execannotations.ExecutionModelAnnotationsAllowedBuildItem;
+
+public class ReactiveRoutesMethodsProcessor {
+ @BuildStep
+ ExecutionModelAnnotationsAllowedBuildItem reactiveRoutesMethods() {
+ return new ExecutionModelAnnotationsAllowedBuildItem(new Predicate() {
+ @Override
+ public boolean test(MethodInfo method) {
+ return method.hasDeclaredAnnotation(DotNames.ROUTE)
+ || method.hasDeclaredAnnotation(DotNames.ROUTES);
+ }
+ });
+ }
+}
diff --git a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/execannotations/ExecAnnotationInvalidTest.java b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/execannotations/ExecAnnotationInvalidTest.java
new file mode 100644
index 00000000000000..2b6f6e0536bf68
--- /dev/null
+++ b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/execannotations/ExecAnnotationInvalidTest.java
@@ -0,0 +1,34 @@
+package io.quarkus.execannotations;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.smallrye.common.annotation.Blocking;
+
+public class ExecAnnotationInvalidTest {
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest()
+ .withApplicationRoot(jar -> jar.addClasses(MyService.class))
+ .assertException(e -> {
+ assertInstanceOf(IllegalStateException.class, e);
+ assertTrue(e.getMessage().contains("Wrong usage"));
+ assertTrue(e.getMessage().contains("MyService.hello()"));
+ });
+
+ @Test
+ public void test() {
+ fail();
+ }
+
+ static class MyService {
+ @Blocking
+ String hello() {
+ return "Hello world!";
+ }
+ }
+}
diff --git a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/execannotations/ExecAnnotationValidTest.java b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/execannotations/ExecAnnotationValidTest.java
new file mode 100644
index 00000000000000..940f101046d703
--- /dev/null
+++ b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/execannotations/ExecAnnotationValidTest.java
@@ -0,0 +1,30 @@
+package io.quarkus.execannotations;
+
+import static io.restassured.RestAssured.when;
+import static org.hamcrest.Matchers.is;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.vertx.web.Route;
+import io.smallrye.common.annotation.Blocking;
+
+public class ExecAnnotationValidTest {
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest()
+ .withApplicationRoot(jar -> jar.addClasses(MyService.class));
+
+ @Test
+ public void test() {
+ when().get("/").then().statusCode(200).body(is("Hello world!"));
+ }
+
+ static class MyService {
+ @Route(path = "/")
+ @Blocking
+ String hello() {
+ return "Hello world!";
+ }
+ }
+}
diff --git a/extensions/resteasy-classic/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/JaxrsMethodsProcessor.java b/extensions/resteasy-classic/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/JaxrsMethodsProcessor.java
new file mode 100644
index 00000000000000..5c184981a45518
--- /dev/null
+++ b/extensions/resteasy-classic/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/JaxrsMethodsProcessor.java
@@ -0,0 +1,35 @@
+package io.quarkus.resteasy.common.deployment;
+
+import java.util.function.Predicate;
+
+import org.jboss.jandex.MethodInfo;
+
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.execannotations.ExecutionModelAnnotationsAllowedBuildItem;
+import io.quarkus.resteasy.common.spi.ResteasyDotNames;
+
+public class JaxrsMethodsProcessor {
+ @BuildStep
+ ExecutionModelAnnotationsAllowedBuildItem jaxrsMethods() {
+ return new ExecutionModelAnnotationsAllowedBuildItem(new Predicate() {
+ @Override
+ public boolean test(MethodInfo method) {
+ // looking for `@Path` on the declaring class is enough
+ // to avoid having to process inherited JAX-RS annotations
+ if (method.declaringClass().hasDeclaredAnnotation(ResteasyDotNames.PATH)) {
+ return true;
+ }
+
+ // we currently don't handle custom @HttpMethod annotations, should be fine most of the time
+ return method.hasDeclaredAnnotation(ResteasyDotNames.PATH)
+ || method.hasDeclaredAnnotation(ResteasyDotNames.GET)
+ || method.hasDeclaredAnnotation(ResteasyDotNames.POST)
+ || method.hasDeclaredAnnotation(ResteasyDotNames.PUT)
+ || method.hasDeclaredAnnotation(ResteasyDotNames.DELETE)
+ || method.hasDeclaredAnnotation(ResteasyDotNames.PATCH)
+ || method.hasDeclaredAnnotation(ResteasyDotNames.HEAD)
+ || method.hasDeclaredAnnotation(ResteasyDotNames.OPTIONS);
+ }
+ });
+ }
+}
diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/JaxrsMethodsProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/JaxrsMethodsProcessor.java
new file mode 100644
index 00000000000000..8ec28ced232222
--- /dev/null
+++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/JaxrsMethodsProcessor.java
@@ -0,0 +1,35 @@
+package io.quarkus.resteasy.reactive.common.deployment;
+
+import java.util.function.Predicate;
+
+import org.jboss.jandex.MethodInfo;
+import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames;
+
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.execannotations.ExecutionModelAnnotationsAllowedBuildItem;
+
+public class JaxrsMethodsProcessor {
+ @BuildStep
+ ExecutionModelAnnotationsAllowedBuildItem jaxrsMethods() {
+ return new ExecutionModelAnnotationsAllowedBuildItem(new Predicate() {
+ @Override
+ public boolean test(MethodInfo method) {
+ // looking for `@Path` on the declaring class is enough
+ // to avoid having to process inherited JAX-RS annotations
+ if (method.declaringClass().hasDeclaredAnnotation(ResteasyReactiveDotNames.PATH)) {
+ return true;
+ }
+
+ // we currently don't handle custom @HttpMethod annotations, should be fine most of the time
+ return method.hasDeclaredAnnotation(ResteasyReactiveDotNames.PATH)
+ || method.hasDeclaredAnnotation(ResteasyReactiveDotNames.GET)
+ || method.hasDeclaredAnnotation(ResteasyReactiveDotNames.POST)
+ || method.hasDeclaredAnnotation(ResteasyReactiveDotNames.PUT)
+ || method.hasDeclaredAnnotation(ResteasyReactiveDotNames.DELETE)
+ || method.hasDeclaredAnnotation(ResteasyReactiveDotNames.PATCH)
+ || method.hasDeclaredAnnotation(ResteasyReactiveDotNames.HEAD)
+ || method.hasDeclaredAnnotation(ResteasyReactiveDotNames.OPTIONS);
+ }
+ });
+ }
+}
diff --git a/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerMethodsProcessor.java b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerMethodsProcessor.java
new file mode 100644
index 00000000000000..dcb47c2703845d
--- /dev/null
+++ b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerMethodsProcessor.java
@@ -0,0 +1,21 @@
+package io.quarkus.scheduler.deployment;
+
+import java.util.function.Predicate;
+
+import org.jboss.jandex.MethodInfo;
+
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.execannotations.ExecutionModelAnnotationsAllowedBuildItem;
+
+public class SchedulerMethodsProcessor {
+ @BuildStep
+ ExecutionModelAnnotationsAllowedBuildItem schedulerMethods() {
+ return new ExecutionModelAnnotationsAllowedBuildItem(new Predicate() {
+ @Override
+ public boolean test(MethodInfo method) {
+ return method.hasDeclaredAnnotation(SchedulerDotNames.SCHEDULED_NAME)
+ || method.hasDeclaredAnnotation(SchedulerDotNames.SCHEDULES_NAME);
+ }
+ });
+ }
+}
diff --git a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/GraphqlMethodsProcessor.java b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/GraphqlMethodsProcessor.java
new file mode 100644
index 00000000000000..a08df63f9f1971
--- /dev/null
+++ b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/GraphqlMethodsProcessor.java
@@ -0,0 +1,31 @@
+package io.quarkus.smallrye.graphql.deployment;
+
+import java.util.function.Predicate;
+
+import org.eclipse.microprofile.graphql.Mutation;
+import org.eclipse.microprofile.graphql.Query;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.MethodInfo;
+
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.execannotations.ExecutionModelAnnotationsAllowedBuildItem;
+import io.smallrye.graphql.api.Subscription;
+
+public class GraphqlMethodsProcessor {
+ private static final DotName QUERY = DotName.createSimple(Query.class);
+ private static final DotName MUTATION = DotName.createSimple(Mutation.class);
+ private static final DotName SUBSCRIPTION = DotName.createSimple(Subscription.class);
+
+ @BuildStep
+ ExecutionModelAnnotationsAllowedBuildItem graphqlMethods() {
+ return new ExecutionModelAnnotationsAllowedBuildItem(new Predicate() {
+ @Override
+ public boolean test(MethodInfo method) {
+ // maybe just look for `@GraphQLApi` on the declaring class?
+ return method.hasDeclaredAnnotation(QUERY)
+ || method.hasDeclaredAnnotation(MUTATION)
+ || method.hasDeclaredAnnotation(SUBSCRIPTION);
+ }
+ });
+ }
+}
diff --git a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/ReactiveMessagingMethodsProcessor.java b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/ReactiveMessagingMethodsProcessor.java
new file mode 100644
index 00000000000000..b19916a6a68b63
--- /dev/null
+++ b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/ReactiveMessagingMethodsProcessor.java
@@ -0,0 +1,23 @@
+package io.quarkus.smallrye.reactivemessaging.deployment;
+
+import java.util.function.Predicate;
+
+import org.jboss.jandex.MethodInfo;
+
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.execannotations.ExecutionModelAnnotationsAllowedBuildItem;
+
+public class ReactiveMessagingMethodsProcessor {
+ @BuildStep
+ ExecutionModelAnnotationsAllowedBuildItem reactiveMessagingMethods() {
+ return new ExecutionModelAnnotationsAllowedBuildItem(new Predicate() {
+ @Override
+ public boolean test(MethodInfo method) {
+ return method.hasDeclaredAnnotation(ReactiveMessagingDotNames.INCOMING)
+ || method.hasDeclaredAnnotation(ReactiveMessagingDotNames.INCOMINGS)
+ || method.hasDeclaredAnnotation(ReactiveMessagingDotNames.OUTGOING)
+ || method.hasDeclaredAnnotation(ReactiveMessagingDotNames.OUTGOINGS);
+ }
+ });
+ }
+}
diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIMethodsProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIMethodsProcessor.java
new file mode 100644
index 00000000000000..86f0fb85ff4b73
--- /dev/null
+++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIMethodsProcessor.java
@@ -0,0 +1,30 @@
+package io.quarkus.devui.deployment;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.MethodInfo;
+
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.execannotations.ExecutionModelAnnotationsAllowedBuildItem;
+import io.quarkus.devui.spi.JsonRPCProvidersBuildItem;
+
+public class DevUIMethodsProcessor {
+ @BuildStep
+ ExecutionModelAnnotationsAllowedBuildItem devuiMethods(List rpcProviders) {
+ Set rpcProviderClasses = new HashSet<>();
+ for (JsonRPCProvidersBuildItem rpcProvider : rpcProviders) {
+ rpcProviderClasses.add(DotName.createSimple(rpcProvider.getJsonRPCMethodProviderClass()));
+ }
+
+ return new ExecutionModelAnnotationsAllowedBuildItem(new Predicate() {
+ @Override
+ public boolean test(MethodInfo method) {
+ return rpcProviderClasses.contains(method.declaringClass().name());
+ }
+ });
+ }
+}