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

Fix #253 Build time property to register classes for reflection #987

Merged
merged 1 commit into from
Mar 26, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/modules/ROOT/pages/native-mode.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,49 @@ in Quarkus documentation.
== Embedding resource in native executable

Resources needed at runtime need to be explicitly embedded in the built native executable. In such situations, the `include-patterns` and `exclude-patterns` configurations could be set in `application.properties` as demonstrated below:

[source,properties]
----
quarkus.camel.native.resources.include-patterns = docs/*,images/*
quarkus.camel.native.resources.exclude-patterns = docs/ignored.adoc,images/ignored.png
----

In the example above, resources named _docs/included.adoc_ and _images/included.png_ would be embedded in the native executable while _docs/ignored.adoc_ and _images/ignored.png_ would not.

`include-patterns` and `exclude-patterns` are list of comma separated link:https://github.com/apache/camel/blob/master/core/camel-util/src/main/java/org/apache/camel/util/AntPathMatcher.java[Ant-path style patterns].
At the end of the day, resources matching `include-patterns` are marked for inclusion at the exception of resources matching `exclude-patterns`.

[[reflection]]
== Registering classes for reflection

By default, dynamic reflection is not available in native mode. Classes for which reflective access is needed have to be
registered for reflection at compile time.

In many cases, application developers do not need to care because Quarkus extensions are able to detect the classes that
require the reflection and register them automatically.

However, in some situations Quarkus extensions may miss some classes and it is up to the application developer to
register them. There are two ways to do that:

1. The `https://quarkus.io/guides/writing-native-applications-tips#alternative-with-registerforreflection[@io.quarkus.runtime.annotations.RegisterForReflection]`
annotation can be used to register classes on which it is used, or it can also register third party classes via
its `targets` attribute.

2. The `quarkus.camel.native.reflection` options in `application.properties`:
+
[source,properties]
----
quarkus.camel.native.reflection.include-patterns = org.apache.commons.lang3.tuple.*
quarkus.camel.native.reflection.exclude-patterns = org.apache.commons.lang3.tuple.*Triple
----
+
For these options to work properly, the artifacts containing the selected classes
must either contain a Jandex index ({@code META-INF/jandex.idx}) or they must
be registered for indexing using the {@code quarkus.index-dependency.*} options
in {@code application.properties} - e.g.
+
[source,properties]
----
quarkus.index-dependency.commons-lang3.group-id = org.apache.commons
quarkus.index-dependency.commons-lang3.artifact-id = commons-lang3
----
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import org.apache.camel.impl.engine.DefaultDataFormatResolver;
import org.apache.camel.impl.engine.DefaultLanguageResolver;
import org.apache.camel.quarkus.core.CamelConfig;
import org.apache.camel.quarkus.core.CamelConfig.ReflectionConfig;
import org.apache.camel.quarkus.core.CamelConfig.ResourcesConfig;
import org.apache.camel.quarkus.core.Flags;
import org.apache.camel.quarkus.core.deployment.util.PathFilter;
Expand All @@ -63,7 +64,7 @@

import static org.apache.commons.lang3.ClassUtils.getPackageName;

class NativeImageProcessor {
public class NativeImageProcessor {
private static final Logger LOGGER = LoggerFactory.getLogger(NativeImageProcessor.class);

/*
Expand Down Expand Up @@ -240,6 +241,40 @@ void embedSelectResourcesInNativeExecutable(CamelConfig config, ApplicationArchi
});
}
}

@BuildStep
void reflection(CamelConfig config, ApplicationArchivesBuildItem archives,
BuildProducer<ReflectiveClassBuildItem> reflectiveClasses) {

final ReflectionConfig reflectionConfig = config.native_.reflection;
if (!reflectionConfig.includePatterns.isPresent()) {
LOGGER.debug("No classes registered for reflection via quarkus.camel.native.reflection.include-patterns");
return;
}

LOGGER.debug("Scanning resources for native inclusion from include-patterns {}",
reflectionConfig.includePatterns.get());

final PathFilter.Builder builder = new PathFilter.Builder();
reflectionConfig.includePatterns.map(list -> list.stream()).orElseGet(Stream::empty)
.map(className -> className.replace('.', '/'))
.forEach(pathPattern -> builder.include(pathPattern));
reflectionConfig.excludePatterns.map(list -> list.stream()).orElseGet(Stream::empty)
.map(className -> className.replace('.', '/'))
.forEach(pathPattern -> builder.exclude(pathPattern));
final PathFilter pathFilter = builder.build();

for (ApplicationArchive archive : archives.getAllApplicationArchives()) {
LOGGER.debug("Scanning resources for native inclusion from archive at {}", archive.getArchiveLocation());

final Path rootPath = archive.getArchiveRoot();
String[] selectedClassNames = pathFilter.scanClassNames(rootPath, CamelSupport.safeWalk(rootPath),
Files::isRegularFile);
if (selectedClassNames.length > 0) {
reflectiveClasses.produce(new ReflectiveClassBuildItem(true, true, selectedClassNames));
}
}
}
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;

import org.apache.camel.util.AntPathMatcher;
import org.apache.camel.util.ObjectHelper;
Expand All @@ -31,6 +32,9 @@
* A utility able to filter resource paths using Ant-like includes and excludes.
*/
public class PathFilter {
private static final String CLASS_SUFFIX = ".class";
private static final int CLASS_SUFFIX_LENGTH = CLASS_SUFFIX.length();

private final AntPathMatcher matcher = new AntPathMatcher();
private final List<String> includePatterns;
private final List<String> excludePatterns;
Expand Down Expand Up @@ -83,6 +87,18 @@ public Predicate<Path> asPathPredicate() {
}
}

public String[] scanClassNames(Path rootPath, Stream<Path> pathStream, Predicate<Path> isRegularFile) {
return pathStream
.filter(isRegularFile)
.filter(path -> path.getFileName().toString().endsWith(CLASS_SUFFIX))
.map(filePath -> rootPath.relativize(filePath))
.map(relPath -> relPath.toString())
.map(stringPath -> stringPath.substring(0, stringPath.length() - CLASS_SUFFIX_LENGTH))
.filter(stringPredicate)
.map(slashClassName -> slashClassName.replace('/', '.'))
.toArray(String[]::new);
}

static String sanitize(String path) {
path = path.trim();
return (!path.isEmpty() && path.charAt(0) == '/')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Stream;

import org.jboss.jandex.DotName;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
Expand Down Expand Up @@ -96,4 +98,34 @@ static boolean isPathIncluded(String path, List<String> excludePatterns, List<St
return new PathFilter(includePatterns, excludePatterns).asStringPredicate().test(path);
}

@Test
void scanClassNames() {
final PathFilter filter = new PathFilter.Builder()
.include("org/p1/*")
.include("org/p2/**")
.exclude("org/p1/ExcludedClass")
.exclude("org/p2/excludedpackage/**")
.build();
final Path rootPath = Paths.get("/foo");
final Stream<Path> pathStream = Stream.of(
"org/p1/Class1.class",
"org/p1/Class1$Inner.class",
"org/p1/Class1.txt",
"org/p1/ExcludedClass.class",
"org/p2/excludedpackage/ExcludedClass.class",
"org/p2/excludedpackage/p/ExcludedClass.class",
"org/p2/whatever/Class2.class")
.map(rootPath::resolve);

final Predicate<Path> isRegularFile = path -> path.getFileName().toString().contains(".");
final String[] classNames = filter.scanClassNames(rootPath, pathStream, isRegularFile);

Assertions.assertArrayEquals(new String[] {
"org.p1.Class1",
"org.p1.Class1$Inner",
"org.p2.whatever.Class2"
}, classNames);

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ public static class NativeConfig {
@ConfigItem
public ResourcesConfig resources;

/**
* Register classes for reflection.
*/
@ConfigItem
public ReflectionConfig reflection;

}

@ConfigGroup
Expand Down Expand Up @@ -225,6 +231,62 @@ public static class ResourcesConfig {

}

@ConfigGroup
public static class ReflectionConfig {

/**
* A comma separated list of Ant-path style patterns to match class names
* that should be <strong>excluded</strong> from registering for reflection.
* Use the class name format as returned by the {@code java.lang.Class.getName()}
* method: package segments delimited by period {@code .} and inner classes
* by dollar sign {@code $}.
* <p>
* This option narrows down the set selected by {@link #includePatterns}.
* By default, no classes are excluded.
* <p>
* This option cannot be used to unregister classes which have been registered
* internally by Quarkus extensions.
*/
@ConfigItem
public Optional<List<String>> excludePatterns;

/**
* A comma separated list of Ant-path style patterns to match class names
* that should be registered for reflection.
* Use the class name format as returned by the {@code java.lang.Class.getName()}
* method: package segments delimited by period {@code .} and inner classes
* by dollar sign {@code $}.
* <p>
* By default, no classes are included. The set selected by this option can be
* narrowed down by {@link #excludePatterns}.
* <p>
* Note that Quarkus extensions typically register the required classes for
* reflection by themselves. This option is useful in situations when the
* built in functionality is not sufficient.
* <p>
* Note that this option enables the full reflective access for constructors,
* fields and methods. If you need a finer grained control, consider using
* <code>io.quarkus.runtime.annotations.RegisterForReflection</code> annotation
* in your Java code.
* <p>
* For this option to work properly, the artifacts containing the selected classes
* must either contain a Jandex index ({@code META-INF/jandex.idx}) or they must
* be registered for indexing using the {@code quarkus.index-dependency.*} family
* of options in {@code application.properties} - e.g.
*
* <pre>
* quarkus.index-dependency.my-dep.group-id = org.my-group
* quarkus.index-dependency.my-dep.artifact-id = my-artifact
* </pre>
*
* where {@code my-dep} is a label of your choice to tell Quarkus that
* {@code org.my-group} and with {@code my-artifact} belong together.
*/
@ConfigItem
public Optional<List<String>> includePatterns;

}

@ConfigGroup
public static class RuntimeCatalogConfig {
/**
Expand Down
4 changes: 4 additions & 0 deletions integration-tests/core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>

<!-- test dependencies -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;

import javax.enterprise.context.ApplicationScoped;
Expand All @@ -29,6 +32,7 @@
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.apache.camel.CamelContext;
import org.apache.camel.ExtendedCamelContext;
Expand Down Expand Up @@ -167,4 +171,42 @@ public String getResource(@PathParam("name") String name) throws IOException {
return IOUtils.toString(is, StandardCharsets.UTF_8);
}
}

@Path("/reflection/{className}/method/{methodName}/{value}")
@GET
@Produces(MediaType.TEXT_PLAIN)
public Response reflectMethod(@PathParam("className") String className,
@PathParam("methodName") String methodName,
@PathParam("value") String value) {
try {
final Class<?> cl = Class.forName(className);
final Object inst = cl.newInstance();
final Method method = cl.getDeclaredMethod(methodName, Object.class);
method.invoke(inst, value);
return Response.ok(inst.toString()).build();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException
| SecurityException | IllegalArgumentException | InvocationTargetException e) {
return Response.serverError().entity(e.getClass().getName() + ": " + e.getMessage()).build();
}
}

@Path("/reflection/{className}/field/{fieldName}/{value}")
@GET
@Produces(MediaType.TEXT_PLAIN)
public Response reflectField(@PathParam("className") String className,
@PathParam("fieldName") String fieldName,
@PathParam("value") String value) {
try {
final Class<?> cl = Class.forName(className);
final Object inst = cl.newInstance();
final Field field = cl.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(inst, value);
return Response.ok(inst.toString()).build();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchFieldException
| SecurityException | IllegalArgumentException e) {
return Response.serverError().entity(e.getClass().getName() + ": " + e.getMessage()).build();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ quarkus.camel.runtime-catalog.languages = false
quarkus.camel.native.resources.include-patterns = include-pattern-folder/*
quarkus.camel.native.resources.exclude-patterns = exclude-pattern-folder/*,include-pattern-folder/excluded.txt


# declarative reflection
quarkus.index-dependency.commons-lang3.group-id = org.apache.commons
quarkus.index-dependency.commons-lang3.artifact-id = commons-lang3
quarkus.camel.native.reflection.include-patterns = org.apache.commons.lang3.tuple.*
quarkus.camel.native.reflection.exclude-patterns = org.apache.commons.lang3.tuple.*Triple

#
# Camel
#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.restassured.RestAssured;
import org.junit.jupiter.api.Test;

import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -56,4 +57,30 @@ public void resourceMatchingNoPatternCouldNotBeLoadedFromNativeExecutable() {
RestAssured.when().get("/test/resources/no-pattern-folder/excluded.properties.txt").then().assertThat()
.statusCode(204);
}

@Test
void reflectiveMethod() {
RestAssured.when()
.get(
"/test/reflection/{className}/method/{methodName}/{value}",
"org.apache.commons.lang3.tuple.MutableTriple",
"setLeft",
"Kermit")
.then()
.statusCode(500) // *Triple is excluded in application.properties, but 500 will happen only in native mode
.body(is("java.lang.ClassNotFoundException: org.apache.commons.lang3.tuple.MutableTriple"));
}

@Test
void reflectiveField() {
RestAssured.when()
.get(
"/test/reflection/{className}/field/{fieldName}/{value}",
"org.apache.commons.lang3.tuple.MutableTriple",
"left",
"Joe")
.then()
.statusCode(500) // *Triple is excluded in application.properties, but 500 will happen only in native mode
.body(is("java.lang.ClassNotFoundException: org.apache.commons.lang3.tuple.MutableTriple"));
}
}