This is an automated email from the ASF dual-hosted git repository.
zhfeng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
The following commit(s) were added to refs/heads/main by this push:
new 76f2ba2bf4 Auto-detect onException/doCatch/throwException exception
classes for native reflection (#8508)
76f2ba2bf4 is described below
commit 76f2ba2bf4f02683d67eb1447d9c801f9897db56
Author: Zheng Feng <[email protected]>
AuthorDate: Thu Apr 9 15:05:39 2026 +0800
Auto-detect onException/doCatch/throwException exception classes for native
reflection (#8508)
* Auto-detect onException/doCatch/throwException exception classes for
native reflection
Scans Java DSL (ASM bytecode), XML DSL (ModelParser), and YAML DSL
(SnakeYAML) route definitions at build time to detect exception classes
used in onException(), doCatch(), and throwException() calls, and
registers them for GraalVM native image reflection automatically.
This removes the need for manual @RegisterForReflection annotations.
Closes: https://github.com/apache/camel-quarkus/issues/7841
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Update to only run these steps in native build
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../core/deployment/CamelNativeImageProcessor.java | 144 +++++++++++++++++++
.../dsl/xml/io/deployment/XmlIoDslProcessor.java | 156 +++++++++++++++++++++
.../dsl/yaml/deployment/YamlDslProcessor.java | 98 +++++++++++++
.../org/apache/camel/quarkus/eip/it/EipRoutes.java | 2 -
.../RouteConfigurationsException.java | 10 +-
.../RouteConfigurationsResource.java | 14 ++
...rationsException.java => XmlOnlyException.java} | 15 +-
...ationsException.java => YamlOnlyException.java} | 15 +-
.../src/main/resources/application.properties | 2 +-
.../src/main/resources/xml/xmlOnlyRoutes.xml | 32 +++++
.../src/main/resources/yaml/yamlOnlyRoutes.yaml | 33 +++++
.../RouteConfigurationsTest.java | 26 ++++
12 files changed, 525 insertions(+), 22 deletions(-)
diff --git
a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/CamelNativeImageProcessor.java
b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/CamelNativeImageProcessor.java
index 5773b4065b..46d3f23cd3 100644
---
a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/CamelNativeImageProcessor.java
+++
b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/CamelNativeImageProcessor.java
@@ -17,11 +17,15 @@
package org.apache.camel.quarkus.core.deployment;
import java.io.IOException;
+import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -33,6 +37,7 @@ import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import
io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem;
+import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild;
import org.apache.camel.CamelContext;
import org.apache.camel.Component;
import org.apache.camel.Consumer;
@@ -64,6 +69,11 @@ import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -72,6 +82,11 @@ import static
org.apache.commons.lang3.ClassUtils.getPackageName;
public class CamelNativeImageProcessor {
private static final Logger LOGGER =
LoggerFactory.getLogger(CamelNativeImageProcessor.class);
+ private static final DotName THROWABLE_TYPE =
DotName.createSimple(Throwable.class.getName());
+
+ private static final Set<String> EXCEPTION_REGISTRATION_METHODS =
Set.of("onException", "doCatch", "exception",
+ "throwException");
+
private static final List<Class<?>> CAMEL_REFLECTIVE_CLASSES =
List.<Class<?>> of(
Endpoint.class,
Consumer.class,
@@ -285,4 +300,133 @@ public class CamelNativeImageProcessor {
});
}
+ /**
+ * Scans RouteBuilder bytecode to auto-detect exception classes used in
onException() and doCatch() calls, and
+ * registers them for reflection. This is needed because Camel stores
exception class names as strings in the route
+ * model and resolves them back to Class objects at runtime via
ClassLoader.loadClass().
+ *
+ * @see <a
href="https://github.com/apache/camel-quarkus/issues/7841">camel-quarkus#7841</a>
+ */
+ @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class)
+ void registerOnExceptionClassesForReflection(
+ CombinedIndexBuildItem combinedIndex,
+ List<CamelRoutesBuilderClassBuildItem> camelRoutesBuilders,
+ BuildProducer<ReflectiveClassBuildItem> reflectiveClass) {
+
+ final IndexView index = combinedIndex.getIndex();
+ final Set<String> confirmedClasses = new HashSet<>();
+
+ for (CamelRoutesBuilderClassBuildItem routeBuilder :
camelRoutesBuilders) {
+ String className = routeBuilder.getDotName().toString();
+ String resourceName = className.replace('.', '/') + ".class";
+
+ try (InputStream is =
Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName))
{
+ if (is == null) {
+ LOGGER.debug("Could not read bytecode for RouteBuilder:
{}", className);
+ continue;
+ }
+ ClassReader reader = new ClassReader(is);
+ reader.accept(new ClassVisitor(Opcodes.ASM9) {
+ @Override
+ public MethodVisitor visitMethod(int access, String name,
String descriptor,
+ String signature, String[] exceptions) {
+ return new ExceptionClassDetector(confirmedClasses,
index);
+ }
+ }, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+ } catch (IOException e) {
+ LOGGER.debug("Failed to scan bytecode for RouteBuilder: {}",
className, e);
+ }
+ }
+
+ if (!confirmedClasses.isEmpty()) {
+ LOGGER.debug("Auto-detected onException/doCatch classes for
reflection: {}", confirmedClasses);
+ for (String cls : confirmedClasses) {
+
reflectiveClass.produce(ReflectiveClassBuildItem.builder(cls).build());
+ }
+ }
+ }
+
+ /**
+ * Checks whether the given class name is a Throwable subclass. First
checks the Jandex index (covers application
+ * and indexed dependency classes), then falls back to Class.forName() for
JDK/unindexed classes.
+ */
+ static boolean isThrowable(String className, IndexView index) {
+ // Check via Jandex index first (covers application and indexed
dependency classes)
+ ClassInfo classInfo =
index.getClassByName(DotName.createSimple(className));
+ if (classInfo != null) {
+ return isThrowableInIndex(classInfo, index);
+ }
+
+ // Fall back to Class.forName() for JDK and unindexed library classes
+ try {
+ Class<?> clazz =
Thread.currentThread().getContextClassLoader().loadClass(className);
+ return Throwable.class.isAssignableFrom(clazz);
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+
+ private static boolean isThrowableInIndex(ClassInfo classInfo, IndexView
index) {
+ DotName current = classInfo.name();
+ while (current != null) {
+ if (THROWABLE_TYPE.equals(current)) {
+ return true;
+ }
+ ClassInfo info = index.getClassByName(current);
+ if (info == null) {
+ // Class not in index, try Class.forName() for the remaining
hierarchy
+ try {
+ Class<?> clazz =
Thread.currentThread().getContextClassLoader().loadClass(current.toString());
+ return Throwable.class.isAssignableFrom(clazz);
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+ current = info.superName();
+ }
+ return false;
+ }
+
+ /**
+ * ASM MethodVisitor that detects Throwable class constants passed to
onException/doCatch/exception method calls.
+ * <p>
+ * It tracks class constants loaded via LDC instructions in a pending set.
When a method call instruction is
+ * visited:
+ * <ul>
+ * <li>If the method is onException/doCatch/exception with a Class
parameter, all pending Throwable classes are
+ * confirmed.</li>
+ * <li>The pending set is cleared after any method call, so unrelated
class references are discarded.</li>
+ * </ul>
+ */
+ static class ExceptionClassDetector extends MethodVisitor {
+ private final Set<String> pendingClasses = new LinkedHashSet<>();
+ private final Set<String> confirmedClasses;
+ private final IndexView index;
+
+ ExceptionClassDetector(Set<String> confirmedClasses, IndexView index) {
+ super(Opcodes.ASM9);
+ this.confirmedClasses = confirmedClasses;
+ this.index = index;
+ }
+
+ @Override
+ public void visitLdcInsn(Object value) {
+ if (value instanceof Type type && type.getSort() == Type.OBJECT) {
+ String className = type.getClassName();
+ if (isThrowable(className, index)) {
+ pendingClasses.add(className);
+ }
+ }
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name,
+ String descriptor, boolean isInterface) {
+ if (EXCEPTION_REGISTRATION_METHODS.contains(name) &&
descriptor.contains("Ljava/lang/Class;")) {
+ confirmedClasses.addAll(pendingClasses);
+ }
+ pendingClasses.clear();
+ }
+ }
+
}
diff --git
a/extensions-core/xml-io-dsl/deployment/src/main/java/org/apache/camel/quarkus/dsl/xml/io/deployment/XmlIoDslProcessor.java
b/extensions-core/xml-io-dsl/deployment/src/main/java/org/apache/camel/quarkus/dsl/xml/io/deployment/XmlIoDslProcessor.java
index 8dc3382ee5..340d23f67c 100644
---
a/extensions-core/xml-io-dsl/deployment/src/main/java/org/apache/camel/quarkus/dsl/xml/io/deployment/XmlIoDslProcessor.java
+++
b/extensions-core/xml-io-dsl/deployment/src/main/java/org/apache/camel/quarkus/dsl/xml/io/deployment/XmlIoDslProcessor.java
@@ -17,14 +17,37 @@
package org.apache.camel.quarkus.dsl.xml.io.deployment;
+import java.io.InputStream;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+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.nativeimage.ReflectiveClassBuildItem;
+import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild;
+import org.apache.camel.model.CatchDefinition;
+import org.apache.camel.model.OnExceptionDefinition;
+import org.apache.camel.model.ProcessorDefinition;
+import org.apache.camel.model.RouteConfigurationDefinition;
+import org.apache.camel.model.RouteConfigurationsDefinition;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.RoutesDefinition;
+import org.apache.camel.model.ThrowExceptionDefinition;
+import org.apache.camel.model.app.ApplicationDefinition;
import
org.apache.camel.quarkus.core.deployment.spi.CamelModelToXMLDumperBuildItem;
+import
org.apache.camel.quarkus.core.deployment.spi.CamelRouteResourceBuildItem;
import org.apache.camel.quarkus.dsl.xml.XmlIoDslRecorder;
+import org.apache.camel.xml.in.ModelParser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class XmlIoDslProcessor {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(XmlIoDslProcessor.class);
private static final String FEATURE = "camel-xml-io-dsl";
@BuildStep
@@ -37,4 +60,137 @@ public class XmlIoDslProcessor {
CamelModelToXMLDumperBuildItem xmlModelDumper(XmlIoDslRecorder recorder) {
return new
CamelModelToXMLDumperBuildItem(recorder.newXmlIoModelToXMLDumper());
}
+
+ /**
+ * Parses XML DSL route files to detect exception classes used in
onException/doCatch definitions,
+ * and registers them for reflection in native builds.
+ *
+ * @see <a
href="https://github.com/apache/camel-quarkus/issues/7841">camel-quarkus#7841</a>
+ */
+ @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class)
+ void registerOnExceptionClassesForReflection(
+ List<CamelRouteResourceBuildItem> camelRouteResources,
+ BuildProducer<ReflectiveClassBuildItem> reflectiveClass) {
+
+ final Set<String> exceptionClasses = new HashSet<>();
+
+ for (CamelRouteResourceBuildItem routeResource : camelRouteResources) {
+ String sourcePath = routeResource.getSourcePath();
+ if (!sourcePath.endsWith(".xml")) {
+ continue;
+ }
+
+ // Try each XML format: <routes>, <routeConfiguration(s)>,
<beans>/<camel>
+ if (tryParseRoutes(sourcePath, exceptionClasses)) {
+ continue;
+ }
+ if (tryParseRouteConfigurations(sourcePath, exceptionClasses)) {
+ continue;
+ }
+ tryParseApplication(sourcePath, exceptionClasses);
+ }
+
+ if (!exceptionClasses.isEmpty()) {
+ LOGGER.debug("Auto-detected onException/doCatch classes from XML
routes for reflection: {}", exceptionClasses);
+ for (String cls : exceptionClasses) {
+
reflectiveClass.produce(ReflectiveClassBuildItem.builder(cls).build());
+ }
+ }
+ }
+
+ private boolean tryParseRoutes(String sourcePath, Set<String>
exceptionClasses) {
+ try (InputStream is =
Thread.currentThread().getContextClassLoader().getResourceAsStream(sourcePath))
{
+ if (is == null) {
+ return false;
+ }
+ ModelParser parser = new ModelParser(is);
+ Optional<RoutesDefinition> routesOpt =
parser.parseRoutesDefinition();
+ if (routesOpt.isPresent()) {
+ RoutesDefinition routes = routesOpt.get();
+ collectOnExceptionClasses(routes.getOnExceptions(),
exceptionClasses);
+ if (routes.getRoutes() != null) {
+ for (RouteDefinition route : routes.getRoutes()) {
+ collectExceptionClassesFromOutputs(route.getOutputs(),
exceptionClasses);
+ }
+ }
+ return true;
+ }
+ } catch (Exception e) {
+ LOGGER.debug("Failed to parse XML route resource: {}", sourcePath,
e);
+ }
+ return false;
+ }
+
+ private boolean tryParseRouteConfigurations(String sourcePath, Set<String>
exceptionClasses) {
+ try (InputStream is =
Thread.currentThread().getContextClassLoader().getResourceAsStream(sourcePath))
{
+ if (is == null) {
+ return false;
+ }
+ ModelParser parser = new ModelParser(is);
+ Optional<RouteConfigurationsDefinition> rcOpt =
parser.parseRouteConfigurationsDefinition();
+ if (rcOpt.isPresent()) {
+ for (RouteConfigurationDefinition rc :
rcOpt.get().getRouteConfigurations()) {
+ collectOnExceptionClasses(rc.getOnExceptions(),
exceptionClasses);
+ }
+ return true;
+ }
+ } catch (Exception e) {
+ LOGGER.debug("Failed to parse XML route configuration: {}",
sourcePath, e);
+ }
+ return false;
+ }
+
+ private boolean tryParseApplication(String sourcePath, Set<String>
exceptionClasses) {
+ try (InputStream is =
Thread.currentThread().getContextClassLoader().getResourceAsStream(sourcePath))
{
+ if (is == null) {
+ return false;
+ }
+ ModelParser parser = new ModelParser(is);
+ Optional<ApplicationDefinition> appOpt =
parser.parseApplicationDefinition();
+ if (appOpt.isPresent()) {
+ ApplicationDefinition app = appOpt.get();
+ if (app.getRoutes() != null) {
+ for (RouteDefinition route : app.getRoutes()) {
+ collectExceptionClassesFromOutputs(route.getOutputs(),
exceptionClasses);
+ }
+ }
+ if (app.getRouteConfigurations() != null) {
+ for (RouteConfigurationDefinition rc :
app.getRouteConfigurations()) {
+ collectOnExceptionClasses(rc.getOnExceptions(),
exceptionClasses);
+ }
+ }
+ return true;
+ }
+ } catch (Exception e) {
+ LOGGER.debug("Failed to parse XML application definition: {}",
sourcePath, e);
+ }
+ return false;
+ }
+
+ private static void collectOnExceptionClasses(List<OnExceptionDefinition>
onExceptions, Set<String> exceptionClasses) {
+ if (onExceptions != null) {
+ for (OnExceptionDefinition onEx : onExceptions) {
+ exceptionClasses.addAll(onEx.getExceptions());
+ }
+ }
+ }
+
+ private static void
collectExceptionClassesFromOutputs(List<ProcessorDefinition<?>> outputs,
+ Set<String> exceptionClasses) {
+ if (outputs == null) {
+ return;
+ }
+ for (ProcessorDefinition<?> output : outputs) {
+ if (output instanceof OnExceptionDefinition onEx) {
+ exceptionClasses.addAll(onEx.getExceptions());
+ } else if (output instanceof CatchDefinition catchDef) {
+ exceptionClasses.addAll(catchDef.getExceptions());
+ } else if (output instanceof ThrowExceptionDefinition throwEx) {
+ if (throwEx.getExceptionType() != null) {
+ exceptionClasses.add(throwEx.getExceptionType());
+ }
+ }
+ collectExceptionClassesFromOutputs(output.getOutputs(),
exceptionClasses);
+ }
+ }
}
diff --git
a/extensions-core/yaml-dsl/deployment/src/main/java/org/apache/camel/quarkus/dsl/yaml/deployment/YamlDslProcessor.java
b/extensions-core/yaml-dsl/deployment/src/main/java/org/apache/camel/quarkus/dsl/yaml/deployment/YamlDslProcessor.java
index d55cd92834..551466ae71 100644
---
a/extensions-core/yaml-dsl/deployment/src/main/java/org/apache/camel/quarkus/dsl/yaml/deployment/YamlDslProcessor.java
+++
b/extensions-core/yaml-dsl/deployment/src/main/java/org/apache/camel/quarkus/dsl/yaml/deployment/YamlDslProcessor.java
@@ -17,14 +17,112 @@
package org.apache.camel.quarkus.dsl.yaml.deployment;
+import java.io.InputStream;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.FeatureBuildItem;
+import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
+import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild;
+import
org.apache.camel.quarkus.core.deployment.spi.CamelRouteResourceBuildItem;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.snakeyaml.engine.v2.api.Load;
+import org.snakeyaml.engine.v2.api.LoadSettings;
public class YamlDslProcessor {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(YamlDslProcessor.class);
private static final String FEATURE = "camel-yaml-dsl";
+ private static final Set<String> EXCEPTION_CONTAINER_KEYS =
Set.of("onException", "on-exception", "doCatch", "do-catch");
+ private static final Set<String> THROW_EXCEPTION_KEYS =
Set.of("throwException", "throw-exception");
@BuildStep
FeatureBuildItem feature() {
return new FeatureBuildItem(FEATURE);
}
+
+ /**
+ * Parses YAML DSL route files to detect exception classes used in
onException/doCatch definitions,
+ * and registers them for reflection in native builds.
+ *
+ * @see <a
href="https://github.com/apache/camel-quarkus/issues/7841">camel-quarkus#7841</a>
+ */
+ @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class)
+ void registerOnExceptionClassesForReflection(
+ List<CamelRouteResourceBuildItem> camelRouteResources,
+ BuildProducer<ReflectiveClassBuildItem> reflectiveClass) {
+
+ final Set<String> exceptionClasses = new HashSet<>();
+
+ for (CamelRouteResourceBuildItem routeResource : camelRouteResources) {
+ String sourcePath = routeResource.getSourcePath();
+ if (!sourcePath.endsWith(".yaml") && !sourcePath.endsWith(".yml"))
{
+ continue;
+ }
+
+ try (InputStream is =
Thread.currentThread().getContextClassLoader().getResourceAsStream(sourcePath))
{
+ if (is == null) {
+ LOGGER.debug("Could not read YAML route resource: {}",
sourcePath);
+ continue;
+ }
+
+ LoadSettings settings = LoadSettings.builder().build();
+ Load load = new Load(settings);
+ for (Object document : load.loadAllFromInputStream(is)) {
+ collectExceptionClasses(document, exceptionClasses);
+ }
+ } catch (Exception e) {
+ LOGGER.debug("Failed to parse YAML route resource for
exception detection: {}", sourcePath, e);
+ }
+ }
+
+ if (!exceptionClasses.isEmpty()) {
+ LOGGER.debug("Auto-detected onException/doCatch classes from YAML
routes for reflection: {}", exceptionClasses);
+ for (String cls : exceptionClasses) {
+
reflectiveClass.produce(ReflectiveClassBuildItem.builder(cls).build());
+ }
+ }
+ }
+
+ /**
+ * Recursively walks a parsed YAML structure looking for
onException/doCatch mappings
+ * and extracts exception class names from their "exception" fields.
+ */
+ @SuppressWarnings("unchecked")
+ private static void collectExceptionClasses(Object node, Set<String>
exceptionClasses) {
+ if (node instanceof java.util.Map<?, ?> map) {
+ for (java.util.Map.Entry<?, ?> entry : map.entrySet()) {
+ String key = String.valueOf(entry.getKey());
+ if (EXCEPTION_CONTAINER_KEYS.contains(key) && entry.getValue()
instanceof java.util.Map<?, ?> inner) {
+ // Extract "exception" list from onException/doCatch
mapping
+ Object exceptions = inner.get("exception");
+ if (exceptions instanceof List<?> list) {
+ for (Object item : list) {
+ if (item instanceof String className) {
+ exceptionClasses.add(className);
+ }
+ }
+ } else if (exceptions instanceof String className) {
+ exceptionClasses.add(className);
+ }
+ } else if (THROW_EXCEPTION_KEYS.contains(key)
+ && entry.getValue() instanceof java.util.Map<?, ?>
inner) {
+ // Extract "exceptionType" from throwException mapping
+ Object exType = inner.get("exceptionType");
+ if (exType instanceof String className) {
+ exceptionClasses.add(className);
+ }
+ }
+ // Continue walking the tree
+ collectExceptionClasses(entry.getValue(), exceptionClasses);
+ }
+ } else if (node instanceof List<?> list) {
+ for (Object item : list) {
+ collectExceptionClasses(item, exceptionClasses);
+ }
+ }
+ }
}
diff --git
a/integration-test-groups/foundation/eip/src/main/java/org/apache/camel/quarkus/eip/it/EipRoutes.java
b/integration-test-groups/foundation/eip/src/main/java/org/apache/camel/quarkus/eip/it/EipRoutes.java
index 34b18e59bf..cc4a7b811d 100644
---
a/integration-test-groups/foundation/eip/src/main/java/org/apache/camel/quarkus/eip/it/EipRoutes.java
+++
b/integration-test-groups/foundation/eip/src/main/java/org/apache/camel/quarkus/eip/it/EipRoutes.java
@@ -16,7 +16,6 @@
*/
package org.apache.camel.quarkus.eip.it;
-import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Named;
@@ -28,7 +27,6 @@ import
org.apache.camel.processor.ThrottlerRejectedExecutionException;
import org.apache.camel.processor.loadbalancer.RoundRobinLoadBalancer;
@ApplicationScoped
-@RegisterForReflection(targets = ThrottlerRejectedExecutionException.class)
public class EipRoutes extends RouteBuilder {
public static final int THROTTLE_TIMEOUT = 5000;
diff --git
a/integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsException.java
b/integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsException.java
index 27dda72929..34a6d91763 100644
---
a/integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsException.java
+++
b/integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsException.java
@@ -16,14 +16,18 @@
*/
package org.apache.camel.quarkus.core.it.routeconfigurations;
-import io.quarkus.runtime.annotations.RegisterForReflection;
-
/**
* Having an Exception implementation dedicated to route configurations tests
* reinforce tests isolation. It's especially useful when the foundation tests
* are running grouped.
+ *
+ * Note: This class intentionally does NOT use {@code @RegisterForReflection}.
+ * The build step in {@code
CamelNativeImageProcessor#registerOnExceptionClassesForReflection}
+ * auto-detects it from the {@code
onException(RouteConfigurationsException.class)} calls
+ * in the Java DSL route builders.
+ *
+ * @see <a
href="https://github.com/apache/camel-quarkus/issues/7841">camel-quarkus#7841</a>
*/
-@RegisterForReflection
public class RouteConfigurationsException extends Exception {
private static final long serialVersionUID = 1L;
diff --git
a/integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsResource.java
b/integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsResource.java
index e31de46930..ad084e97c8 100644
---
a/integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsResource.java
+++
b/integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsResource.java
@@ -70,4 +70,18 @@ public class RouteConfigurationsResource {
return producerTemplate.requestBody("direct:yamlRoute", null,
String.class);
}
+ @Path("/route-configurations/xmlOnlyOnException")
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ public String xmlOnlyOnException() {
+ return producerTemplate.requestBody("direct:xmlOnlyOnException", null,
String.class);
+ }
+
+ @Path("/route-configurations/yamlOnlyOnException")
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ public String yamlOnlyOnException() {
+ return producerTemplate.requestBody("direct:yamlOnlyOnException",
null, String.class);
+ }
+
}
diff --git
a/integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsException.java
b/integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/XmlOnlyException.java
similarity index 69%
copy from
integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsException.java
copy to
integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/XmlOnlyException.java
index 27dda72929..83370944ad 100644
---
a/integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsException.java
+++
b/integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/XmlOnlyException.java
@@ -16,19 +16,18 @@
*/
package org.apache.camel.quarkus.core.it.routeconfigurations;
-import io.quarkus.runtime.annotations.RegisterForReflection;
-
/**
- * Having an Exception implementation dedicated to route configurations tests
- * reinforce tests isolation. It's especially useful when the foundation tests
- * are running grouped.
+ * Exception class that is ONLY referenced from XML DSL route definitions,
+ * never from Java DSL or YAML DSL. This tests that the XML DSL build-time
+ * parser correctly detects and registers exception classes for native image
reflection.
+ *
+ * @see <a
href="https://github.com/apache/camel-quarkus/issues/7841">camel-quarkus#7841</a>
*/
-@RegisterForReflection
-public class RouteConfigurationsException extends Exception {
+public class XmlOnlyException extends Exception {
private static final long serialVersionUID = 1L;
- public RouteConfigurationsException(String message) {
+ public XmlOnlyException(String message) {
super(message);
}
diff --git
a/integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsException.java
b/integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/YamlOnlyException.java
similarity index 68%
copy from
integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsException.java
copy to
integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/YamlOnlyException.java
index 27dda72929..a84f8b31d7 100644
---
a/integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsException.java
+++
b/integration-test-groups/foundation/route-configurations/src/main/java/org/apache/camel/quarkus/core/it/routeconfigurations/YamlOnlyException.java
@@ -16,19 +16,18 @@
*/
package org.apache.camel.quarkus.core.it.routeconfigurations;
-import io.quarkus.runtime.annotations.RegisterForReflection;
-
/**
- * Having an Exception implementation dedicated to route configurations tests
- * reinforce tests isolation. It's especially useful when the foundation tests
- * are running grouped.
+ * Exception class that is ONLY referenced from YAML DSL route definitions,
+ * never from Java DSL or XML DSL. This tests that the YAML DSL build-time
+ * parser correctly detects and registers exception classes for native image
reflection.
+ *
+ * @see <a
href="https://github.com/apache/camel-quarkus/issues/7841">camel-quarkus#7841</a>
*/
-@RegisterForReflection
-public class RouteConfigurationsException extends Exception {
+public class YamlOnlyException extends Exception {
private static final long serialVersionUID = 1L;
- public RouteConfigurationsException(String message) {
+ public YamlOnlyException(String message) {
super(message);
}
diff --git
a/integration-test-groups/foundation/route-configurations/src/main/resources/application.properties
b/integration-test-groups/foundation/route-configurations/src/main/resources/application.properties
index 80c421db33..66318de609 100644
---
a/integration-test-groups/foundation/route-configurations/src/main/resources/application.properties
+++
b/integration-test-groups/foundation/route-configurations/src/main/resources/application.properties
@@ -14,4 +14,4 @@
## See the License for the specific language governing permissions and
## limitations under the License.
## ---------------------------------------------------------------------------
-camel.main.routes-include-pattern =
classpath:xml/routes.xml,classpath:xml/routeConfigurations.xml,classpath:yaml/routeConfigurations.yaml,classpath:yaml/routes.yaml
+camel.main.routes-include-pattern =
classpath:xml/routes.xml,classpath:xml/routeConfigurations.xml,classpath:xml/xmlOnlyRoutes.xml,classpath:yaml/routeConfigurations.yaml,classpath:yaml/routes.yaml,classpath:yaml/yamlOnlyRoutes.yaml
diff --git
a/integration-test-groups/foundation/route-configurations/src/main/resources/xml/xmlOnlyRoutes.xml
b/integration-test-groups/foundation/route-configurations/src/main/resources/xml/xmlOnlyRoutes.xml
new file mode 100644
index 0000000000..7b80caf495
--- /dev/null
+++
b/integration-test-groups/foundation/route-configurations/src/main/resources/xml/xmlOnlyRoutes.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<routes xmlns="http://camel.apache.org/schema/xml-io">
+
+ <route id="xmlOnlyOnExceptionRoute">
+ <from uri="direct:xmlOnlyOnException"/>
+ <onException>
+
<exception>org.apache.camel.quarkus.core.it.routeconfigurations.XmlOnlyException</exception>
+ <handled><constant>true</constant></handled>
+ <setBody><constant>onException caught XmlOnlyException from XML
route</constant></setBody>
+ </onException>
+ <throwException
exceptionType="org.apache.camel.quarkus.core.it.routeconfigurations.XmlOnlyException"
message="test throwException from XML route"/>
+ </route>
+
+</routes>
diff --git
a/integration-test-groups/foundation/route-configurations/src/main/resources/yaml/yamlOnlyRoutes.yaml
b/integration-test-groups/foundation/route-configurations/src/main/resources/yaml/yamlOnlyRoutes.yaml
new file mode 100644
index 0000000000..a555b61086
--- /dev/null
+++
b/integration-test-groups/foundation/route-configurations/src/main/resources/yaml/yamlOnlyRoutes.yaml
@@ -0,0 +1,33 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+- route:
+ id: yamlOnlyOnExceptionRoute
+ from:
+ uri: "direct:yamlOnlyOnException"
+ steps:
+ - onException:
+ exception:
+ -
"org.apache.camel.quarkus.core.it.routeconfigurations.YamlOnlyException"
+ handled:
+ constant: true
+ steps:
+ - setBody:
+ constant: "onException caught YamlOnlyException from YAML
route"
+ - throwException:
+ exceptionType:
"org.apache.camel.quarkus.core.it.routeconfigurations.YamlOnlyException"
+ message: "test throwException from YAML route"
diff --git
a/integration-test-groups/foundation/route-configurations/src/test/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsTest.java
b/integration-test-groups/foundation/route-configurations/src/test/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsTest.java
index de9d935ee6..c4d99974b9 100644
---
a/integration-test-groups/foundation/route-configurations/src/test/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsTest.java
+++
b/integration-test-groups/foundation/route-configurations/src/test/java/org/apache/camel/quarkus/core/it/routeconfigurations/RouteConfigurationsTest.java
@@ -97,4 +97,30 @@ public class RouteConfigurationsTest {
.then()
.body(is(expected));
}
+
+ /**
+ * Verifies that exception classes referenced ONLY in XML DSL onException
definitions (not in any
+ * Java DSL or YAML DSL route) are auto-detected and registered for
reflection in native builds.
+ */
+ @Test
+ public void xmlOnlyOnExceptionShouldBeAutoDetectedForReflection() {
+ String expected = "onException caught XmlOnlyException from XML route";
+ RestAssured.given()
+ .get("/core/route-configurations/xmlOnlyOnException")
+ .then()
+ .body(is(expected));
+ }
+
+ /**
+ * Verifies that exception classes referenced ONLY in YAML DSL onException
definitions (not in any
+ * Java DSL or XML DSL route) are auto-detected and registered for
reflection in native builds.
+ */
+ @Test
+ public void yamlOnlyOnExceptionShouldBeAutoDetectedForReflection() {
+ String expected = "onException caught YamlOnlyException from YAML
route";
+ RestAssured.given()
+ .get("/core/route-configurations/yamlOnlyOnException")
+ .then()
+ .body(is(expected));
+ }
}