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()) {

Reply via email to