Skip to content

Commit

Permalink
Support RunOnVirtualThread annotations on beans implementing a JAX-RS…
Browse files Browse the repository at this point in the history
… interface
  • Loading branch information
danielbobbert committed Dec 28, 2024
1 parent 6ce7dcf commit fedbd80
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<MethodInfo>() {
@Override
public boolean test(MethodInfo method) {
Expand All @@ -19,7 +24,29 @@ public boolean test(MethodInfo method) {
if (method.declaringClass().hasDeclaredAnnotation(ResteasyReactiveDotNames.PATH)) {
return true;
}
if (isJaxrsResourceMethod(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 && isJaxrsResourceMethod(overriddenMethodInfo)) {
return true;
}
}
}

return false;
}

private boolean isJaxrsResourceMethod(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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@ private ResourceMethod createResourceMethod(ClassInfo currentClassInfo, ClassInf
}
Set<String> 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())) {
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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<AnnotationTarget, AnnotationInstance> runOnVirtualThreadAnnotation = getInheritableAnnotation(info,
RUN_ON_VIRTUAL_THREAD);

Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.hamcrest.Matchers.is;

import java.util.Arrays;
import java.util.UUID;

import org.junit.jupiter.api.Test;
Expand All @@ -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
Expand Down

0 comments on commit fedbd80

Please sign in to comment.