From d23ca45cd8f93f38492695c55649c55580113b87 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 27 Jul 2020 15:26:06 +0200 Subject: [PATCH] Reactive routes - register return/body types for reflection if needed - resolves #10893 --- .github/workflows/ci-actions.yml | 1 + .../web/deployment/VertxWebProcessor.java | 89 +++++++++----- integration-tests/pom.xml | 3 +- integration-tests/vertx-web/pom.xml | 110 ++++++++++++++++++ .../io/quarkus/it/vertx/SimpleEndpoint.java | 82 +++++++++++++ .../io/quarkus/it/vertx/SimpleEndpointIT.java | 8 ++ .../it/vertx/SimpleEndpointTestCase.java | 30 +++++ 7 files changed, 290 insertions(+), 33 deletions(-) create mode 100644 integration-tests/vertx-web/pom.xml create mode 100644 integration-tests/vertx-web/src/main/java/io/quarkus/it/vertx/SimpleEndpoint.java create mode 100644 integration-tests/vertx-web/src/test/java/io/quarkus/it/vertx/SimpleEndpointIT.java create mode 100644 integration-tests/vertx-web/src/test/java/io/quarkus/it/vertx/SimpleEndpointTestCase.java diff --git a/.github/workflows/ci-actions.yml b/.github/workflows/ci-actions.yml index cc4bee4d3bc5c..92d5f24c06b3b 100644 --- a/.github/workflows/ci-actions.yml +++ b/.github/workflows/ci-actions.yml @@ -517,6 +517,7 @@ jobs: resteasy-mutiny vertx vertx-http + vertx-web vertx-graphql virtual-http rest-client diff --git a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java index cecbf04fb9057..c9c990fed2a79 100644 --- a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java +++ b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java @@ -63,6 +63,7 @@ import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem; import io.quarkus.gizmo.AssignableResultHandle; import io.quarkus.gizmo.BranchResult; import io.quarkus.gizmo.BytecodeCreator; @@ -190,6 +191,7 @@ void addAdditionalRoutes( List routeFilterBusinessMethods, BuildProducer generatedClass, BuildProducer reflectiveClasses, + BuildProducer reflectiveHierarchy, io.quarkus.vertx.http.deployment.BodyHandlerBuildItem bodyHandler, BuildProducer routeProducer, BuildProducer filterProducer, @@ -233,7 +235,7 @@ void addAdditionalRoutes( if (routeHandler == null) { String handlerClass = generateHandler(new HandlerDescriptor(businessMethod.getMethod()), businessMethod.getBean(), businessMethod.getMethod(), classOutput, transformedAnnotations, - routeString); + routeString, reflectiveHierarchy); reflectiveClasses.produce(new ReflectiveClassBuildItem(false, false, handlerClass)); routeHandler = recorder.createHandler(handlerClass); routeHandlers.put(routeString, routeHandler); @@ -314,7 +316,7 @@ void addAdditionalRoutes( for (AnnotatedRouteFilterBuildItem filterMethod : routeFilterBusinessMethods) { String handlerClass = generateHandler(new HandlerDescriptor(filterMethod.getMethod()), filterMethod.getBean(), filterMethod.getMethod(), classOutput, transformedAnnotations, - filterMethod.getRouteFilter().toString(true)); + filterMethod.getRouteFilter().toString(true), reflectiveHierarchy); reflectiveClasses.produce(new ReflectiveClassBuildItem(false, false, handlerClass)); Handler routingHandler = recorder.createHandler(handlerClass); AnnotationValue priorityValue = filterMethod.getRouteFilter().value(); @@ -405,7 +407,8 @@ private void validateRouteMethod(BeanInfo bean, MethodInfo method, TransformedAn } private String generateHandler(HandlerDescriptor desc, BeanInfo bean, MethodInfo method, ClassOutput classOutput, - TransformedAnnotationsBuildItem transformedAnnotations, String hashSuffix) { + TransformedAnnotationsBuildItem transformedAnnotations, String hashSuffix, + BuildProducer reflectiveHierarchy) { String baseName; if (bean.getImplClazz().enclosingClass() != null) { @@ -442,7 +445,8 @@ private String generateHandler(HandlerDescriptor desc, BeanInfo bean, MethodInfo } implementConstructor(bean, invokerCreator, beanField, contextField, containerField); - implementInvoke(desc, bean, method, invokerCreator, beanField, contextField, containerField, transformedAnnotations); + implementInvoke(desc, bean, method, invokerCreator, beanField, contextField, containerField, transformedAnnotations, + reflectiveHierarchy); invokerCreator.close(); return generatedName.replace('/', '.'); @@ -477,7 +481,8 @@ void implementConstructor(BeanInfo bean, ClassCreator invokerCreator, FieldCreat void implementInvoke(HandlerDescriptor descriptor, BeanInfo bean, MethodInfo method, ClassCreator invokerCreator, FieldCreator beanField, FieldCreator contextField, FieldCreator containerField, - TransformedAnnotationsBuildItem transformedAnnotations) { + TransformedAnnotationsBuildItem transformedAnnotations, + BuildProducer reflectiveHierarchy) { // The descriptor is: void invoke(RoutingContext rc) MethodCreator invoke = invokerCreator.getMethodCreator("invoke", void.class, RoutingContext.class); ResultHandle beanHandle = invoke.readInstanceField(beanField.getFieldDescriptor(), invoke.getThis()); @@ -531,7 +536,7 @@ void implementInvoke(HandlerDescriptor descriptor, BeanInfo bean, MethodInfo met Set paramAnnotations = Annotations.getParameterAnnotations(transformedAnnotations, method, idx); // At this point we can be sure that a matching injector is available paramHandles[idx] = getMatchingInjectors(paramType, paramAnnotations).get(0).getResultHandle(method, paramType, - paramAnnotations, routingContext, invoke, idx); + paramAnnotations, routingContext, invoke, idx, reflectiveHierarchy); parameterTypes[idx] = paramType.name().toString(); idx++; } @@ -563,6 +568,9 @@ void implementInvoke(HandlerDescriptor descriptor, BeanInfo bean, MethodInfo met ResultHandle sub = invoke.invokeInterfaceMethod(Methods.UNI_SUBSCRIBE, res); invoke.invokeVirtualMethod(Methods.UNI_SUBSCRIBE_WITH, sub, successCallback.getInstance(), failureCallback); + + registerForReflection(descriptor.getContentType(), reflectiveHierarchy); + } else if (descriptor.isReturningMulti()) { // 3 cases - regular multi vs. sse multi vs. json array multi, we need to check the type. @@ -581,6 +589,8 @@ void implementInvoke(HandlerDescriptor descriptor, BeanInfo bean, MethodInfo met isRegular.close(); isNotSSE.close(); + registerForReflection(descriptor.getContentType(), reflectiveHierarchy); + } else if (descriptor.getContentType() != null) { // The method returns "something" in a synchronous manner, write it into the response ResultHandle response = invoke.invokeInterfaceMethod(Methods.RESPONSE, routingContext); @@ -589,6 +599,8 @@ void implementInvoke(HandlerDescriptor descriptor, BeanInfo bean, MethodInfo met // If the method returns an object, the result is mapped to JSON and written into the response ResultHandle content = getContentToWrite(descriptor, response, res, invoke); invoke.invokeInterfaceMethod(end, response, content); + + registerForReflection(descriptor.getContentType(), reflectiveHierarchy); } // Destroy dependent instance afterwards @@ -599,6 +611,17 @@ void implementInvoke(HandlerDescriptor descriptor, BeanInfo bean, MethodInfo met invoke.returnValue(null); } + private static final List TYPES_IGNORED_FOR_REFLECTION = Arrays.asList(io.quarkus.arc.processor.DotNames.STRING, + DotNames.BUFFER, DotNames.JSON_ARRAY, DotNames.JSON_OBJECT); + + private static void registerForReflection(Type contentType, + BuildProducer reflectiveHierarchy) { + if (TYPES_IGNORED_FOR_REFLECTION.contains(contentType.name())) { + return; + } + reflectiveHierarchy.produce(new ReflectiveHierarchyBuildItem(contentType, TYPES_IGNORED_FOR_REFLECTION::contains)); + } + private void handleRegularMulti(HandlerDescriptor descriptor, BytecodeCreator writer, ResultHandle rc, ResultHandle res) { // The method returns a Multi. @@ -896,8 +919,8 @@ static List initParamInjectors() { .resultHandleProvider(new ResultHandleProvider() { @Override public ResultHandle get(MethodInfo method, Type paramType, Set annotations, - ResultHandle routingContext, - MethodCreator invoke, int position) { + ResultHandle routingContext, MethodCreator invoke, int position, + BuildProducer reflectiveHierarchy) { return routingContext; } }).build()); @@ -906,8 +929,8 @@ public ResultHandle get(MethodInfo method, Type paramType, Set annotations, - ResultHandle routingContext, - MethodCreator invoke, int position) { + ResultHandle routingContext, MethodCreator invoke, int position, + BuildProducer reflectiveHierarchy) { return invoke.newInstance( MethodDescriptor .ofConstructor(io.vertx.reactivex.ext.web.RoutingContext.class, RoutingContext.class), @@ -919,8 +942,8 @@ public ResultHandle get(MethodInfo method, Type paramType, Set annotations, - ResultHandle routingContext, - MethodCreator invoke, int position) { + ResultHandle routingContext, MethodCreator invoke, int position, + BuildProducer reflectiveHierarchy) { return invoke .newInstance(MethodDescriptor.ofConstructor(RoutingExchangeImpl.class, RoutingContext.class), routingContext); @@ -931,8 +954,8 @@ public ResultHandle get(MethodInfo method, Type paramType, Set annotations, - ResultHandle routingContext, - MethodCreator invoke, int position) { + ResultHandle routingContext, MethodCreator invoke, int position, + BuildProducer reflectiveHierarchy) { return invoke .invokeInterfaceMethod(Methods.REQUEST, routingContext); @@ -943,8 +966,8 @@ public ResultHandle get(MethodInfo method, Type paramType, Set annotations, - ResultHandle routingContext, - MethodCreator invoke, int position) { + ResultHandle routingContext, MethodCreator invoke, int position, + BuildProducer reflectiveHierarchy) { return invoke .invokeInterfaceMethod(Methods.RESPONSE, routingContext); @@ -955,8 +978,8 @@ public ResultHandle get(MethodInfo method, Type paramType, Set annotations, - ResultHandle routingContext, - MethodCreator invoke, int position) { + ResultHandle routingContext, MethodCreator invoke, int position, + BuildProducer reflectiveHierarchy) { return invoke.newInstance( MethodDescriptor .ofConstructor(io.vertx.reactivex.core.http.HttpServerRequest.class, @@ -972,8 +995,8 @@ public ResultHandle get(MethodInfo method, Type paramType, Set annotations, - ResultHandle routingContext, - MethodCreator invoke, int position) { + ResultHandle routingContext, MethodCreator invoke, int position, + BuildProducer reflectiveHierarchy) { return invoke.newInstance( MethodDescriptor .ofConstructor(io.vertx.reactivex.core.http.HttpServerResponse.class, @@ -993,8 +1016,8 @@ public ResultHandle get(MethodInfo method, Type paramType, Set annotations, - ResultHandle routingContext, - MethodCreator invoke, int position) { + ResultHandle routingContext, MethodCreator invoke, int position, + BuildProducer reflectiveHierarchy) { AnnotationValue paramAnnotationValue = Annotations .find(annotations, PARAM).value(); String paramName = paramAnnotationValue != null ? paramAnnotationValue.asString() : null; @@ -1035,8 +1058,8 @@ public ResultHandle get(MethodInfo method, Type paramType, Set annotations, - ResultHandle routingContext, - MethodCreator invoke, int position) { + ResultHandle routingContext, MethodCreator invoke, int position, + BuildProducer reflectiveHierarchy) { AnnotationValue paramAnnotationValue = Annotations .find(annotations, DotNames.HEADER).value(); String paramName = paramAnnotationValue != null ? paramAnnotationValue.asString() : null; @@ -1079,8 +1102,8 @@ public ResultHandle get(MethodInfo method, Type paramType, Set annotations, - ResultHandle routingContext, - MethodCreator invoke, int position) { + ResultHandle routingContext, MethodCreator invoke, int position, + BuildProducer reflectiveHierarchy) { if (paramType.name().equals(io.quarkus.arc.processor.DotNames.STRING)) { return invoke.invokeInterfaceMethod(Methods.GET_BODY_AS_STRING, routingContext); } else if (paramType.name().equals(DotNames.BUFFER)) { @@ -1105,8 +1128,9 @@ public ResultHandle get(MethodInfo method, Type paramType, Set annotations, - ResultHandle routingContext, - MethodCreator invoke, int position) { + ResultHandle routingContext, MethodCreator invoke, int position, + BuildProducer reflectiveHierarchy) { + registerForReflection(paramType, reflectiveHierarchy); AssignableResultHandle ret = invoke.createVariable(Object.class); ResultHandle bodyAsJson = invoke.invokeInterfaceMethod(Methods.GET_BODY_AS_JSON, routingContext); @@ -1199,9 +1223,9 @@ boolean matches(Type paramType, Set paramAnnotations) { } ResultHandle getResultHandle(MethodInfo method, Type paramType, Set annotations, - ResultHandle routingContext, - MethodCreator invoke, int position) { - return provider.get(method, paramType, annotations, routingContext, invoke, position); + ResultHandle routingContext, MethodCreator invoke, int position, + BuildProducer reflectiveHierarchy) { + return provider.get(method, paramType, annotations, routingContext, invoke, position, reflectiveHierarchy); } @Override @@ -1265,7 +1289,8 @@ ParameterInjector build() { interface ResultHandleProvider { ResultHandle get(MethodInfo method, Type paramType, Set annotations, - ResultHandle routingContext, MethodCreator invoke, int position); + ResultHandle routingContext, MethodCreator invoke, int position, + BuildProducer reflectiveHierarchy); } diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index c6146c8e36e42..38cbc9147b337 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -56,6 +56,7 @@ hibernate-tenancy hibernate-orm-envers vertx-http + vertx-web vertx spring-di spring-web @@ -129,7 +130,7 @@ ide-launcher elasticsearch-rest-client elasticsearch-rest-high-level-client - + grpc-plain-text grpc-tls diff --git a/integration-tests/vertx-web/pom.xml b/integration-tests/vertx-web/pom.xml new file mode 100644 index 0000000000000..d90e7d5855d1f --- /dev/null +++ b/integration-tests/vertx-web/pom.xml @@ -0,0 +1,110 @@ + + + 4.0.0 + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + ../ + + + quarkus-integration-test-vertx-web + Quarkus - Integration Tests - Vert.x Web + + + + io.quarkus + quarkus-vertx-web + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + native-image + + + native + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${native.surefire.skip} + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + io.quarkus + quarkus-maven-plugin + + + native-image + + native-image + + + true + true + + + -H:EnableURLProtocols=http,https + + ${graalvmHome} + true + + + + + + + + + diff --git a/integration-tests/vertx-web/src/main/java/io/quarkus/it/vertx/SimpleEndpoint.java b/integration-tests/vertx-web/src/main/java/io/quarkus/it/vertx/SimpleEndpoint.java new file mode 100644 index 0000000000000..284d6fe71f723 --- /dev/null +++ b/integration-tests/vertx-web/src/main/java/io/quarkus/it/vertx/SimpleEndpoint.java @@ -0,0 +1,82 @@ +package io.quarkus.it.vertx; + +import static io.vertx.core.http.HttpMethod.GET; +import static io.vertx.core.http.HttpMethod.POST; + +import io.quarkus.vertx.web.Body; +import io.quarkus.vertx.web.Route; +import io.quarkus.vertx.web.RouteBase; +import io.smallrye.mutiny.Uni; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.JsonObject; + +@RouteBase(path = "/simple") +public class SimpleEndpoint { + + @Route(path = "person", methods = GET) + public Person getPerson() { + Person person = new Person(); + person.setName("Jan"); + return person; + } + + @Route(path = "pet", methods = GET) + public Uni getPet() { + Pet pet = new Pet(); + pet.setName("Jack"); + return Uni.createFrom().item(pet); + } + + @Route(path = "pong", methods = GET) + public JsonObject getPong() { + return new JsonObject().put("name", "ping"); + } + + @Route(path = "data", methods = POST, produces = "application/json") + public Buffer createData(@Body Data data) { + data.setName(data.getName() + data.getName()); + return JsonObject.mapFrom(data).toBuffer(); + } + + public static class Person { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } + + public static class Pet { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } + + public static class Data { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } +} diff --git a/integration-tests/vertx-web/src/test/java/io/quarkus/it/vertx/SimpleEndpointIT.java b/integration-tests/vertx-web/src/test/java/io/quarkus/it/vertx/SimpleEndpointIT.java new file mode 100644 index 0000000000000..314308b74ed09 --- /dev/null +++ b/integration-tests/vertx-web/src/test/java/io/quarkus/it/vertx/SimpleEndpointIT.java @@ -0,0 +1,8 @@ +package io.quarkus.it.vertx; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +public class SimpleEndpointIT extends SimpleEndpointTestCase { + +} diff --git a/integration-tests/vertx-web/src/test/java/io/quarkus/it/vertx/SimpleEndpointTestCase.java b/integration-tests/vertx-web/src/test/java/io/quarkus/it/vertx/SimpleEndpointTestCase.java new file mode 100644 index 0000000000000..a678aa300cc84 --- /dev/null +++ b/integration-tests/vertx-web/src/test/java/io/quarkus/it/vertx/SimpleEndpointTestCase.java @@ -0,0 +1,30 @@ +package io.quarkus.it.vertx; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class SimpleEndpointTestCase { + + @Test + public void testEndpoint() throws Exception { + when().get("/simple/person").then().statusCode(200) + .body("name", is("Jan")) + .header("content-type", "application/json"); + when().get("/simple/pet").then().statusCode(200) + .body("name", is("Jack")) + .header("content-type", "application/json"); + when().get("/simple/pong").then().statusCode(200) + .body("name", is("ping")) + .header("content-type", "application/json"); + given().body("{\"name\":\"pi\"}").post("/simple/data").then().statusCode(200) + .body("name", is("pipi")) + .header("content-type", "application/json"); + } + +}