forked from quarkusio/quarkus
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add validation of execution model annotations
The `@Blocking`, `@NonBlocking` and `@RunOnVirtualThread` annotations may only be used on "entrypoint" methods (methods invoked by various frameworks in Quarkus). Using these annotations on methods that can only be invoked by application code is invalid.
- Loading branch information
Showing
13 changed files
with
437 additions
and
0 deletions.
There are no files selected for viewing
28 changes: 28 additions & 0 deletions
28
...java/io/quarkus/deployment/execannotations/ExecutionModelAnnotationsAllowedBuildItem.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}). | ||
* <p> | ||
* 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<MethodInfo> predicate; | ||
|
||
public ExecutionModelAnnotationsAllowedBuildItem(Predicate<MethodInfo> predicate) { | ||
this.predicate = Objects.requireNonNull(predicate); | ||
} | ||
|
||
public boolean matches(MethodInfo method) { | ||
return predicate.test(method); | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
.../src/main/java/io/quarkus/deployment/execannotations/ExecutionModelAnnotationsConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
* <p> | ||
* An execution model annotation is {@code @Blocking}, {@code @NonBlocking} and {@code @RunOnVirtualThread}. | ||
* These annotations may only be used on "entrypoint" methods (methods invoked by various frameworks in Quarkus); | ||
* using them on methods that can only be invoked by application code 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, | ||
} | ||
} |
94 changes: 94 additions & 0 deletions
94
...c/main/java/io/quarkus/deployment/execannotations/ExecutionModelAnnotationsProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
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<GeneratedClassBuildItem> ignored, // only to make sure this build step is executed | ||
ExecutionModelAnnotationsConfig config, CombinedIndexBuildItem index, | ||
List<ExecutionModelAnnotationsAllowedBuildItem> 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 \"entrypoint\" methods (methods invoked by various frameworks in Quarkus)\n"); | ||
message.append("Using the @Blocking, @NonBlocking and @RunOnVirtualThread annotations on methods " | ||
+ "that can only be invoked by application code is invalid"); | ||
if (config.detectionMode() == ExecutionModelAnnotationsConfig.Mode.WARN) { | ||
log.warn(message); | ||
} else { | ||
throw new IllegalStateException(message.toString()); | ||
} | ||
} | ||
} | ||
|
||
private void doCheck(StringBuilder message, IndexView index, | ||
List<ExecutionModelAnnotationsAllowedBuildItem> predicates, DotName annotationName) { | ||
|
||
List<String> 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(); | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
...nsions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcMethodsProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<MethodInfo>() { | ||
@Override | ||
public boolean test(MethodInfo method) { | ||
return method.declaringClass().hasDeclaredAnnotation(GrpcDotNames.GRPC_SERVICE); | ||
} | ||
}); | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
...loyment/src/main/java/io/quarkus/vertx/web/deployment/ReactiveRoutesMethodsProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<MethodInfo>() { | ||
@Override | ||
public boolean test(MethodInfo method) { | ||
return method.hasDeclaredAnnotation(DotNames.ROUTE) | ||
|| method.hasDeclaredAnnotation(DotNames.ROUTES); | ||
} | ||
}); | ||
} | ||
} |
34 changes: 34 additions & 0 deletions
34
...routes/deployment/src/test/java/io/quarkus/execannotations/ExecAnnotationInvalidTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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!"; | ||
} | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
...e-routes/deployment/src/test/java/io/quarkus/execannotations/ExecAnnotationValidTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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!"; | ||
} | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
...deployment/src/main/java/io/quarkus/resteasy/common/deployment/JaxrsMethodsProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<MethodInfo>() { | ||
@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); | ||
} | ||
}); | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
...t/src/main/java/io/quarkus/resteasy/reactive/common/deployment/JaxrsMethodsProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<MethodInfo>() { | ||
@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); | ||
} | ||
}); | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
...r/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerMethodsProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<MethodInfo>() { | ||
@Override | ||
public boolean test(MethodInfo method) { | ||
return method.hasDeclaredAnnotation(SchedulerDotNames.SCHEDULED_NAME) | ||
|| method.hasDeclaredAnnotation(SchedulerDotNames.SCHEDULES_NAME); | ||
} | ||
}); | ||
} | ||
} |
31 changes: 31 additions & 0 deletions
31
...loyment/src/main/java/io/quarkus/smallrye/graphql/deployment/GraphqlMethodsProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<MethodInfo>() { | ||
@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); | ||
} | ||
}); | ||
} | ||
} |
Oops, something went wrong.