You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by pp...@apache.org on 2020/03/26 20:43:35 UTC

[camel-quarkus] branch master updated: Fix #253 Build time property to register classes for reflection

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 <pp...@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)"));
+    }
 }