This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new e6b49957f1d1 CAMEL-20141: camel-joor - Fix hot-reload of inline bean
scripts with multiple Java files
e6b49957f1d1 is described below
commit e6b49957f1d19e59b96478e48a01495632740a36
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Jul 14 14:49:00 2026 +0200
CAMEL-20141: camel-joor - Fix hot-reload of inline bean scripts with
multiple Java files
During dev mode hot-reload, when a .java file changes only that file was
recompiled in a new ByteArrayClassLoader. Other Java files (e.g. a builder
class) stayed loaded from the original classloader, causing class identity
mismatch and NoSuchBeanException.
Fix: JavaRoutesBuilderLoader now tracks all previously compiled Java
resources
and includes them in any recompilation, ensuring all classes share the same
classloader. Also fixes the classloader parent chain, compile directory
alignment, and adds proper class cleanup on reload.
Closes #24677
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
.../camel/language/joor/ByteArrayClassLoader.java | 5 +++++
.../camel/language/joor/CamelJoorClassLoader.java | 12 +++++++++--
.../camel/language/joor/JoorScriptingCompiler.java | 17 ++++++++++++++-
.../apache/camel/language/joor/MultiCompile.java | 2 +-
.../dsl/java/joor/JavaRoutesBuilderLoader.java | 24 ++++++++++++++++++++--
5 files changed, 54 insertions(+), 6 deletions(-)
diff --git
a/components/camel-joor/src/main/java/org/apache/camel/language/joor/ByteArrayClassLoader.java
b/components/camel-joor/src/main/java/org/apache/camel/language/joor/ByteArrayClassLoader.java
index 3e4153423929..f819e400548f 100644
---
a/components/camel-joor/src/main/java/org/apache/camel/language/joor/ByteArrayClassLoader.java
+++
b/components/camel-joor/src/main/java/org/apache/camel/language/joor/ByteArrayClassLoader.java
@@ -30,6 +30,11 @@ final class ByteArrayClassLoader extends ClassLoader {
this.classes = classes;
}
+ public ByteArrayClassLoader(ClassLoader parent, Map<String, byte[]>
classes) {
+ super(parent);
+ this.classes = classes;
+ }
+
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = classes.get(name);
diff --git
a/components/camel-joor/src/main/java/org/apache/camel/language/joor/CamelJoorClassLoader.java
b/components/camel-joor/src/main/java/org/apache/camel/language/joor/CamelJoorClassLoader.java
index 06503e60b8b6..c7b9fa20431b 100644
---
a/components/camel-joor/src/main/java/org/apache/camel/language/joor/CamelJoorClassLoader.java
+++
b/components/camel-joor/src/main/java/org/apache/camel/language/joor/CamelJoorClassLoader.java
@@ -53,8 +53,7 @@ public class CamelJoorClassLoader extends URLClassLoader {
CompileStrategy cs =
camelContext.getCamelContextExtension().getContextPlugin(CompileStrategy.class);
if (cs != null && cs.getWorkDir() != null) {
try {
- File dir = new File(cs.getWorkDir() + "/joor");
- answer.add(dir.toURI().toURL());
+ answer.add(new File(cs.getWorkDir()).toURI().toURL());
} catch (MalformedURLException e) {
// ignore
}
@@ -74,6 +73,15 @@ public class CamelJoorClassLoader extends URLClassLoader {
return doLoadClass(name);
}
+ @Override
+ protected Class<?> loadClass(String name, boolean resolve) throws
ClassNotFoundException {
+ Class<?> c = doLoadClass(name);
+ if (resolve) {
+ resolveClass(c);
+ }
+ return c;
+ }
+
public Class<?> doLoadClass(String name) throws ClassNotFoundException {
// first try JavaJoorClassLoader
ClassLoader joorClassLoader =
camelContext.getClassResolver().getClassLoader("JavaJoorClassLoader");
diff --git
a/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorScriptingCompiler.java
b/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorScriptingCompiler.java
index c72d6f6db07f..48ee4d13b0f7 100644
---
a/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorScriptingCompiler.java
+++
b/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorScriptingCompiler.java
@@ -23,6 +23,7 @@ import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -45,6 +46,7 @@ public class JoorScriptingCompiler extends ServiceSupport
implements StaticServi
private static final Logger LOG =
LoggerFactory.getLogger(JoorScriptingCompiler.class);
private static final AtomicInteger UUID = new AtomicInteger();
+ private final Set<String> compiledClassNames =
ConcurrentHashMap.newKeySet();
private CamelContext camelContext;
private JavaJoorClassLoader classLoader;
private Set<String> imports = new TreeSet<>();
@@ -92,7 +94,7 @@ public class JoorScriptingCompiler extends ServiceSupport
implements StaticServi
// use work dir for classloader as it writes compiled classes to
disk
CompileStrategy cs =
context.getCamelContextExtension().getContextPlugin(CompileStrategy.class);
if (cs != null && cs.getWorkDir() != null) {
- classLoader.setCompileDirectory(cs.getWorkDir() + "/joor");
+ classLoader.setCompileDirectory(cs.getWorkDir());
}
}
}
@@ -108,6 +110,15 @@ public class JoorScriptingCompiler extends ServiceSupport
implements StaticServi
CamelContext camelContext, String script, Map<String, Object>
bindings, boolean singleQuotes) {
StopWatch watch = new StopWatch();
+ // clean up previously compiled script classes to prevent classloader
leak during reloads
+ if (classLoader != null && !compiledClassNames.isEmpty()) {
+ for (String old : compiledClassNames) {
+ LOG.debug("Removing old compiled class: {}", old);
+ classLoader.removeClass(old);
+ }
+ compiledClassNames.clear();
+ }
+
JoorScriptingMethod answer;
String className = nextFQN();
String code = evalCode(camelContext, className, script, bindings,
singleQuotes);
@@ -131,6 +142,10 @@ public class JoorScriptingCompiler extends ServiceSupport
implements StaticServi
if (clazz != null) {
LOG.debug("Compiled to Java class: {}", clazz);
answer = (JoorScriptingMethod)
clazz.getConstructor(CamelContext.class).newInstance(camelContext);
+ // register compiled class for proper lifecycle management
+ byte[] byteCode = result.getByteCode(className);
+ classLoader.addClass(className, clazz, byteCode);
+ compiledClassNames.add(className);
} else {
answer = null;
}
diff --git
a/components/camel-joor/src/main/java/org/apache/camel/language/joor/MultiCompile.java
b/components/camel-joor/src/main/java/org/apache/camel/language/joor/MultiCompile.java
index 66905b1543d1..55f1ff0d6bf7 100644
---
a/components/camel-joor/src/main/java/org/apache/camel/language/joor/MultiCompile.java
+++
b/components/camel-joor/src/main/java/org/apache/camel/language/joor/MultiCompile.java
@@ -183,7 +183,7 @@ public final class MultiCompile {
// We need a private-access lookup from the class in that stack
frame in order to get
// private-access to any local interfaces at that location.
int index = 2;
- ByteArrayClassLoader c = new
ByteArrayClassLoader(fileManager.classes());
+ ByteArrayClassLoader c = new ByteArrayClassLoader(cl,
fileManager.classes());
for (CharSequenceJavaFileObject f : files) {
String className = f.getClassName();
diff --git
a/dsl/camel-java-joor-dsl/src/main/java/org/apache/camel/dsl/java/joor/JavaRoutesBuilderLoader.java
b/dsl/camel-java-joor-dsl/src/main/java/org/apache/camel/dsl/java/joor/JavaRoutesBuilderLoader.java
index d45ed2d6a406..70b7894cee59 100644
---
a/dsl/camel-java-joor-dsl/src/main/java/org/apache/camel/dsl/java/joor/JavaRoutesBuilderLoader.java
+++
b/dsl/camel-java-joor-dsl/src/main/java/org/apache/camel/dsl/java/joor/JavaRoutesBuilderLoader.java
@@ -158,7 +158,7 @@ public class JavaRoutesBuilderLoader extends
ExtendedRouteBuilderLoaderSupport {
// inject context and resource
CamelContextAware.trySetCamelContext(obj,
getCamelContext());
- ResourceAware.trySetResource(obj,
nameToResource.remove(className));
+ ResourceAware.trySetResource(obj,
nameToResource.get(className));
}
} catch (Exception e) {
throw new RuntimeCamelException("Cannot create
instance of class: " + className, e);
@@ -212,6 +212,25 @@ public class JavaRoutesBuilderLoader extends
ExtendedRouteBuilderLoaderSupport {
}
}
+ // During hot-reload only the changed file is passed in, but all Java
files
+ // must be recompiled together so class identity is consistent across
classloaders
+ for (Map.Entry<String, Resource> entry : nameToResource.entrySet()) {
+ if (!unit.getInput().containsKey(entry.getKey())) {
+ try (InputStream is = resourceInputStream(entry.getValue())) {
+ if (is != null) {
+ String content = IOHelper.loadText(is);
+ for (CompilePreProcessor pre :
getCompilePreProcessors()) {
+ pre.preCompile(getCamelContext(), entry.getKey(),
content);
+ }
+ unit.addClass(entry.getKey(), content);
+ classLoader.removeClass(entry.getKey());
+ }
+ } catch (Exception e) {
+ LOG.debug("Could not read previously compiled resource:
{}", entry.getValue(), e);
+ }
+ }
+ }
+
// include classloader from Camel, so we can load any already compiled
and loaded classes
ClassLoader parent = resolveParentClassLoader();
if (parent instanceof URLClassLoader ucl) {
@@ -227,7 +246,8 @@ public class JavaRoutesBuilderLoader extends
ExtendedRouteBuilderLoaderSupport {
// remember the last loaded resource-set if route reloading is enabled
if (getCamelContext().hasService(RouteWatcherReloadStrategy.class) !=
null) {
-
getCamelContext().getRegistry().bind(RouteWatcherReloadStrategy.RELOAD_RESOURCES,
nameToResource.values());
+
getCamelContext().getRegistry().bind(RouteWatcherReloadStrategy.RELOAD_RESOURCES,
+ new ArrayList<>(nameToResource.values()));
}
for (String className : result.getClassNames()) {