From 212f4781510b61a20766d3a825b7294b11d1ec5d Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 5 Mar 2018 14:34:04 -0500 Subject: [PATCH] INT-4417: Add @EndpointId JIRA: https://jira.spring.io/browse/INT-4417 Provide more flexibility with bean naming when using EIP Annotations with Java configuration. * Polishing - PR Comments * More polishing - remove `@Inherited`; disallow with more than one EIP annotation. --- .../integration/annotation/EndpointId.java | 53 ++++++ ...AbstractMethodAnnotationPostProcessor.java | 22 ++- .../MessagingAnnotationPostProcessor.java | 31 ++-- .../util/MessagingAnnotationUtils.java | 14 +- .../integration/endpoint/BeanNameTests.java | 155 +++++++++++++++++ .../micrometer/MicrometerMetricsTests.java | 35 +++- src/reference/asciidoc/configuration.adoc | 3 + src/reference/asciidoc/overview.adoc | 161 ++++++++++++++++++ src/reference/asciidoc/polling-consumer.adoc | 1 + src/reference/asciidoc/whats-new.adoc | 6 + 10 files changed, 455 insertions(+), 26 deletions(-) create mode 100644 spring-integration-core/src/main/java/org/springframework/integration/annotation/EndpointId.java create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/endpoint/BeanNameTests.java diff --git a/spring-integration-core/src/main/java/org/springframework/integration/annotation/EndpointId.java b/spring-integration-core/src/main/java/org/springframework/integration/annotation/EndpointId.java new file mode 100644 index 0000000000..c0d111236c --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/annotation/EndpointId.java @@ -0,0 +1,53 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed 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.springframework.integration.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * When used alongside an EIP annotation (and no {@code @Bean}), specifies the bean name of + * the consumer bean with the handler bean being {@code id.handler} (for a consuming + * endpoint) or {@code id.source} for a message source (e.g. inbound channel adapter). + *

+ * When there is also a {@code @Bean} annotation, this is the name of the consumer or + * source polling bean (the handler or source gets the normal {@code @Bean} name). When + * using on a {@code MessageHandler @Bean}, it is recommended to name the bean + * {@code foo.handler} when using {@code @EndpointId("foo"}. This will align with + * conventions in the framework. Similarly, for a message source, use + * {@code @Bean("bar.source"} and {@code @EndpointId("bar")}. + *

+ * This is not allowed if there are multiple EIP annotations on the same method. + * + * @author Gary Russell + * + * @since 5.0.4 + */ +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface EndpointId { + + /** + * @return the id + */ + String value(); + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/AbstractMethodAnnotationPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/AbstractMethodAnnotationPostProcessor.java index 6384ffb5c0..401a4cc310 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/AbstractMethodAnnotationPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/annotation/AbstractMethodAnnotationPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,6 +59,7 @@ import org.springframework.integration.endpoint.ReactiveStreamsConsumer; import org.springframework.integration.endpoint.SourcePollingChannelAdapter; import org.springframework.integration.handler.AbstractMessageProducingHandler; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; +import org.springframework.integration.handler.ReplyProducingMessageHandlerWrapper; import org.springframework.integration.handler.advice.HandleMessageAdvice; import org.springframework.integration.router.AbstractMessageRouter; import org.springframework.integration.scheduling.PollerMetadata; @@ -176,6 +177,10 @@ public abstract class AbstractMethodAnnotationPostProcessor, MethodAnnotationPostProcessor> postProcessors = new HashMap, MethodAnnotationPostProcessor>(); - private final MultiValueMap lazyLifecycleRoles = new LinkedMultiValueMap(); - private ConfigurableListableBeanFactory beanFactory; private final Set> noAnnotationsCache = @@ -151,7 +147,11 @@ public class MessagingAnnotationPostProcessor implements BeanPostProcessor, Bean } } } - + if (StringUtils.hasText(MessagingAnnotationUtils.endpointIdValue(method)) + && annotationChains.keySet().size() > 1) { + throw new IllegalStateException("@EndpointId on " + method.toGenericString() + + " can only have one EIP annotation, found: " + annotationChains.keySet().size()); + } for (Entry, List> entry : annotationChains.entrySet()) { Class annotationType = entry.getKey(); List annotations = entry.getValue(); @@ -225,7 +225,7 @@ public class MessagingAnnotationPostProcessor implements BeanPostProcessor, Bean List annotationChain = new LinkedList(); Set visited = new HashSet(); for (Annotation ann : annotations) { - this.recursiveFindAnnotation(annotationType, ann, annotationChain, visited); + recursiveFindAnnotation(annotationType, ann, annotationChain, visited); if (annotationChain.size() > 0) { Collections.reverse(annotationChain); return annotationChain; @@ -244,7 +244,7 @@ public class MessagingAnnotationPostProcessor implements BeanPostProcessor, Bean if (!ann.equals(metaAnn) && !visited.contains(metaAnn) && !(metaAnn.annotationType().getPackage().getName().startsWith("java.lang"))) { visited.add(metaAnn); // prevent infinite recursion if the same annotation is found again - if (this.recursiveFindAnnotation(annotationType, metaAnn, annotationChain, visited)) { + if (recursiveFindAnnotation(annotationType, metaAnn, annotationChain, visited)) { annotationChain.add(ann); return true; } @@ -255,12 +255,15 @@ public class MessagingAnnotationPostProcessor implements BeanPostProcessor, Bean protected String generateBeanName(String originalBeanName, Method method, Class annotationType) { - String baseName = originalBeanName + "." + method.getName() + "." - + ClassUtils.getShortNameAsProperty(annotationType); - String name = baseName; - int count = 1; - while (this.beanFactory.containsBean(name)) { - name = baseName + "#" + (++count); + String name = MessagingAnnotationUtils.endpointIdValue(method); + if (!StringUtils.hasText(name)) { + String baseName = originalBeanName + "." + method.getName() + "." + + ClassUtils.getShortNameAsProperty(annotationType); + name = baseName; + int count = 1; + while (this.beanFactory.containsBean(name)) { + name = baseName + "#" + (++count); + } } return name; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/util/MessagingAnnotationUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/util/MessagingAnnotationUtils.java index c25e110fdf..493e5582ca 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/util/MessagingAnnotationUtils.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/util/MessagingAnnotationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.springframework.aop.support.AopUtils; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.integration.annotation.EndpointId; import org.springframework.integration.annotation.Payloads; import org.springframework.messaging.MessagingException; import org.springframework.messaging.handler.annotation.Header; @@ -117,6 +118,17 @@ public final class MessagingAnnotationUtils { return match; } + /** + * Return the {@link EndpointId#value()} property, if present. + * @param method the methods. + * @return the id, or null. + * @since 5.0.4 + */ + public static String endpointIdValue(Method method) { + EndpointId endpointId = AnnotationUtils.findAnnotation(method, EndpointId.class); + return endpointId != null ? endpointId.value() : null; + } + private static Class getTargetClass(Object targetObject) { Class targetClass = targetObject.getClass(); if (AopUtils.isAopProxy(targetObject)) { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/endpoint/BeanNameTests.java b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/BeanNameTests.java new file mode 100644 index 0000000000..27b003f81d --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/BeanNameTests.java @@ -0,0 +1,155 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed 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.springframework.integration.endpoint; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.annotation.EndpointId; +import org.springframework.integration.annotation.InboundChannelAdapter; +import org.springframework.integration.annotation.Poller; +import org.springframework.integration.annotation.ServiceActivator; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.integration.config.EnableIntegrationManagement; +import org.springframework.integration.core.MessageSource; +import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandler; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +/** + * @author Gary Russell + * @since 5.0.4 + * + */ +@RunWith(SpringRunner.class) +@DirtiesContext +public class BeanNameTests { + + @SuppressWarnings("unused") + @Autowired + private EventDrivenConsumer eipMethod; + + @SuppressWarnings("unused") + @Autowired + @Qualifier("eipMethod.handler") + private MessageHandler eipMethodHandler; + + @SuppressWarnings("unused") + @Autowired + private EventDrivenConsumer eipBean; + + @SuppressWarnings("unused") + @Autowired + @Qualifier("eipBean.handler") + private MessageHandler eipBeanHandler; + + @SuppressWarnings("unused") + @Autowired + private EventDrivenConsumer eipBean2; + + @SuppressWarnings("unused") + @Autowired + @Qualifier("eipBean2.handler") + private MessageHandler eipBean2Handler; + + @SuppressWarnings("unused") + @Autowired + private SourcePollingChannelAdapter eipMethodSource; + + @SuppressWarnings("unused") + @Autowired + @Qualifier("eipMethodSource.source") + private MessageSource eipMethodSourceSource; + + @SuppressWarnings("unused") + @Autowired + private SourcePollingChannelAdapter eipSource; + + @SuppressWarnings("unused") + @Autowired + @Qualifier("eipSource.source") + private MessageSource eipSourceSource; + + @Test + public void contextLoads() { + + } + + @Configuration + @EnableIntegration + @EnableIntegrationManagement + public static class Config { + + @Bean + public MeterRegistry meterRegistry() { + return new SimpleMeterRegistry(); + } + + @ServiceActivator(inputChannel = "channel") + @EndpointId("eipMethod") + public void service(String in) { + if ("bar".equals(in)) { + throw new RuntimeException("testErrorCount"); + } + } + + @Bean("eipBean.handler") + @EndpointId("eipBean") + @ServiceActivator(inputChannel = "channel2") + public MessageHandler replyingHandler() { + return new AbstractReplyProducingMessageHandler() { + + @Override + protected Object handleRequestMessage(Message requestMessage) { + return null; + } + + }; + } + + @Bean("eipBean2.handler") + @EndpointId("eipBean2") + @ServiceActivator(inputChannel = "channel3") + public MessageHandler handler() { + return m -> { }; + } + + @EndpointId("eipMethodSource") + @InboundChannelAdapter(channel = "channel3", poller = @Poller(fixedDelay = "5000")) + public String pojoSource() { + return null; + } + + @Bean("eipSource.source") + @EndpointId("eipSource") + @InboundChannelAdapter(channel = "channel3", poller = @Poller(fixedDelay = "5000")) + public MessageSource source() { + return () -> null; + } + + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/support/management/micrometer/MicrometerMetricsTests.java b/spring-integration-core/src/test/java/org/springframework/integration/support/management/micrometer/MicrometerMetricsTests.java index dc317cdbde..89b51450bb 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/support/management/micrometer/MicrometerMetricsTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/support/management/micrometer/MicrometerMetricsTests.java @@ -28,6 +28,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.integration.annotation.EndpointId; import org.springframework.integration.annotation.ServiceActivator; import org.springframework.integration.channel.AbstractMessageChannel; import org.springframework.integration.channel.AbstractPollableChannel; @@ -38,7 +39,9 @@ import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.config.EnableIntegrationManagement; import org.springframework.integration.core.MessageSource; import org.springframework.integration.endpoint.AbstractMessageSource; +import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessagingException; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; @@ -67,6 +70,9 @@ public class MicrometerMetricsTests { @Autowired private AbstractMessageChannel channel; + @Autowired + private AbstractMessageChannel channel2; + @Autowired private MessageSource source; @@ -90,6 +96,7 @@ public class MicrometerMetricsTests { catch (MessagingException e) { assertThat(e.getCause().getMessage()).isEqualTo("testErrorCount"); } + this.channel2.send(message); this.queue.send(message); this.queue.send(message); this.queue.receive(); @@ -103,8 +110,8 @@ public class MicrometerMetricsTests { } nullChannel.send(message); MeterRegistry registry = this.meterRegistry; - assertThat(registry.get("spring.integration.channels").gauge().value()).isEqualTo(5); - assertThat(registry.get("spring.integration.handlers").gauge().value()).isEqualTo(2); + assertThat(registry.get("spring.integration.channels").gauge().value()).isEqualTo(6); + assertThat(registry.get("spring.integration.handlers").gauge().value()).isEqualTo(3); assertThat(registry.get("spring.integration.sources").gauge().value()).isEqualTo(1); assertThat(registry.get("spring.integration.receive") @@ -118,7 +125,12 @@ public class MicrometerMetricsTests { .counter().count()).isEqualTo(1); assertThat(registry.get("spring.integration.send") - .tag("name", "micrometerMetricsTests.Config.service.serviceActivator.handler") + .tag("name", "eipBean.handler") + .tag("result", "success") + .timer().count()).isEqualTo(1); + + assertThat(registry.get("spring.integration.send") + .tag("name", "eipMethod.handler") .tag("result", "success") .timer().count()).isEqualTo(1); @@ -133,7 +145,7 @@ public class MicrometerMetricsTests { .timer().count()).isEqualTo(1); assertThat(registry.get("spring.integration.send") - .tag("name", "micrometerMetricsTests.Config.service.serviceActivator.handler") + .tag("name", "eipMethod.handler") .tag("result", "failure") .timer().count()).isEqualTo(1); @@ -171,12 +183,27 @@ public class MicrometerMetricsTests { } @ServiceActivator(inputChannel = "channel") + @EndpointId("eipMethod") public void service(String in) { if ("bar".equals(in)) { throw new RuntimeException("testErrorCount"); } } + @Bean("eipBean.handler") + @EndpointId("eipBean") + @ServiceActivator(inputChannel = "channel2") + public MessageHandler replyingHandler() { + return new AbstractReplyProducingMessageHandler() { + + @Override + protected Object handleRequestMessage(Message requestMessage) { + return null; + } + + }; + } + @Bean public MessageSource source() { return new AbstractMessageSource() { diff --git a/src/reference/asciidoc/configuration.adoc b/src/reference/asciidoc/configuration.adoc index 587708a2dc..2288725a5c 100644 --- a/src/reference/asciidoc/configuration.adoc +++ b/src/reference/asciidoc/configuration.adoc @@ -598,6 +598,7 @@ public class MyFlowConfiguration { public Supplier> messageSupplier() { return () -> new GenericMessage<>("foo"); } +} ---- The meta-annotation rules work on `@Bean` methods as well (`@MyServiceActivator` above can be applied to a `@Bean` definition). @@ -613,11 +614,13 @@ This works like there is no Messaging Annotation on the `@Bean` method. `[configurationComponentName].[methodName].[decapitalizedAnnotationClassShortName]`. For example the endpoint (`SourcePollingChannelAdapter`) for the `consoleSource()` definition above gets a bean name like: `myFlowConfiguration.consoleSource.inboundChannelAdapter`. +Also see <>. IMPORTANT: When using these annotations on `@Bean` definitions, the `inputChannel` must reference a declared bean; channels are not automatically declared in this case. NOTE: With Java & Annotation configuration we can use any `@Conditional` (e.g. `@Profile`) definition on the `@Bean` method level, meaning to skip the bean registration by some condition reason: + [source,java] ---- @Bean diff --git a/src/reference/asciidoc/overview.adoc b/src/reference/asciidoc/overview.adoc index 3a090b26fc..7643771866 100644 --- a/src/reference/asciidoc/overview.adoc +++ b/src/reference/asciidoc/overview.adoc @@ -211,6 +211,167 @@ image::images/target-endpoint.jpg[align="center", scaledwidth=100%] NOTE: As discussed in <> above, channels can be _Pollable_ or _Subscribable_; in this diagram, this is depicted by the "clock" symbol and the solid arrow (poll) and the dotted arrow (subscribe). +[[endpoint-bean-names]] +==== Endpoint Bean Names + +Consuming endpoints (anything with an `inputChannel`) consist of two beans, the consumer and message handler. +The consumer has a reference to the message handler and invokes it as messages arrive. + +When configuring with XML: + +[source, xml] +---- + +---- + +the bean names will be as follows: + +- Consumer: `someService` (the `id`) +- Handler: `someService.handler` + +When using EIP annotations, the names depend on several factors. + +**When Annotating POJO Methods** + +[source, java] +---- +@Component +public class SomeComponent { + + @ServiceActivator(inputChannel = ...) + public String someMethod(...) { + ... + } + +} +---- + +the bean names will be as follows: + +- Consumer: `someComponent.someMethod.serviceActivator` +- Handler: `someComponent.someMethod.serviceActivator.handler` + +Starting with _version 5.0.4_, these names can be modified using the `@EndpointId` annotation: + +[source, java] +---- +@Component +public class SomeComponent { + + @EndpointId("someService") + @ServiceActivator(inputChannel = ...) + public String someMethod(...) { + ... + } + +} +---- + +the bean names will be as follows: + +- Consumer: `someService` +- Handler: `someService.handler` + +i.e. `@EndpointId` creates names as created by the `id` attribute with XML configuration. + + +**When Annotating @Beans** + +[source, java] +---- +@Configuratiom +public class SomeConfiguration { + + @Bean + @ServiceActivator(inputChannel = ...) + public MessageHandler someHandler() { + ... + } + +} +---- + +the bean names will be as follows: + +- Consumer: `someConfiguration.someHandler.serviceActivator` +- Handler: `someHandler` (the `@Bean` name) + +Starting with _version 5.0.4_, these names can be modified using the `@EndpointId` annotation: + +[source, java] +---- +@Configuratiom +public class SomeConfiguration { + + @Bean("someService.handler") + @EndpointId("someService") + @ServiceActivator(inputChannel = ...) + public MessageHandler someHandler() { + ... + } + +} +---- + +- Consumer: `someService` +- Handler: `someService.handler` + +i.e. `@EndpointId` creates names as created by the `id` attribute with XML configuration, as long as you use the convention of appending `.handler` to the `@Bean` name. + +There is one special case where a third bean is created; for architectural reasons, if a `MessageHandler` `@Bean` does not define an `AbstractReplyProducingMessageHandler`, the framework wraps the provided bean in a `ReplyProducingMessageHandlerWrapper`. +This wrapper supports request handler advice handling as well as emitting the normal 'produced no reply' debug log messages. +Its bean name is the handler bean name plus `.wrapper` (when there is an `@EndpointId`, otherwise it's the normal generated handler name). + +**Message Sources** + +Similarly <> create two beans, a `SourcePollingChannelAdapter` (SPCA) and a `MessageSource`. + +When configuring with XML: + +[source, xml] +---- + +---- + +the bean names will be as follows: + +- SPCA: `someAdapter` (the `id`) +- Handler: `someAdapter.source` + +Using `@EndpointId` with Java configuration: + +[source, java] +---- +@EndpointId("someAdapter") +@InboundChannelAdapter(channel = "channel3", poller = @Poller(fixedDelay = "5000")) +public String pojoSource() { + ... +} +---- + +the bean names will be as follows: + +- SPCA: `someAdapter` +- Handler: `someAdapter.source` + +[source, java] +---- +@Bean("someAdapter.source") +@EndpointId("someAdapter") +@InboundChannelAdapter(channel = "channel3", poller = @Poller(fixedDelay = "5000")) +public MessageSource source() { + return () -> { + ... + }; +} +---- + +the bean names will be as follows: + +- SPCA: `someAdapter` +- Handler: `someAdapter.source` (as long as you use the convention of appending `.source` to the `@Bean` name) + + [[configuration-enable-integration]] === Configuration and @EnableIntegration diff --git a/src/reference/asciidoc/polling-consumer.adoc b/src/reference/asciidoc/polling-consumer.adoc index 475c8ac46b..3b28964284 100644 --- a/src/reference/asciidoc/polling-consumer.adoc +++ b/src/reference/asciidoc/polling-consumer.adoc @@ -22,6 +22,7 @@ You can find a description of the pattern on the book's website at: http://www.enterpriseintegrationpatterns.com/PollingConsumer.html[http://www.enterpriseintegrationpatterns.com/PollingConsumer.html] +[[pollable-message-source]] ==== Pollable Message Source Furthermore, in Spring Integration a second variation of the Polling Consumer pattern exists. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 4d7fd77886..cece8b94fc 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -308,3 +308,9 @@ http://micrometer.io/[Micrometer] application monitoring is now supported (since See <> for more information. IMPORTANT: Changes were made to the Micrometer `Meters` in _version 5.0.3_ to make them more suitable for use in dimensional systems. + + +==== @EndpointId Annotations + +Introduced in _version 5.0.4_, this annotation provides control over bean naming when using Java configuration. +See <> for more information.