Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the vertx-graphql extension work in native mode #5253

Merged
merged 4 commits into from
Nov 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ci-templates/stages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -310,11 +310,12 @@ stages:
parameters:
poolSettings: ${{parameters.poolSettings}}
expectUseVMs: ${{parameters.expectUseVMs}}
timeoutInMinutes: 25
timeoutInMinutes: 30
modules:
- resteasy-jackson
- vertx
- vertx-http
- vertx-graphql
- virtual-http
name: http

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.quarkus.deployment.builditem.nativeimage;

import io.quarkus.builder.item.MultiBuildItem;

/**
* A build item that indicates that directory resources should be included in the native image
*/
public final class NativeImageResourceDirectoryBuildItem extends MultiBuildItem {

private final String path;

public NativeImageResourceDirectoryBuildItem(String path) {
this.path = path;
}

public String getPath() {
return path;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@

import static io.quarkus.gizmo.MethodDescriptor.ofMethod;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;

import org.graalvm.nativeimage.ImageSingletons;
Expand All @@ -22,10 +31,12 @@

import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.DeploymentClassLoaderBuildItem;
import io.quarkus.deployment.builditem.GeneratedNativeImageClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceDirectoryBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveFieldBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem;
Expand Down Expand Up @@ -56,6 +67,32 @@ public class NativeImageAutoFeatureStep {
static final String DYNAMIC_PROXY_REGISTRY = "com.oracle.svm.core.jdk.proxy.DynamicProxyRegistry";
static final String LOCALIZATION_SUPPORT = "com.oracle.svm.core.jdk.LocalizationSupport";

@BuildStep
List<NativeImageResourceBuildItem> registerPackageResources(
List<NativeImageResourceDirectoryBuildItem> nativeImageResourceDirectories,
DeploymentClassLoaderBuildItem classLoader)
throws IOException, URISyntaxException {
List<NativeImageResourceBuildItem> resources = new ArrayList<>();

for (NativeImageResourceDirectoryBuildItem nativeImageResourceDirectory : nativeImageResourceDirectories) {
String path = classLoader.getClassLoader().getResource(nativeImageResourceDirectory.getPath()).getPath();
File resourceFile = Paths.get(new URL(path.substring(0, path.indexOf("!"))).toURI()).toFile();
try (JarFile jarFile = new JarFile(resourceFile)) {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String resourceName = entry.getName();
if (!entry.isDirectory() && resourceName.startsWith(nativeImageResourceDirectory.getPath())
&& !resourceName.endsWith(".class")) {
resources.add(new NativeImageResourceBuildItem(resourceName));
}
}
}
}

return resources;
}

@BuildStep
void generateFeature(BuildProducer<GeneratedNativeImageClassBuildItem> nativeImageClass,
List<RuntimeInitializedClassBuildItem> runtimeInitializedClassBuildItems,
Expand Down
29 changes: 16 additions & 13 deletions docs/src/main/asciidoc/writing-extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1642,6 +1642,9 @@ executable. Some of these build items are listed below:
`io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem`::
Includes static resources into the native executable.

`io.quarkus.deployment.builditem.nativeimage.NativeImageResourceDirectoryBuildItem`::
Includes directory's static resources into the native executable.

`io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem`::
A class that will be reinitialized at runtime by Substrate. This will result in the static initializer running twice.

Expand Down Expand Up @@ -2099,16 +2102,16 @@ public final class MyExtProcessor {
@BuildStep
void registerNativeImageReources(BuildProducer<ServiceProviderBuildItem> services) {
String service = "META-INF/services/" + io.quarkus.SomeService.class.getName();

// find out all the implementation classes listed in the service files
Set<String> implementations =
Set<String> implementations =
ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(),
service);

// register every listed implementation class so they can be instantiated
// register every listed implementation class so they can be instantiated
// in native-image at run-time
services.produce(
new ServiceProviderBuildItem(io.quarkus.SomeService.class.getName(),
new ServiceProviderBuildItem(io.quarkus.SomeService.class.getName(),
implementations.toArray(new String[0])));
}
}
Expand All @@ -2130,13 +2133,13 @@ public final class MyExtProcessor {
void registerNativeImageReources(BuildProducer<NativeImageResourceBuildItem> resource,
BuildProducer<ReflectiveClassBuildItem> reflectionClasses) {
String service = "META-INF/services/" + io.quarkus.SomeService.class.getName();

// register the service file so it is visible in native-image
resource.produce(new NativeImageResourceBuildItem(service));
// register every listed implementation class so they can be inspected/instantiated

// register every listed implementation class so they can be inspected/instantiated
// in native-image at run-time
Set<String> implementations =
Set<String> implementations =
ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(),
service);
reflectionClasses.produce(
Expand All @@ -2148,7 +2151,7 @@ public final class MyExtProcessor {
While this is the easiest way to get your services running natively, it's less efficient than scanning the implementation
classes at build time and generating code that registers them at static-init time instead of relying on reflection.

You can achieve that by adapting the previous build step to use a static-init recorder instead of registering
You can achieve that by adapting the previous build step to use a static-init recorder instead of registering
classes for reflection:

[source,java]
Expand All @@ -2157,19 +2160,19 @@ public final class MyExtProcessor {

@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void registerNativeImageReources(RecorderContext recorderContext,
void registerNativeImageReources(RecorderContext recorderContext,
SomeServiceRecorder recorder) {
String service = "META-INF/services/" + io.quarkus.SomeService.class.getName();

// read the implementation classes
Collection<Class<? extends io.quarkus.SomeService>> implementationClasses = new LinkedHashSet<>();
Set<String> implementations = ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(),
service);
for(String implementation : implementations) {
implementationClasses.add((Class<? extends io.quarkus.SomeService>)
implementationClasses.add((Class<? extends io.quarkus.SomeService>)
recorderContext.classProxy(implementation));
}

// produce a static-initializer with those classes
recorder.configure(implementationClasses);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ static void startWiser() {
@AfterAll
static void stopWiser() {
wiser.stop();
vertx.close();
vertx.close().toCompletableFuture().join();
}

@BeforeEach
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ static void start() {

@AfterAll
static void stop() {
vertx.close();
vertx.close().toCompletableFuture().join();
}

@BeforeEach
Expand Down
10 changes: 10 additions & 0 deletions extensions/vertx-graphql/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-graphql</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.quarkus.vertx.graphql.deployment;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigRoot;

@ConfigRoot
public final class VertxGraphqlConfig {
/**
* GraphQL UI configuration
*/
@ConfigItem
VertxGraphqlUiConfig ui;

@ConfigGroup
public static class VertxGraphqlUiConfig {
/**
* If GraphQL UI should be included every time. By default this is only included when the application is running
* in dev mode.
*/
@ConfigItem(defaultValue = "false")
boolean alwaysInclude;

/**
* The path where GraphQL UI is available.
* <p>
* The value `/` is not allowed as it blocks the application from serving anything else.
*/
@ConfigItem(defaultValue = "/graphql-ui")
String path;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
package io.quarkus.vertx.graphql.deployment;

import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceDirectoryBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.vertx.graphql.runtime.VertxGraphqlRecorder;
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import io.quarkus.vertx.http.deployment.WebsocketSubProtocolsBuildItem;
import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem;
import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.graphql.impl.GraphQLBatch;
import io.vertx.ext.web.handler.graphql.impl.GraphQLInputDeserializer;
import io.vertx.ext.web.handler.graphql.impl.GraphQLQuery;

class VertxGraphqlProcessor {
private static Pattern TRAILING_SLASH_SUFFIX_REGEX = Pattern.compile("/+$");

@BuildStep
FeatureBuildItem feature() {
Expand All @@ -15,4 +36,39 @@ FeatureBuildItem feature() {
WebsocketSubProtocolsBuildItem websocketSubProtocols() {
return new WebsocketSubProtocolsBuildItem("graphql-ws");
}

@BuildStep
List<ReflectiveClassBuildItem> registerForReflection() {
return Arrays.asList(
new ReflectiveClassBuildItem(true, true, GraphQLInputDeserializer.class.getName()),
new ReflectiveClassBuildItem(true, true, GraphQLBatch.class.getName()),
new ReflectiveClassBuildItem(true, true, GraphQLQuery.class.getName()));
}

@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void registerVertxGraphqlUI(VertxGraphqlRecorder recorder,
BuildProducer<NativeImageResourceDirectoryBuildItem> nativeResourcesProducer, VertxGraphqlConfig config,
LaunchModeBuildItem launchMode, BuildProducer<NotFoundPageDisplayableEndpointBuildItem> displayableEndpoints,
BuildProducer<RouteBuildItem> routes) {

boolean includeVertxGraphqlUi = launchMode.getLaunchMode().isDevOrTest() || config.ui.alwaysInclude;
if (!includeVertxGraphqlUi) {
return;
}

Matcher matcher = TRAILING_SLASH_SUFFIX_REGEX.matcher(config.ui.path);
String path = matcher.replaceAll("");
if (path.isEmpty()) {
throw new ConfigurationException(
"quarkus.vertx-graphql.ui.path was set to \"" + config.ui.path
+ "\", this is not allowed as it blocks the application from serving anything else.");
}

Handler<RoutingContext> handler = recorder.handler(path);
machi1990 marked this conversation as resolved.
Show resolved Hide resolved
routes.produce(new RouteBuildItem(path, handler));
routes.produce(new RouteBuildItem(path + "/*", handler));
displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem(path + "/"));
nativeResourcesProducer.produce(new NativeImageResourceDirectoryBuildItem("io/vertx/ext/web/handler/graphiql"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.quarkus.vertx.graphql.deployment;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.test.QuarkusUnitTest;

public class ErroneousConfigTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setExpectedException(ConfigurationException.class)
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addAsResource(new StringAsset("quarkus.vertx-graphql.ui.path=/\n"), "application.properties"));

@Test
public void shouldNotStartApplicationIfPathIsASlash() {
Assertions.fail();
machi1990 marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.quarkus.vertx.graphql.deployment;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;

public class ServingUIFromCustomPathTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addAsResource(new StringAsset("quarkus.vertx-graphql.ui.path=/custom\n"), "application.properties"));

@Test
public void shouldServeVertxGraphqlUiFromCustomPath() {
RestAssured.when().get("/custom").then().statusCode(200);
}
}
Loading