This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-24879 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 5757a196c2b5dda1866779a2ee40aec052a84c6f Author: Claus Ibsen <[email protected]> AuthorDate: Mon Sep 21 18:59:29 2026 +0200 CAMEL-24879: camel-groovy - Bind @BindToRegistry and @Converter classes from Groovy sources in every runtime The Camel-annotation compile post-processors move from camel-kamelet-main to camel-support (org.apache.camel.support.compile). DefaultGroovyScriptCompiler uses the processors in the registry when there are any (camel-jbang), and otherwise these built-in ones, so a .groovy file with @BindToRegistry or @Converter also works after camel export, in camel-main, Spring Boot and Quarkus. camel-main is not changed. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../apache/camel/catalog/docs/groovy-language.adoc | 12 +- .../apache/camel/catalog/docs/simple-advanced.adoc | 7 +- .../src/main/docs/groovy-language.adoc | 12 +- .../groovy/DefaultGroovyScriptCompiler.java | 28 +++-- .../GroovyDefaultCompilePostProcessorTest.java | 87 +++++++++++++++ .../camel-groovy-default/MaskEmailFunction.groovy | 34 ++++++ .../camel-groovy-default/OrderConverter.groovy | 27 +++++ .../modules/languages/pages/simple-advanced.adoc | 7 +- .../BindToRegistryCompilePostProcessor.java | 105 ++++++++++++++++++ .../compile/EventNotifierCompilePostProcessor.java | 53 +++++++++ .../compile/TypeConverterCompilePostProcessor.java | 50 +++++++++ .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 17 ++- .../modules/ROOT/pages/camel-jbang-beans.adoc | 7 +- .../injection/AnnotationDependencyInjection.java | 123 +-------------------- 14 files changed, 418 insertions(+), 151 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/groovy-language.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/groovy-language.adoc index 6806dcf3ad4a..9d51d0d55a13 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/groovy-language.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/groovy-language.adoc @@ -192,11 +192,13 @@ with Camel features that would support this such as in the route DSL and elsewhe However, there may be some features in Camel where this may not work (yet). -A compiled class with class-level annotations is handed to the registered `org.apache.camel.spi.CompilePostProcessor`s -with a new instance, as the Java DSL does for `.java` sources. With Camel CLI (`camel run`) that binds a -`@BindToRegistry` class in the Registry and registers a `@Converter` class as type converters (the Spring -and Quarkus annotations are supported as well), so a Groovy source can provide a bean such as a custom -xref:languages:simple-advanced.adoc[simple function]. Plain classes without annotations are not instantiated. +A compiled class with class-level annotations is handed to the `org.apache.camel.spi.CompilePostProcessor`s with a +new instance, as the Java DSL does for `.java` sources. The processors in the Registry are used when there are any +(Camel CLI registers processors that also handle the Spring and Quarkus annotations); otherwise the built-in +processors for the Camel annotations are used. So in every runtime a `@BindToRegistry` class is bound in the +Registry, a `@Converter` class is registered as type converters, and an `EventNotifier` class is added, and a Groovy +source can provide a bean such as a custom xref:languages:simple-advanced.adoc[simple function]. Plain classes +without annotations are not instantiated. IMPORTANT: This feature is only intended to include smaller groovy sources as small functions, DTOs that makes it easier to use together with Camel for low-code integrations. It is not diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-advanced.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-advanced.adoc index 70cc13c280e0..ad5014dfab36 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-advanced.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-advanced.adoc @@ -1056,9 +1056,10 @@ In dev mode (`--dev`) an edit of the function, in the `.groovy` file or in the Y message: the bean is created again on reload and the simple expression resolves the function again per evaluation when the `dev` profile is active. -NOTE: The `@BindToRegistry` binding of a `.groovy` file is done by the Camel CLI. In a project created with -`camel export` the file is compiled, but the class is not bound as a bean, so the function is unknown. For a project -that is exported, use the inline YAML bean shown above, which works in every runtime, or write the function in Java. +NOTE: The `.groovy` file keeps working in a project created with `camel export`: the Groovy compiler in camel-groovy +binds a `@BindToRegistry` class itself, so the function is available to Camel in camel-main, Spring Boot and Quarkus +alike. The bean lives in Camel's registry, not in the Spring or CDI container, so it is not a candidate for +`@Autowired` or `@Inject`; for that, write the class in Java. == JavaScript Validator diff --git a/components/camel-groovy/src/main/docs/groovy-language.adoc b/components/camel-groovy/src/main/docs/groovy-language.adoc index 6806dcf3ad4a..9d51d0d55a13 100644 --- a/components/camel-groovy/src/main/docs/groovy-language.adoc +++ b/components/camel-groovy/src/main/docs/groovy-language.adoc @@ -192,11 +192,13 @@ with Camel features that would support this such as in the route DSL and elsewhe However, there may be some features in Camel where this may not work (yet). -A compiled class with class-level annotations is handed to the registered `org.apache.camel.spi.CompilePostProcessor`s -with a new instance, as the Java DSL does for `.java` sources. With Camel CLI (`camel run`) that binds a -`@BindToRegistry` class in the Registry and registers a `@Converter` class as type converters (the Spring -and Quarkus annotations are supported as well), so a Groovy source can provide a bean such as a custom -xref:languages:simple-advanced.adoc[simple function]. Plain classes without annotations are not instantiated. +A compiled class with class-level annotations is handed to the `org.apache.camel.spi.CompilePostProcessor`s with a +new instance, as the Java DSL does for `.java` sources. The processors in the Registry are used when there are any +(Camel CLI registers processors that also handle the Spring and Quarkus annotations); otherwise the built-in +processors for the Camel annotations are used. So in every runtime a `@BindToRegistry` class is bound in the +Registry, a `@Converter` class is registered as type converters, and an `EventNotifier` class is added, and a Groovy +source can provide a bean such as a custom xref:languages:simple-advanced.adoc[simple function]. Plain classes +without annotations are not instantiated. IMPORTANT: This feature is only intended to include smaller groovy sources as small functions, DTOs that makes it easier to use together with Camel for low-code integrations. It is not diff --git a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/DefaultGroovyScriptCompiler.java b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/DefaultGroovyScriptCompiler.java index 6d493c05ec1d..628742204522 100644 --- a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/DefaultGroovyScriptCompiler.java +++ b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/DefaultGroovyScriptCompiler.java @@ -47,6 +47,9 @@ import org.apache.camel.spi.Resource; import org.apache.camel.spi.annotations.JdkService; import org.apache.camel.support.PluginHelper; import org.apache.camel.support.SimpleEventNotifierSupport; +import org.apache.camel.support.compile.BindToRegistryCompilePostProcessor; +import org.apache.camel.support.compile.EventNotifierCompilePostProcessor; +import org.apache.camel.support.compile.TypeConverterCompilePostProcessor; import org.apache.camel.support.service.ServiceSupport; import org.apache.camel.util.FileUtil; import org.apache.camel.util.IOHelper; @@ -68,6 +71,7 @@ public class DefaultGroovyScriptCompiler extends ServiceSupport private static final Logger LOG = LoggerFactory.getLogger(DefaultGroovyScriptCompiler.class); private GroovyPreCompiledClassLoader groovyPreCompiledClassLoader; + private List<CompilePostProcessor> defaultPostProcessors; private GroovyScriptClassLoader classLoader; private CamelContext camelContext; private EventNotifier notifier; @@ -374,21 +378,29 @@ public class DefaultGroovyScriptCompiler extends ServiceSupport } /** - * Runs the registered {@link CompilePostProcessor}s on a compiled class, as the Java DSL loader does for - * {@code .java} sources, so annotations such as {@link BindToRegistry} and {@link org.apache.camel.Converter} (and - * the Spring and Quarkus equivalents camel-jbang registers) work in Groovy sources as well. On a recompile (live - * reload) the bean is created and bound again, replacing the previous one. + * Runs the {@link CompilePostProcessor}s on a compiled class, as the Java DSL loader does for {@code .java} + * sources, so annotations such as {@link BindToRegistry} and {@link org.apache.camel.Converter} work in Groovy + * sources as well. The processors in the registry are used when there are any (camel-jbang registers processors + * that also handle the Spring and Quarkus annotations); otherwise the built-in processors for the Camel annotations + * are used, so a Groovy source works the same in every runtime. On a recompile (live reload) the bean is created + * and bound again, replacing the previous one. */ private void postCompile(Class<?> clazz, byte[] byteCode) throws Exception { - Set<CompilePostProcessor> posts = camelContext.getRegistry().findByType(CompilePostProcessor.class); - if (posts == null || posts.isEmpty()) { - return; - } // only annotated classes are instantiated: a plain groovy class or script is a DTO or a // function library, and creating it here would only run its constructor for nothing if (clazz.getAnnotations().length == 0 || Script.class.isAssignableFrom(clazz)) { return; } + Collection<CompilePostProcessor> posts = camelContext.getRegistry().findByType(CompilePostProcessor.class); + if (posts == null || posts.isEmpty()) { + if (defaultPostProcessors == null) { + defaultPostProcessors = List.of( + new TypeConverterCompilePostProcessor(), + new EventNotifierCompilePostProcessor(), + new BindToRegistryCompilePostProcessor()); + } + posts = defaultPostProcessors; + } Object instance = null; BindToRegistry bir = clazz.getAnnotation(BindToRegistry.class); boolean skip = clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers()) diff --git a/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyDefaultCompilePostProcessorTest.java b/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyDefaultCompilePostProcessorTest.java new file mode 100644 index 000000000000..c613b8be4769 --- /dev/null +++ b/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyDefaultCompilePostProcessorTest.java @@ -0,0 +1,87 @@ +/* + * 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.language.groovy; + +import org.apache.camel.CamelContext; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.spi.CompilePostProcessor; +import org.apache.camel.spi.SimpleFunction; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Without any {@link CompilePostProcessor} in the registry (as in a camel-main, Spring Boot or Quarkus application, + * unlike camel-jbang) the compiler falls back to the built-in processors for the Camel annotations. + */ +public class GroovyDefaultCompilePostProcessorTest extends CamelTestSupport { + + @Override + protected CamelContext createCamelContext() throws Exception { + CamelContext context = super.createCamelContext(); + + DefaultGroovyScriptCompiler compiler = new DefaultGroovyScriptCompiler(); + compiler.setCamelContext(context); + compiler.setScriptPattern("file:src/test/resources/camel-groovy-default/*"); + context.addService(compiler); + + return context; + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:mask") + .setBody().simple("${maskEmail(${body})}") + .to("mock:result"); + + from("direct:order") + .convertBodyTo(Order.class) + .setBody().simple("${body.id}") + .to("mock:result"); + } + }; + } + + @Test + public void testBindToRegistryWithoutRegisteredPostProcessor() throws Exception { + assertTrue(context.getRegistry().findByType(CompilePostProcessor.class).isEmpty()); + assertInstanceOf(SimpleFunction.class, context.getRegistry().lookupByName("mask-email-function")); + + getMockEndpoint("mock:result").expectedBodiesReceived("j***@example.com"); + template.sendBody("direct:mask", "[email protected]"); + MockEndpoint.assertIsSatisfied(context); + } + + @Test + public void testConverterWithoutRegisteredPostProcessor() throws Exception { + assertNotNull(context.getTypeConverterRegistry().lookup(Order.class, String.class)); + + getMockEndpoint("mock:result").expectedBodiesReceived("123"); + template.sendBody("direct:order", " 123 "); + MockEndpoint.assertIsSatisfied(context); + } + + public record Order(String id) { + } +} diff --git a/components/camel-groovy/src/test/resources/camel-groovy-default/MaskEmailFunction.groovy b/components/camel-groovy/src/test/resources/camel-groovy-default/MaskEmailFunction.groovy new file mode 100644 index 000000000000..c9e578c534fa --- /dev/null +++ b/components/camel-groovy/src/test/resources/camel-groovy-default/MaskEmailFunction.groovy @@ -0,0 +1,34 @@ +/* + * 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. + */ + +import org.apache.camel.BindToRegistry +import org.apache.camel.Exchange +import org.apache.camel.spi.SimpleFunction + +@BindToRegistry("mask-email-function") +class MaskEmailFunction implements SimpleFunction { + + String getName() { 'maskEmail' } + + Object apply(Exchange exchange, Object input) { + def email = input.toString().trim() + int at = email.indexOf('@') + if (at <= 0 || at == email.length() - 1) return '***' + return email[0] + '***' + email.substring(at) + } + +} diff --git a/components/camel-groovy/src/test/resources/camel-groovy-default/OrderConverter.groovy b/components/camel-groovy/src/test/resources/camel-groovy-default/OrderConverter.groovy new file mode 100644 index 000000000000..de8338abc6ad --- /dev/null +++ b/components/camel-groovy/src/test/resources/camel-groovy-default/OrderConverter.groovy @@ -0,0 +1,27 @@ +/* + * 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. + */ +import org.apache.camel.Converter +import org.apache.camel.language.groovy.GroovyDefaultCompilePostProcessorTest.Order + +@Converter +class OrderConverter { + + @Converter + static Order toOrder(String body) { + new Order(body.trim()) + } +} diff --git a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-advanced.adoc b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-advanced.adoc index 70cc13c280e0..ad5014dfab36 100644 --- a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-advanced.adoc +++ b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-advanced.adoc @@ -1056,9 +1056,10 @@ In dev mode (`--dev`) an edit of the function, in the `.groovy` file or in the Y message: the bean is created again on reload and the simple expression resolves the function again per evaluation when the `dev` profile is active. -NOTE: The `@BindToRegistry` binding of a `.groovy` file is done by the Camel CLI. In a project created with -`camel export` the file is compiled, but the class is not bound as a bean, so the function is unknown. For a project -that is exported, use the inline YAML bean shown above, which works in every runtime, or write the function in Java. +NOTE: The `.groovy` file keeps working in a project created with `camel export`: the Groovy compiler in camel-groovy +binds a `@BindToRegistry` class itself, so the function is available to Camel in camel-main, Spring Boot and Quarkus +alike. The bean lives in Camel's registry, not in the Spring or CDI container, so it is not a candidate for +`@Autowired` or `@Inject`; for that, write the class in Java. == JavaScript Validator diff --git a/core/camel-support/src/main/java/org/apache/camel/support/compile/BindToRegistryCompilePostProcessor.java b/core/camel-support/src/main/java/org/apache/camel/support/compile/BindToRegistryCompilePostProcessor.java new file mode 100644 index 000000000000..5740c8ffe1b2 --- /dev/null +++ b/core/camel-support/src/main/java/org/apache/camel/support/compile/BindToRegistryCompilePostProcessor.java @@ -0,0 +1,105 @@ +/* + * 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.support.compile; + +import java.util.function.Supplier; + +import org.apache.camel.BindToRegistry; +import org.apache.camel.CamelConfiguration; +import org.apache.camel.CamelContext; +import org.apache.camel.Configuration; +import org.apache.camel.RuntimeCamelException; +import org.apache.camel.spi.CamelBeanPostProcessor; +import org.apache.camel.spi.CompilePostProcessor; +import org.apache.camel.support.PluginHelper; +import org.apache.camel.util.ObjectHelper; + +/** + * Binds a compiled class annotated with {@link BindToRegistry} or {@link Configuration} (or that is a + * {@link CamelConfiguration}) to the registry, using the {@link CamelBeanPostProcessor} so the Camel dependency + * injection annotations on the class are processed as well. As the class may be compiled again (live reload) the + * previous bean is unbound first. + */ +public class BindToRegistryCompilePostProcessor implements CompilePostProcessor { + + private final boolean lazyBean; + + public BindToRegistryCompilePostProcessor() { + this(false); + } + + /** + * @param lazyBean whether to bind every {@link BindToRegistry} bean lazily (created on first use), as if + * {@link BindToRegistry#lazy()} was set + */ + public BindToRegistryCompilePostProcessor(boolean lazyBean) { + this.lazyBean = lazyBean; + } + + @Override + public void postCompile(CamelContext camelContext, String name, Class<?> clazz, byte[] byteCode, Object instance) + throws Exception { + + BindToRegistry bir = clazz.getAnnotation(BindToRegistry.class); + Configuration cfg = clazz.getAnnotation(Configuration.class); + + // special for lazy beans which we must create on-demand + if (instance == null && bir != null && (lazyBean || bir.lazy())) { + final String beanName = bir.value(); + instance = (Supplier<Object>) () -> { + Object answer = camelContext.getInjector().newInstance(clazz); + CamelBeanPostProcessor bpp = PluginHelper.getBeanPostProcessor(camelContext); + try { + bpp.postProcessBeforeInitialization(answer, beanName); + bpp.postProcessAfterInitialization(answer, beanName); + } catch (Exception e) { + throw RuntimeCamelException.wrapRuntimeException(e); + } + return answer; + }; + // unbind old bean and register lazy bean + camelContext.getRegistry().unbind(beanName); + // use dependency injection factory to perform the task of binding the bean to registry + Runnable task = PluginHelper.getDependencyInjectionAnnotationFactory(camelContext) + .createBindToRegistryFactory(name, instance, clazz, beanName, false, bir.initMethod(), + bir.destroyMethod()); + task.run(); + } else { + if (bir != null || cfg != null || instance instanceof CamelConfiguration) { + CamelBeanPostProcessor bpp = PluginHelper.getBeanPostProcessor(camelContext); + if (bir != null && ObjectHelper.isNotEmpty(bir.value())) { + name = bir.value(); + } else if (cfg != null && ObjectHelper.isNotEmpty(cfg.value())) { + name = cfg.value(); + } + // to support hot reloading of beans then we need to enable unbind mode in bean post processor + bpp.setUnbindEnabled(true); + try { + // this class uses camels own annotations so the bind to registry happens + // automatic by the bean post processor + bpp.postProcessBeforeInitialization(instance, name); + bpp.postProcessAfterInitialization(instance, name); + } finally { + bpp.setUnbindEnabled(false); + } + if (instance instanceof CamelConfiguration cc) { + cc.configure(camelContext); + } + } + } + } +} diff --git a/core/camel-support/src/main/java/org/apache/camel/support/compile/EventNotifierCompilePostProcessor.java b/core/camel-support/src/main/java/org/apache/camel/support/compile/EventNotifierCompilePostProcessor.java new file mode 100644 index 000000000000..393678b8e32f --- /dev/null +++ b/core/camel-support/src/main/java/org/apache/camel/support/compile/EventNotifierCompilePostProcessor.java @@ -0,0 +1,53 @@ +/* + * 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.support.compile; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.spi.CompilePostProcessor; +import org.apache.camel.spi.EventNotifier; +import org.apache.camel.spi.ManagementStrategy; + +/** + * Adds a compiled class that is an {@link EventNotifier} to the {@link ManagementStrategy}. As the class may be + * compiled again (live reload) the previous notifier of the same name is removed first. + */ +public class EventNotifierCompilePostProcessor implements CompilePostProcessor { + + private final Map<String, EventNotifier> notifiers = new HashMap<>(); + + @Override + public void postCompile(CamelContext camelContext, String name, Class<?> clazz, byte[] byteCode, Object instance) + throws Exception { + if (instance instanceof EventNotifier en) { + ManagementStrategy ms = camelContext.getManagementStrategy(); + if (ms != null) { + notifiers.compute(name, (key, old) -> { + // remove previous instance + if (old != null) { + ms.removeEventNotifier(old); + } + // and new notifier + ms.addEventNotifier(en); + return en; + }); + } + } + } +} diff --git a/core/camel-support/src/main/java/org/apache/camel/support/compile/TypeConverterCompilePostProcessor.java b/core/camel-support/src/main/java/org/apache/camel/support/compile/TypeConverterCompilePostProcessor.java new file mode 100644 index 000000000000..c25ba37508fd --- /dev/null +++ b/core/camel-support/src/main/java/org/apache/camel/support/compile/TypeConverterCompilePostProcessor.java @@ -0,0 +1,50 @@ +/* + * 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.support.compile; + +import org.apache.camel.CamelContext; +import org.apache.camel.Converter; +import org.apache.camel.LoggingLevel; +import org.apache.camel.TypeConverterExists; +import org.apache.camel.spi.CompilePostProcessor; +import org.apache.camel.spi.TypeConverterRegistry; + +/** + * Registers a compiled class annotated with {@link Converter} as type converters. As the class may be compiled again + * (live reload) the converters override any existing ones. + */ +public class TypeConverterCompilePostProcessor implements CompilePostProcessor { + + @Override + public void postCompile(CamelContext camelContext, String name, Class<?> clazz, byte[] byteCode, Object instance) + throws Exception { + if (clazz.isAnnotationPresent(Converter.class)) { + TypeConverterRegistry tcr = camelContext.getTypeConverterRegistry(); + TypeConverterExists exists = tcr.getTypeConverterExists(); + LoggingLevel level = tcr.getTypeConverterExistsLoggingLevel(); + // force type converter to override as we could be re-loading + tcr.setTypeConverterExists(TypeConverterExists.Override); + tcr.setTypeConverterExistsLoggingLevel(LoggingLevel.OFF); + try { + tcr.addTypeConverters(clazz); + } finally { + tcr.setTypeConverterExists(exists); + tcr.setTypeConverterExistsLoggingLevel(level); + } + } + } +} diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc index 95a24578bb36..a864081edc8f 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc @@ -140,11 +140,18 @@ script languages and the Camel 4 API use; `request` and `in` stay as its older n provided its own global variable named `message` is now hidden by the exchange variable, like the other exchange variable names. -The Groovy script compiler now runs the registered `org.apache.camel.spi.CompilePostProcessor`s on a compiled -class that has class-level annotations, with a new instance of the class, as the Java DSL loader does for `.java` -sources. In the Camel CLI this binds a `@BindToRegistry` class and registers a `@Converter` class (and handles the -Spring and Quarkus annotations) from a `.groovy` file, and binds it again on a reload in dev mode. A Groovy class -without annotations, and a Groovy script, is not instantiated, as before. +The Groovy script compiler now runs the `org.apache.camel.spi.CompilePostProcessor`s on a compiled class that has +class-level annotations, with a new instance of the class, as the Java DSL loader does for `.java` sources. The +processors in the registry are used when there are any (the Camel CLI registers processors that also handle the +Spring and Quarkus annotations); otherwise built-in processors for the Camel annotations are used. So in every runtime +(camel-main, Spring Boot, Quarkus) a `@BindToRegistry` or `@Configuration` class from a `.groovy` file is bound in +Camel's registry, a `@Converter` class is registered as type converters, and an `EventNotifier` class is added to +the management strategy; in the Camel CLI the bean is bound again on a reload in dev mode. A Groovy class without +annotations, and a Groovy script, is not instantiated, as before. + +The Camel-annotation compile post-processors that camel-kamelet-main used internally are now public classes in +camel-support: `org.apache.camel.support.compile.BindToRegistryCompilePostProcessor`, +`TypeConverterCompilePostProcessor` and `EventNotifierCompilePostProcessor`. === camel-djl (Breaking change) diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-beans.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-beans.adoc index 8bbfa4e7edf5..b68f14438609 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-beans.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-beans.adoc @@ -79,9 +79,10 @@ NOTE: A `.groovy` file is compiled after the beans of the YAML and XML files hav `- beans:` entry cannot refer to a Groovy class by its `type`; use `@BindToRegistry` on the class instead, or create the bean with an inline Groovy `script` in the `- beans:` entry. -NOTE: The annotations on a `.groovy` file are handled by the Camel CLI only. In a project created with -`camel export` the file is compiled, but the class is not bound as a bean. For a project that is exported, create -the bean with an inline Groovy `script` in a `- beans:` entry, which works in every runtime, or write it in Java. +NOTE: The Camel annotations (`@BindToRegistry`, `@Configuration`, `@Converter`) on a `.groovy` file keep working in +a project created with `camel export`, as the Groovy compiler in camel-groovy handles them itself in every runtime; +the Spring and Quarkus annotations are handled by the Camel CLI only. The bean lives in Camel's registry, not in the +Spring or CDI container. == Defining beans in XML DSL diff --git a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/injection/AnnotationDependencyInjection.java b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/injection/AnnotationDependencyInjection.java index 0fcedcdd1f54..cc7f280015da 100644 --- a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/injection/AnnotationDependencyInjection.java +++ b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/injection/AnnotationDependencyInjection.java @@ -18,28 +18,20 @@ package org.apache.camel.main.injection; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Map; import java.util.function.Supplier; -import org.apache.camel.BindToRegistry; -import org.apache.camel.CamelConfiguration; import org.apache.camel.CamelContext; -import org.apache.camel.Configuration; -import org.apache.camel.Converter; -import org.apache.camel.LoggingLevel; import org.apache.camel.NoSuchBeanException; import org.apache.camel.RuntimeCamelException; -import org.apache.camel.TypeConverterExists; import org.apache.camel.impl.engine.CamelPostProcessorHelper; import org.apache.camel.spi.CamelBeanPostProcessor; import org.apache.camel.spi.CamelBeanPostProcessorInjector; import org.apache.camel.spi.CompilePostProcessor; -import org.apache.camel.spi.EventNotifier; -import org.apache.camel.spi.ManagementStrategy; import org.apache.camel.spi.Registry; -import org.apache.camel.spi.TypeConverterRegistry; import org.apache.camel.support.PluginHelper; +import org.apache.camel.support.compile.BindToRegistryCompilePostProcessor; +import org.apache.camel.support.compile.EventNotifierCompilePostProcessor; +import org.apache.camel.support.compile.TypeConverterCompilePostProcessor; import org.apache.camel.util.AnnotationHelper; import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.ReflectionHelper; @@ -79,7 +71,7 @@ public final class AnnotationDependencyInjection { // camel / common registry.bind("CamelTypeConverterCompilePostProcessor", new TypeConverterCompilePostProcessor()); registry.bind("CamelEventNotifierCompilePostProcessor", new EventNotifierCompilePostProcessor()); - registry.bind("CamelBindToRegistryCompilePostProcessor", new BindToRegistryCompilePostProcessor()); + registry.bind("CamelBindToRegistryCompilePostProcessor", new BindToRegistryCompilePostProcessor(lazyBean)); // spring registry.bind("SpringAnnotationCompilePostProcessor", new SpringAnnotationCompilePostProcessor()); cbbp.addCamelBeanPostProjectInjector(new SpringBeanPostProcessorInjector(context)); @@ -88,113 +80,6 @@ public final class AnnotationDependencyInjection { cbbp.addCamelBeanPostProjectInjector(new QuarkusBeanPostProcessorInjector(context)); } - private static class TypeConverterCompilePostProcessor implements CompilePostProcessor { - - @Override - public void postCompile(CamelContext camelContext, String name, Class<?> clazz, byte[] byteCode, Object instance) - throws Exception { - if (clazz.isAnnotationPresent(Converter.class)) { - TypeConverterRegistry tcr = camelContext.getTypeConverterRegistry(); - TypeConverterExists exists = tcr.getTypeConverterExists(); - LoggingLevel level = tcr.getTypeConverterExistsLoggingLevel(); - // force type converter to override as we could be re-loading - tcr.setTypeConverterExists(TypeConverterExists.Override); - tcr.setTypeConverterExistsLoggingLevel(LoggingLevel.OFF); - try { - tcr.addTypeConverters(clazz); - } finally { - tcr.setTypeConverterExists(exists); - tcr.setTypeConverterExistsLoggingLevel(level); - } - } - } - } - - private static class EventNotifierCompilePostProcessor implements CompilePostProcessor { - - private final Map<String, EventNotifier> notifiers = new HashMap<>(); - - @Override - public void postCompile(CamelContext camelContext, String name, Class<?> clazz, byte[] byteCode, Object instance) - throws Exception { - if (instance == null) { - return; - } - - if (instance instanceof EventNotifier) { - ManagementStrategy ms = camelContext.getManagementStrategy(); - if (ms != null) { - notifiers.compute(name, (key, old) -> { - // remove previous instance - if (old != null) { - ms.removeEventNotifier(old); - } - // and new notifier - EventNotifier en = (EventNotifier) instance; - ms.addEventNotifier(en); - return en; - }); - } - } - } - } - - private class BindToRegistryCompilePostProcessor implements CompilePostProcessor { - - @Override - public void postCompile(CamelContext camelContext, String name, Class<?> clazz, byte[] byteCode, Object instance) - throws Exception { - - BindToRegistry bir = clazz.getAnnotation(BindToRegistry.class); - Configuration cfg = clazz.getAnnotation(Configuration.class); - - // special for lazy beans which we must create on-demand - if (instance == null && bir != null && (lazyBean || bir.lazy())) { - final String beanName = bir.value(); - instance = (Supplier<Object>) () -> { - Object answer = camelContext.getInjector().newInstance(clazz); - CamelBeanPostProcessor bpp = PluginHelper.getBeanPostProcessor(camelContext); - try { - bpp.postProcessBeforeInitialization(answer, beanName); - bpp.postProcessAfterInitialization(answer, beanName); - } catch (Exception e) { - throw RuntimeCamelException.wrapRuntimeException(e); - } - return answer; - }; - // unbind old bean and register lazy bean - camelContext.getRegistry().unbind(beanName); - // use dependency injection factory to perform the task of binding the bean to registry - Runnable task = PluginHelper.getDependencyInjectionAnnotationFactory(camelContext) - .createBindToRegistryFactory(name, instance, clazz, beanName, false, bir.initMethod(), - bir.destroyMethod()); - task.run(); - } else { - if (bir != null || cfg != null || instance instanceof CamelConfiguration) { - CamelBeanPostProcessor bpp = PluginHelper.getBeanPostProcessor(camelContext); - if (bir != null && ObjectHelper.isNotEmpty(bir.value())) { - name = bir.value(); - } else if (cfg != null && ObjectHelper.isNotEmpty(cfg.value())) { - name = cfg.value(); - } - // to support hot reloading of beans then we need to enable unbind mode in bean post processor - bpp.setUnbindEnabled(true); - try { - // this class uses camels own annotations so the bind to registry happens - // automatic by the bean post processor - bpp.postProcessBeforeInitialization(instance, name); - bpp.postProcessAfterInitialization(instance, name); - } finally { - bpp.setUnbindEnabled(false); - } - if (instance instanceof CamelConfiguration) { - ((CamelConfiguration) instance).configure(camelContext); - } - } - } - } - } - private class SpringAnnotationCompilePostProcessor implements CompilePostProcessor { @Override
