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 404e080ac23a CAMEL-25428: dev mode reload creates the beans again when 
a Java class changes (#27536)
404e080ac23a is described below

commit 404e080ac23a13e56834b3e8af564996acff414b
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 8 08:50:24 2026 +0200

    CAMEL-25428: dev mode reload creates the beans again when a Java class 
changes (#27536)
    
    A beans file with no routes, such as beans.yaml, was not part of a route
    reload: when a Java class changed and was compiled again, the routes
    went on calling the bean instance of the old class.
    
    When the Java sources are compiled again, the reload now loads the files
    that declare beans again too, so every bean is created again from the
    new classes. All of them: a bean that calls a changed bean needs its
    new instance as well.
    
    
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../camel/main/DefaultConfigurationConfigurer.java |  17 ++
 .../camel/support/RouteWatcherReloadStrategy.java  |  31 ++++
 .../camel/dsl/yaml/RouteReloadBeanClassTest.java   | 183 +++++++++++++++++++++
 3 files changed, 231 insertions(+)

diff --git 
a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java
 
b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java
index 31b1a5261125..4e11c0784e4e 100644
--- 
a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java
+++ 
b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java
@@ -19,6 +19,7 @@ package org.apache.camel.main;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -42,6 +43,7 @@ import org.apache.camel.impl.engine.PooledExchangeFactory;
 import org.apache.camel.impl.engine.PooledProcessorExchangeFactory;
 import org.apache.camel.impl.engine.PrototypeExchangeFactory;
 import org.apache.camel.impl.engine.PrototypeProcessorExchangeFactory;
+import org.apache.camel.model.BeanFactoryDefinition;
 import org.apache.camel.model.Model;
 import org.apache.camel.model.ModelCamelContext;
 import org.apache.camel.model.ModelLifecycleStrategy;
@@ -77,6 +79,7 @@ import org.apache.camel.spi.ProcessorFactory;
 import org.apache.camel.spi.PropertiesComponent;
 import org.apache.camel.spi.ReactiveExecutor;
 import org.apache.camel.spi.Registry;
+import org.apache.camel.spi.Resource;
 import org.apache.camel.spi.RouteController;
 import org.apache.camel.spi.RoutePolicyFactory;
 import org.apache.camel.spi.RuntimeEndpointRegistry;
@@ -303,6 +306,20 @@ public final class DefaultConfigurationConfigurer {
                     config.getRoutesReloadDirectory(), 
config.isRoutesReloadDirectoryRecursive());
             reloader.setPattern(config.getRoutesReloadPattern());
             
reloader.setRemoveAllRoutes(config.isRoutesReloadRemoveAllRoutes());
+            // the files that declare beans, so a bean is created again when 
its Java class changes
+            reloader.setBeanResources(() -> {
+                Model model = 
camelContext.getCamelContextExtension().getContextPlugin(Model.class);
+                if (model == null) {
+                    return null;
+                }
+                Set<Resource> answer = new LinkedHashSet<>();
+                for (BeanFactoryDefinition<?> bean : model.getCustomBeans()) {
+                    if (bean.getResource() != null) {
+                        answer.add(bean.getResource());
+                    }
+                }
+                return answer;
+            });
             camelContext.addService(reloader);
             // disable contentCache on resource-based components so that 
resource files (e.g. XSLT
             // stylesheets, templates) are reloaded live without restarting 
routes
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
index e26f5a1fb375..8d4254d492bd 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
@@ -30,6 +30,7 @@ import java.util.Objects;
 import java.util.Set;
 import java.util.StringJoiner;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
 
 import org.apache.camel.Route;
 import org.apache.camel.RuntimeCamelException;
@@ -75,6 +76,20 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
     private final Map<String, byte[]> lastGoodContent = new 
ConcurrentHashMap<>();
     /** The sources of the last failed reload: retried when the properties 
change, as a missing property may be why. */
     private final List<Resource> failedSources = new ArrayList<>();
+    private Supplier<Collection<Resource>> beanResources;
+
+    public Supplier<Collection<Resource>> getBeanResources() {
+        return beanResources;
+    }
+
+    /**
+     * The resources that declare beans, such as a beans.yaml file with no 
routes. When the Java sources are compiled
+     * again, all these resources are loaded again too, so every bean is 
created again from the new classes: a bean
+     * whose class changed, and also a bean that calls it (bean A calling bean 
B needs the new instance of B).
+     */
+    public void setBeanResources(Supplier<Collection<Resource>> beanResources) 
{
+        this.beanResources = beanResources;
+    }
 
     public RouteWatcherReloadStrategy() {
     }
@@ -388,6 +403,18 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
                     }
                 }
             }
+            // the Java sources are compiled again, so all beans are created 
again: a bean of a changed class, and the
+            // beans that call it
+            if (beanResources != null && 
sources.stream().anyMatch(RouteWatcherReloadStrategy::isJavaSource)) {
+                Collection<Resource> beans = beanResources.get();
+                if (beans != null) {
+                    for (Resource bean : beans) {
+                        if (bean != null && !equalResourceLocation(sources, 
bean)) {
+                            sources.add(bean);
+                        }
+                    }
+                }
+            }
 
             // just in case remember this set of sources as what was attempted 
previously to update
             // in case the update fails with an exception
@@ -572,6 +599,10 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
     /**
      * Whether the target is loading any of the given sources
      */
+    private static boolean isJavaSource(Resource resource) {
+        return resource.getLocation() != null && 
resource.getLocation().endsWith(".java");
+    }
+
     private static boolean equalResourceLocation(Collection<Resource> sources, 
Resource target) {
         if (sources == null || target == null || sources.isEmpty()) {
             return false;
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadBeanClassTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadBeanClassTest.java
new file mode 100644
index 000000000000..89978140a6f4
--- /dev/null
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadBeanClassTest.java
@@ -0,0 +1,183 @@
+/*
+ * 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.
+ */
+package org.apache.camel.dsl.yaml;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.camel.dsl.yaml.support.YamlTestSupport;
+import org.apache.camel.model.BeanFactoryDefinition;
+import org.apache.camel.model.Model;
+import org.apache.camel.spi.Resource;
+import org.apache.camel.support.PluginHelper;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.support.RouteWatcherReloadStrategy;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-25428: when a Java class changes, the beans of a beans file are 
created again, so the routes call the new code:
+ * both a bean of the changed class and a bean that calls it.
+ */
+class RouteReloadBeanClassTest extends YamlTestSupport {
+
+    /** Subclass to expose protected lifecycle methods for testing without a 
live file watcher. */
+    static class TestableStrategy extends RouteWatcherReloadStrategy {
+        TestableStrategy(String dir) {
+            super(dir);
+        }
+
+        @Override
+        public void doStart() throws Exception {
+            super.doStart();
+        }
+
+        @Override
+        public void doStop() throws Exception {
+            super.doStop();
+        }
+    }
+
+    private Path dir;
+    private Path punctuation;
+
+    @Override
+    public void doSetup() throws Exception {
+        dir = Files.createTempDirectory("camel-reload");
+        punctuation = dir.resolve("Punctuation.java");
+        writePunctuation("!");
+        Path greeter = dir.resolve("Greeter.java");
+        Files.writeString(greeter, """
+                package com.example;
+
+                public class Greeter {
+                    private Punctuation punctuation;
+
+                    public void setPunctuation(Punctuation punctuation) {
+                        this.punctuation = punctuation;
+                    }
+
+                    public String greet(String name) {
+                        return "Hello " + name + punctuation.mark();
+                    }
+                }
+                """);
+        Path beans = dir.resolve("beans.yaml");
+        Files.writeString(beans, """
+                - beans:
+                    - name: punctuation
+                      type: com.example.Punctuation
+                    - name: greeter
+                      type: com.example.Greeter
+                      properties:
+                        punctuation: "#bean:punctuation"
+                """);
+        Path route = dir.resolve("route.camel.yaml");
+        Files.writeString(route, """
+                - route:
+                    id: greeting
+                    from:
+                      uri: direct:greet
+                      steps:
+                        - bean:
+                            ref: greeter
+                            method: greet
+                """);
+        context.start();
+        PluginHelper.getRoutesLoader(context).loadRoutes(List.of(
+                ResourceHelper.resolveResource(context, "file:" + route),
+                ResourceHelper.resolveResource(context, "file:" + beans),
+                ResourceHelper.resolveResource(context, "file:" + punctuation),
+                ResourceHelper.resolveResource(context, "file:" + greeter)));
+    }
+
+    @Override
+    public void doCleanup() throws Exception {
+        deleteRecursively(dir);
+    }
+
+    @Test
+    void theBeansAreCreatedAgainWhenAClassChanges() throws Exception {
+        TestableStrategy strategy = new TestableStrategy(dir.toString());
+        strategy.setCamelContext(context);
+        strategy.setPattern("*");
+        // the same as camel-main: the files that declare beans
+        strategy.setBeanResources(() -> {
+            Set<Resource> answer = new LinkedHashSet<>();
+            for (BeanFactoryDefinition<?> bean : 
context.getCamelContextExtension().getContextPlugin(Model.class)
+                    .getCustomBeans()) {
+                answer.add(bean.getResource());
+            }
+            return answer;
+        });
+        // the strategy is not started (no file watcher in the test): its 
reload callback is driven by hand
+        strategy.doStart();
+        try {
+            
assertThat(context.createProducerTemplate().requestBody("direct:greet", 
"Camel", String.class))
+                    .isEqualTo("Hello Camel!");
+
+            // only the class of the bean that greeter calls changes
+            writePunctuation("?");
+            strategy.getResourceReload().onReload(punctuation.toString(),
+                    ResourceHelper.resolveResource(context, "file:" + 
punctuation));
+
+            
assertThat(context.createProducerTemplate().requestBody("direct:greet", 
"Camel", String.class))
+                    .isEqualTo("Hello Camel?");
+        } finally {
+            strategy.doStop();
+        }
+    }
+
+    private void writePunctuation(String mark) throws IOException {
+        Files.writeString(punctuation, """
+                package com.example;
+
+                public class Punctuation {
+                    public String mark() {
+                        return "%s";
+                    }
+                }
+                """.formatted(mark));
+    }
+
+    private static void deleteRecursively(Path path) throws IOException {
+        if (path == null || !Files.exists(path)) {
+            return;
+        }
+        Files.walkFileTree(path, new SimpleFileVisitor<>() {
+            @Override
+            public FileVisitResult visitFile(Path file, BasicFileAttributes 
attrs) throws IOException {
+                Files.delete(file);
+                return FileVisitResult.CONTINUE;
+            }
+
+            @Override
+            public FileVisitResult postVisitDirectory(Path dir, IOException 
exc) throws IOException {
+                Files.delete(dir);
+                return FileVisitResult.CONTINUE;
+            }
+        });
+    }
+}

Reply via email to