diff --git a/extensions/resteasy-reactive/rest-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/JaxrsMethodsProcessor.java b/extensions/resteasy-reactive/rest-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/JaxrsMethodsProcessor.java index 8ec28ced23222..96cb6cdb24df3 100644 --- a/extensions/resteasy-reactive/rest-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/JaxrsMethodsProcessor.java +++ b/extensions/resteasy-reactive/rest-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/JaxrsMethodsProcessor.java @@ -2,15 +2,20 @@ import java.util.function.Predicate; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.Type; import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; +import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.execannotations.ExecutionModelAnnotationsAllowedBuildItem; public class JaxrsMethodsProcessor { @BuildStep - ExecutionModelAnnotationsAllowedBuildItem jaxrsMethods() { + ExecutionModelAnnotationsAllowedBuildItem jaxrsMethods(BeanArchiveIndexBuildItem beanArchiveIndex) { + IndexView index = beanArchiveIndex.getIndex(); return new ExecutionModelAnnotationsAllowedBuildItem(new Predicate() { @Override public boolean test(MethodInfo method) { @@ -19,7 +24,29 @@ public boolean test(MethodInfo method) { if (method.declaringClass().hasDeclaredAnnotation(ResteasyReactiveDotNames.PATH)) { return true; } + if (testMethod(method)) { + return true; + } + + // also look at interfaces implemented by the method's declaringClass + for (Type interfaceType : method.declaringClass().interfaceTypes()) { + ClassInfo interfaceInfo = index.getClassByName(interfaceType.name()); + if (interfaceInfo != null) { + if (interfaceInfo.hasDeclaredAnnotation(ResteasyReactiveDotNames.PATH)) { + return true; + } + MethodInfo overriddenMethodInfo = interfaceInfo.method(method.name(), + method.parameterTypes().toArray(new Type[0])); + if (overriddenMethodInfo != null && testMethod(overriddenMethodInfo)) { + return true; + } + } + } + + return false; + } + private boolean testMethod(MethodInfo method) { // we currently don't handle custom @HttpMethod annotations, should be fine most of the time return method.hasDeclaredAnnotation(ResteasyReactiveDotNames.PATH) || method.hasDeclaredAnnotation(ResteasyReactiveDotNames.GET) diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java index ae5576b31d7a2..c9e4a537b2139 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java @@ -728,7 +728,7 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf } Set nameBindingNames = nameBindingNames(currentMethodInfo, classNameBindings); boolean blocking = isBlocking(currentMethodInfo, defaultBlocking); - boolean runOnVirtualThread = isRunOnVirtualThread(currentMethodInfo, defaultBlocking); + boolean runOnVirtualThread = isRunOnVirtualThread(currentMethodInfo, blocking, defaultBlocking); // we want to allow "overriding" the blocking/non-blocking setting from an implementation class // when the class defining the annotations is an interface if (!actualEndpointInfo.equals(currentClassInfo) && Modifier.isInterface(currentClassInfo.flags())) { @@ -739,7 +739,7 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf //would be reached for a default blocking = isBlocking(actualMethodInfo, blocking ? BlockingDefault.BLOCKING : BlockingDefault.NON_BLOCKING); - runOnVirtualThread = isRunOnVirtualThread(actualMethodInfo, + runOnVirtualThread = isRunOnVirtualThread(actualMethodInfo, blocking, blocking ? BlockingDefault.BLOCKING : BlockingDefault.NON_BLOCKING); } } @@ -841,8 +841,7 @@ private String getAnnotationValueAsString(AnnotationTarget target, DotName annot return value; } - private boolean isRunOnVirtualThread(MethodInfo info, BlockingDefault defaultValue) { - boolean isRunOnVirtualThread = false; + private boolean isRunOnVirtualThread(MethodInfo info, boolean blocking, BlockingDefault defaultValue) { Map.Entry runOnVirtualThreadAnnotation = getInheritableAnnotation(info, RUN_ON_VIRTUAL_THREAD); @@ -856,28 +855,19 @@ private boolean isRunOnVirtualThread(MethodInfo info, BlockingDefault defaultVal throw new DeploymentException("Method '" + info.name() + "' of class '" + info.declaringClass().name() + "' uses @RunOnVirtualThread but the target JDK version doesn't support virtual threads. Please configure your build tool to target Java 19 or above"); } - isRunOnVirtualThread = true; - } - - //BlockingDefault.BLOCKING should mean "block a platform thread" ? here it does - if (defaultValue == BlockingDefault.BLOCKING) { - return false; + if (!blocking) { + throw new DeploymentException( + "Method '" + info.name() + "' of class '" + info.declaringClass().name() + + "' is considered a non blocking method. @RunOnVirtualThread can only be used on " + + " methods considered blocking"); + } else { + return true; + } } else if (defaultValue == BlockingDefault.RUN_ON_VIRTUAL_THREAD) { - isRunOnVirtualThread = true; - } else if (defaultValue == BlockingDefault.NON_BLOCKING) { - return false; - } - - if (isRunOnVirtualThread && !isBlocking(info, defaultValue)) { - throw new DeploymentException( - "Method '" + info.name() + "' of class '" + info.declaringClass().name() - + "' is considered a non blocking method. @RunOnVirtualThread can only be used on " + - " methods considered blocking"); - } else if (isRunOnVirtualThread) { return true; + } else { + return false; } - - return false; } private boolean isBlocking(MethodInfo info, BlockingDefault defaultValue) { diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java index 83db410456094..ff8b53878f00f 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java @@ -105,14 +105,23 @@ public static ApplicationScanningResult scanForApplicationClass(IndexView index, | InvocationTargetException e) { throw new RuntimeException("Unable to handle class: " + applicationClass, e); } - if (applicationClassInfo.declaredAnnotation(ResteasyReactiveDotNames.BLOCKING) != null) { - if (applicationClassInfo.declaredAnnotation(ResteasyReactiveDotNames.NON_BLOCKING) != null) { - throw new DeploymentException("JAX-RS Application class '" + applicationClassInfo.name() - + "' contains both @Blocking and @NonBlocking annotations."); - } + // collect default behaviour, making sure that we don't have multiple contradicting annotations + int numAnnotations = 0; + if (applicationClassInfo.hasDeclaredAnnotation(ResteasyReactiveDotNames.BLOCKING)) { blocking = BlockingDefault.BLOCKING; - } else if (applicationClassInfo.declaredAnnotation(ResteasyReactiveDotNames.NON_BLOCKING) != null) { + numAnnotations++; + } + if (applicationClassInfo.hasDeclaredAnnotation(ResteasyReactiveDotNames.NON_BLOCKING)) { blocking = BlockingDefault.NON_BLOCKING; + numAnnotations++; + } + if (applicationClassInfo.hasDeclaredAnnotation(ResteasyReactiveDotNames.RUN_ON_VIRTUAL_THREAD)) { + blocking = BlockingDefault.RUN_ON_VIRTUAL_THREAD; + numAnnotations++; + } + if (numAnnotations > 1) { + throw new DeploymentException("JAX-RS Application class '" + applicationClassInfo.name() + + "' contains multiple conflicting @Blocking, @NonBlocking and @RunOnVirtualThread annotations."); } } if (selectedAppClass != null) { diff --git a/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/IResource.java b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/IResource.java new file mode 100644 index 0000000000000..75abd9ddedeee --- /dev/null +++ b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/IResource.java @@ -0,0 +1,16 @@ +package io.quarkus.virtual.rr; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +@Path("/itf") +public interface IResource { + + @GET + String testGet(); + + @POST + String testPost(String body); + +} diff --git a/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/IResourceOnClass.java b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/IResourceOnClass.java new file mode 100644 index 0000000000000..f03baad3bc58f --- /dev/null +++ b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/IResourceOnClass.java @@ -0,0 +1,16 @@ +package io.quarkus.virtual.rr; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +@Path("/itfOnClass") +public interface IResourceOnClass { + + @GET + String testGet(); + + @POST + String testPost(String body); + +} diff --git a/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/ResourceImpl.java b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/ResourceImpl.java new file mode 100644 index 0000000000000..b2439436526a8 --- /dev/null +++ b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/ResourceImpl.java @@ -0,0 +1,29 @@ +package io.quarkus.virtual.rr; + +import jakarta.enterprise.context.RequestScoped; + +import io.quarkus.test.vertx.VirtualThreadsAssertions; +import io.smallrye.common.annotation.RunOnVirtualThread; + +@RequestScoped +public class ResourceImpl implements IResource { + + private final Counter counter; + + ResourceImpl(Counter counter) { + this.counter = counter; + } + + @RunOnVirtualThread + public String testGet() { + VirtualThreadsAssertions.assertEverything(); + return "hello-" + counter.increment(); + } + + @RunOnVirtualThread + public String testPost(String body) { + VirtualThreadsAssertions.assertEverything(); + return body + "-" + counter.increment(); + } + +} diff --git a/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/ResourceOnClassImpl.java b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/ResourceOnClassImpl.java new file mode 100644 index 0000000000000..81eabf3de9420 --- /dev/null +++ b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/ResourceOnClassImpl.java @@ -0,0 +1,28 @@ +package io.quarkus.virtual.rr; + +import jakarta.enterprise.context.RequestScoped; + +import io.quarkus.test.vertx.VirtualThreadsAssertions; +import io.smallrye.common.annotation.RunOnVirtualThread; + +@RequestScoped +@RunOnVirtualThread +public class ResourceOnClassImpl implements IResourceOnClass { + + private final Counter counter; + + ResourceOnClassImpl(Counter counter) { + this.counter = counter; + } + + public String testGet() { + VirtualThreadsAssertions.assertEverything(); + return "hello-" + counter.increment(); + } + + public String testPost(String body) { + VirtualThreadsAssertions.assertEverything(); + return body + "-" + counter.increment(); + } + +} diff --git a/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rr/RunOnVirtualThreadTest.java b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rr/RunOnVirtualThreadTest.java index a2af77a6f9be2..1012012b040ff 100644 --- a/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rr/RunOnVirtualThreadTest.java +++ b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rr/RunOnVirtualThreadTest.java @@ -2,6 +2,7 @@ import static org.hamcrest.Matchers.is; +import java.util.Arrays; import java.util.UUID; import org.junit.jupiter.api.Test; @@ -18,30 +19,42 @@ class RunOnVirtualThreadTest { @Test void testGet() { - RestAssured.get().then() - .assertThat().statusCode(200) - .body(is("hello-1")); - RestAssured.get().then() - .assertThat().statusCode(200) - // Same value - request scoped bean - .body(is("hello-1")); + // test all variations: + // - MyResource ("/"): simple JAX-RS bean + // - ResourceImpl ("/itf"): bean implementing a JAX-RS interface with VT annotation on the method + // - ResourceOnClassImpl ("/itfOnClass"): bean implementing a JAX-RS interface with VT annotation on the class + for (String url : Arrays.asList("/", "itf", "itfOnClass")) { + RestAssured.get(url).then() + .assertThat().statusCode(200) + .body(is("hello-1")); + RestAssured.get(url).then() + .assertThat().statusCode(200) + // Same value - request scoped bean + .body(is("hello-1")); + } } @Test void testPost() { - var body1 = UUID.randomUUID().toString(); - var body2 = UUID.randomUUID().toString(); - RestAssured - .given().body(body1) - .post().then() - .assertThat().statusCode(200) - .body(is(body1 + "-1")); - RestAssured - .given().body(body2) - .post().then() - .assertThat().statusCode(200) - // Same value - request scoped bean - .body(is(body2 + "-1")); + // test all variations: + // - MyResource ("/"): simple JAX-RS bean + // - ResourceImpl ("/itf"): bean implementing a JAX-RS interface with VT annotation on the method + // - ResourceOnClassImpl ("/itfOnClass"): bean implementing a JAX-RS interface with VT annotation on the class + for (String url : Arrays.asList("/", "itf", "itfOnClass")) { + var body1 = UUID.randomUUID().toString(); + var body2 = UUID.randomUUID().toString(); + RestAssured + .given().body(body1) + .post(url).then() + .assertThat().statusCode(200) + .body(is(body1 + "-1")); + RestAssured + .given().body(body2) + .post(url).then() + .assertThat().statusCode(200) + // Same value - request scoped bean + .body(is(body2 + "-1")); + } } @Test