This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch init-bean in repository https://gitbox.apache.org/repos/asf/camel.git
commit a81f7cf409537155e90ffd30f3a5237507276f12 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Sep 27 08:15:08 2023 +0200 CAMEL-19808: camel-core-model - Add init and destroy method to <bean> --- .../org/apache/camel/support/ObjectHelper.java | 32 ++++++++++ .../camel/dsl/xml/io/XmlRoutesBuilderLoader.java | 71 ++++++++++++++++++---- .../apache/camel/dsl/xml/io/XmlLoadAppTest.java | 37 +++++++++++ .../camel/dsl/xml/io/beans/MyDestroyBean.java | 63 +++++++++++++++++++ .../org/apache/camel/dsl/xml/io/camel-app8.xml | 39 ++++++++++++ 5 files changed, 229 insertions(+), 13 deletions(-) diff --git a/core/camel-support/src/main/java/org/apache/camel/support/ObjectHelper.java b/core/camel-support/src/main/java/org/apache/camel/support/ObjectHelper.java index 1add25b7d88..decca85f8b6 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/ObjectHelper.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/ObjectHelper.java @@ -39,6 +39,7 @@ import org.apache.camel.Message; import org.apache.camel.Ordered; import org.apache.camel.RuntimeCamelException; import org.apache.camel.TypeConverter; +import org.apache.camel.util.ReflectionHelper; import org.apache.camel.util.Scanner; import org.apache.camel.util.StringHelper; @@ -386,6 +387,37 @@ public final class ObjectHelper { return answer; } + /** + * A helper method to invoke a method via reflection in a safe way by allowing to invoke methods that are not + * accessible by default and wrap any exceptions as {@link RuntimeCamelException} instances + * + * @param name the method name + * @param instance the object instance (or null for static methods) + * @param parameters the parameters to the method + * @return the result of the method invocation + */ + public static Object invokeMethodSafe(String name, Object instance, Object... parameters) + throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + + // find method first + Class<?>[] arr = null; + if (parameters != null) { + arr = new Class[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + Object p = parameters[i]; + if (p != null) { + arr[i] = p.getClass(); + } + } + } + Method m = ReflectionHelper.findMethod(instance.getClass(), name, arr); + if (m != null) { + return invokeMethodSafe(m, instance, parameters); + } else { + throw new NoSuchMethodException(name); + } + } + /** * A helper method to create a new instance of a type using the default constructor arguments. */ diff --git a/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java b/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java index 604378a65ba..d44a6941260 100644 --- a/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java +++ b/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java @@ -18,6 +18,7 @@ package org.apache.camel.dsl.xml.io; import java.io.IOException; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -50,8 +51,10 @@ import org.apache.camel.model.rest.RestsDefinition; import org.apache.camel.spi.Resource; import org.apache.camel.spi.annotations.RoutesLoader; import org.apache.camel.support.CachedResource; +import org.apache.camel.support.ObjectHelper; import org.apache.camel.support.PropertyBindingSupport; import org.apache.camel.support.scan.PackageScanHelper; +import org.apache.camel.util.KeyValueHolder; import org.apache.camel.util.StringHelper; import org.apache.camel.xml.io.util.XmlStreamDetector; import org.apache.camel.xml.io.util.XmlStreamInfo; @@ -71,6 +74,7 @@ public class XmlRoutesBuilderLoader extends RouteBuilderLoaderSupport { private final Map<String, XmlStreamInfo> xmlInfoCache = new ConcurrentHashMap<>(); private final Map<String, BeansDefinition> camelAppCache = new ConcurrentHashMap<>(); private final List<RegistryBeanDefinition> delayedRegistrations = new ArrayList<>(); + private final Map<String, KeyValueHolder<Object, String>> beansToDestroy = new LinkedHashMap<>(); private final AtomicInteger counter = new AtomicInteger(0); @@ -137,14 +141,12 @@ public class XmlRoutesBuilderLoader extends RouteBuilderLoaderSupport { new XmlModelParser(resource, xmlInfo.getRootElementNamespace()) .parseTemplatedRoutesDefinition() .ifPresent(this::setTemplatedRouteCollection); - case "rests", "rest" -> - new XmlModelParser(resource, xmlInfo.getRootElementNamespace()) - .parseRestsDefinition() - .ifPresent(this::setRestCollection); - case "routes", "route" -> - new XmlModelParser(resource, xmlInfo.getRootElementNamespace()) - .parseRoutesDefinition() - .ifPresent(this::addRoutes); + case "rests", "rest" -> new XmlModelParser(resource, xmlInfo.getRootElementNamespace()) + .parseRestsDefinition() + .ifPresent(this::setRestCollection); + case "routes", "route" -> new XmlModelParser(resource, xmlInfo.getRootElementNamespace()) + .parseRoutesDefinition() + .ifPresent(this::addRoutes); default -> { } } @@ -353,12 +355,8 @@ public class XmlRoutesBuilderLoader extends RouteBuilderLoaderSupport { if (def.getProperties() != null && !def.getProperties().isEmpty()) { PropertyBindingSupport.setPropertiesOnTarget(getCamelContext(), target, def.getProperties()); } - getCamelContext().getRegistry().unbind(name); - getCamelContext().getRegistry().bind(name, target); - // register bean in model - Model model = getCamelContext().getCamelContextExtension().getContextPlugin(Model.class); - model.addRegistryBean(def); + bindBean(def, name, target); } catch (Exception e) { if (delayIfFailed) { @@ -370,4 +368,51 @@ public class XmlRoutesBuilderLoader extends RouteBuilderLoaderSupport { } } + protected void bindBean(RegistryBeanDefinition def, String name, Object target) throws Exception { + // destroy and unbind any existing bean + destroyBean(name, true); + getCamelContext().getRegistry().unbind(name); + + // invoke init method and register bean + String initMethod = def.getInitMethod(); + if (initMethod != null) { + ObjectHelper.invokeMethodSafe(initMethod, target); + } + getCamelContext().getRegistry().bind(name, target); + + // remember to destroy bean on shutdown + if (def.getDestroyMethod() != null) { + beansToDestroy.put(name, new KeyValueHolder<>(target, def.getDestroyMethod())); + } + + // register bean in model + Model model = getCamelContext().getCamelContextExtension().getContextPlugin(Model.class); + model.addRegistryBean(def); + } + + protected void destroyBean(String name, boolean remove) { + var holder = remove ? beansToDestroy.remove(name) : beansToDestroy.get(name); + if (holder != null) { + String destroyMethod = holder.getValue(); + Object target = holder.getKey(); + try { + ObjectHelper.invokeMethodSafe(destroyMethod, target); + } catch (Exception e) { + LOG.warn("Error invoking destroy method: {} on bean: {} due to: {}. This exception is ignored.", + destroyMethod, target, e.getMessage(), e); + } + } + } + + @Override + protected void doStop() throws Exception { + super.doStop(); + + // beans should trigger destroy methods on shutdown + for (String name : beansToDestroy.keySet()) { + destroyBean(name, false); + } + beansToDestroy.clear(); + } + } diff --git a/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlLoadAppTest.java b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlLoadAppTest.java index 3f031dccd43..d5984129b8d 100644 --- a/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlLoadAppTest.java +++ b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlLoadAppTest.java @@ -18,6 +18,7 @@ package org.apache.camel.dsl.xml.io; import org.apache.camel.component.mock.MockEndpoint; import org.apache.camel.dsl.xml.io.beans.GreeterMessage; +import org.apache.camel.dsl.xml.io.beans.MyDestroyBean; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.spi.Resource; import org.apache.camel.spi.RoutesLoader; @@ -25,7 +26,9 @@ import org.apache.camel.support.PluginHelper; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; public class XmlLoadAppTest { @@ -190,4 +193,38 @@ public class XmlLoadAppTest { } } + @Test + public void testLoadCamelAppWithBeanInitDestroy() throws Exception { + try (DefaultCamelContext context = new DefaultCamelContext()) { + context.start(); + + assertFalse(MyDestroyBean.initCalled.get()); + assertFalse(MyDestroyBean.destroyCalled.get()); + + Resource resource = PluginHelper.getResourceLoader(context).resolveResource( + "/org/apache/camel/dsl/xml/io/camel-app8.xml"); + + RoutesLoader routesLoader = PluginHelper.getRoutesLoader(context); + routesLoader.preParseRoute(resource, false); + routesLoader.loadRoutes(resource); + + assertNotNull(context.getRoute("r8"), "Loaded r8 route should be there"); + assertEquals(1, context.getRoutes().size()); + + // test that loaded route works + MockEndpoint y8 = context.getEndpoint("mock:y8", MockEndpoint.class); + y8.expectedBodiesReceived("Hello Neptun. I am Camel and 45 years old!"); + context.createProducerTemplate().sendBody("direct:x8", "Neptun"); + y8.assertIsSatisfied(); + + assertTrue(MyDestroyBean.initCalled.get()); + assertFalse(MyDestroyBean.destroyCalled.get()); + + context.stop(); + + assertTrue(MyDestroyBean.initCalled.get()); + assertTrue(MyDestroyBean.destroyCalled.get()); + } + } + } diff --git a/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/beans/MyDestroyBean.java b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/beans/MyDestroyBean.java new file mode 100644 index 00000000000..e92c8bf6207 --- /dev/null +++ b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/beans/MyDestroyBean.java @@ -0,0 +1,63 @@ +/* + * 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.xml.io.beans; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class MyDestroyBean { + + public static final AtomicBoolean initCalled = new AtomicBoolean(); + public static final AtomicBoolean destroyCalled = new AtomicBoolean(); + + private String field1; + private String field2; + private int age; + + public MyDestroyBean(String field1, String field2) { + this.field1 = field1; + this.field2 = field2; + } + + public String getField1() { + return field1; + } + + public String getField2() { + return field2; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String hello(String body) { + return field1 + " " + body + ". I am " + field2 + " and " + age + " years old!"; + } + + public void initMe() { + initCalled.set(true); + } + + public void destroyMe() { + destroyCalled.set(true); + } + +} diff --git a/dsl/camel-xml-io-dsl/src/test/resources/org/apache/camel/dsl/xml/io/camel-app8.xml b/dsl/camel-xml-io-dsl/src/test/resources/org/apache/camel/dsl/xml/io/camel-app8.xml new file mode 100644 index 00000000000..35de6096a77 --- /dev/null +++ b/dsl/camel-xml-io-dsl/src/test/resources/org/apache/camel/dsl/xml/io/camel-app8.xml @@ -0,0 +1,39 @@ +<?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. + +--> +<camel xmlns="http://camel.apache.org/schema/spring" xmlns:s="http://www.springframework.org/schema/beans"> + + <bean name="xml-bean-from-registry" type="org.apache.camel.dsl.xml.io.beans.MyDestroyBean" + initMethod="initMe" destroyMethod="destroyMe"> + <constructors> + <constructor value="Hello"/> + <constructor value="Camel"/> + </constructors> + <properties> + <property key="age" value="45"/> + </properties> + </bean> + + <route id="r8"> + <from uri="direct:x8"/> + <bean ref="xml-bean-from-registry" method="hello"/> + <to uri="mock:y8"/> + </route> + +</camel>
