This is an automated email from the ASF dual-hosted git repository. ppalaga pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
The following commit(s) were added to refs/heads/master by this push: new 11d0386 Fix #253 Build time property to register classes for reflection 11d0386 is described below commit 11d0386514cc9909491f985a0bb3e5b27de7db44 Author: Peter Palaga <ppal...@redhat.com> AuthorDate: Thu Mar 26 16:10:02 2020 +0100 Fix #253 Build time property to register classes for reflection --- docs/modules/ROOT/pages/native-mode.adoc | 37 +++++++++++++ .../core/deployment/NativeImageProcessor.java | 37 ++++++++++++- .../quarkus/core/deployment/util/PathFilter.java | 16 ++++++ .../core/deployment/util/PathFilterTest.java | 32 +++++++++++ .../org/apache/camel/quarkus/core/CamelConfig.java | 62 ++++++++++++++++++++++ integration-tests/core/pom.xml | 4 ++ .../apache/camel/quarkus/core/CoreResource.java | 42 +++++++++++++++ .../core/src/main/resources/application.properties | 7 +++ .../java/org/apache/camel/quarkus/core/CoreIT.java | 27 ++++++++++ .../org/apache/camel/quarkus/core/CoreTest.java | 26 +++++++++ 10 files changed, 289 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/native-mode.adoc b/docs/modules/ROOT/pages/native-mode.adoc index e90cc8a..449b3c0 100644 --- a/docs/modules/ROOT/pages/native-mode.adoc +++ b/docs/modules/ROOT/pages/native-mode.adoc @@ -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 +---- diff --git a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/NativeImageProcessor.java b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/NativeImageProcessor.java index 9f28703..89ce069 100644 --- a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/NativeImageProcessor.java +++ b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/NativeImageProcessor.java @@ -44,6 +44,7 @@ import org.apache.camel.impl.engine.DefaultComponentResolver; 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; @@ -63,7 +64,7 @@ import org.slf4j.LoggerFactory; import static org.apache.commons.lang3.ClassUtils.getPackageName; -class NativeImageProcessor { +public class NativeImageProcessor { private static final Logger LOGGER = LoggerFactory.getLogger(NativeImageProcessor.class); /* @@ -240,6 +241,40 @@ class NativeImageProcessor { }); } } + + @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)); + } + } + } } /* diff --git a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/util/PathFilter.java b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/util/PathFilter.java index 4c6772d..4813bed 100644 --- a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/util/PathFilter.java +++ b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/util/PathFilter.java @@ -22,6 +22,7 @@ import java.util.Collection; 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; @@ -31,6 +32,9 @@ import org.jboss.jandex.DotName; * 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; @@ -83,6 +87,18 @@ public class PathFilter { } } + 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) == '/') diff --git a/extensions-core/core/deployment/src/test/java/org/apache/camel/quarkus/core/deployment/util/PathFilterTest.java b/extensions-core/core/deployment/src/test/java/org/apache/camel/quarkus/core/deployment/util/PathFilterTest.java index 2956860..133d7b6 100644 --- a/extensions-core/core/deployment/src/test/java/org/apache/camel/quarkus/core/deployment/util/PathFilterTest.java +++ b/extensions-core/core/deployment/src/test/java/org/apache/camel/quarkus/core/deployment/util/PathFilterTest.java @@ -21,8 +21,10 @@ import java.nio.file.Paths; 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; @@ -96,4 +98,34 @@ public class PathFilterTest { 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); + + } + } diff --git a/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CamelConfig.java b/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CamelConfig.java index 37ec208..b8fa70b 100644 --- a/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CamelConfig.java +++ b/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CamelConfig.java @@ -194,6 +194,12 @@ public class CamelConfig { @ConfigItem public ResourcesConfig resources; + /** + * Register classes for reflection. + */ + @ConfigItem + public ReflectionConfig reflection; + } @ConfigGroup @@ -226,6 +232,62 @@ public class CamelConfig { } @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 { /** * Used to control the resolution of components catalog info. diff --git a/integration-tests/core/pom.xml b/integration-tests/core/pom.xml index f090a34..599e87f 100644 --- a/integration-tests/core/pom.xml +++ b/integration-tests/core/pom.xml @@ -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> diff --git a/integration-tests/core/src/main/java/org/apache/camel/quarkus/core/CoreResource.java b/integration-tests/core/src/main/java/org/apache/camel/quarkus/core/CoreResource.java index 9e58def..d5a1e74 100644 --- a/integration-tests/core/src/main/java/org/apache/camel/quarkus/core/CoreResource.java +++ b/integration-tests/core/src/main/java/org/apache/camel/quarkus/core/CoreResource.java @@ -18,6 +18,9 @@ package org.apache.camel.quarkus.core; 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; @@ -29,6 +32,7 @@ import javax.ws.rs.Path; 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; @@ -167,4 +171,42 @@ public class CoreResource { 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(); + } + } + } diff --git a/integration-tests/core/src/main/resources/application.properties b/integration-tests/core/src/main/resources/application.properties index 699294e..a03920c 100644 --- a/integration-tests/core/src/main/resources/application.properties +++ b/integration-tests/core/src/main/resources/application.properties @@ -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 # diff --git a/integration-tests/core/src/test/java/org/apache/camel/quarkus/core/CoreIT.java b/integration-tests/core/src/test/java/org/apache/camel/quarkus/core/CoreIT.java index 2996e5b..a631c88 100644 --- a/integration-tests/core/src/test/java/org/apache/camel/quarkus/core/CoreIT.java +++ b/integration-tests/core/src/test/java/org/apache/camel/quarkus/core/CoreIT.java @@ -20,6 +20,7 @@ import io.quarkus.test.junit.NativeImageTest; 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; @@ -56,4 +57,30 @@ public class CoreIT extends CoreTest { 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")); + } } diff --git a/integration-tests/core/src/test/java/org/apache/camel/quarkus/core/CoreTest.java b/integration-tests/core/src/test/java/org/apache/camel/quarkus/core/CoreTest.java index 34977cd..46fac1a 100644 --- a/integration-tests/core/src/test/java/org/apache/camel/quarkus/core/CoreTest.java +++ b/integration-tests/core/src/test/java/org/apache/camel/quarkus/core/CoreTest.java @@ -76,4 +76,30 @@ public class CoreTest { public void testLRUCacheFactory() { RestAssured.when().get("/test/lru-cache-factory").then().body(is(DefaultLRUCacheFactory.class.getName())); } + + @Test + void reflectiveMethod() { + RestAssured.when() + .get( + "/test/reflection/{className}/method/{methodName}/{value}", + "org.apache.commons.lang3.tuple.MutablePair", + "setLeft", + "Kermit") + .then() + .statusCode(200) + .body(is("(Kermit,null)")); + } + + @Test + void reflectiveField() { + RestAssured.when() + .get( + "/test/reflection/{className}/field/{fieldName}/{value}", + "org.apache.commons.lang3.tuple.MutablePair", + "left", + "Joe") + .then() + .statusCode(200) + .body(is("(Joe,null)")); + } }