From b35dc9d7546c35dd66f4ee64a4d23a3ebdb74591 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 16 Apr 2021 13:40:47 -0400 Subject: [PATCH] Revise MessageHistory configuration Currently the `MessageHistoryRegistrar` can parse several sources for message history - `@EnableMessageHistory` and/or ``. Since its logic relies on the reflection it is not compatible with Spring Native. Plus it causes confusion when several sources are declared so when time comes to change something in that configuration, we may miss some place to re-align with our new requirements. Better to reject extra configurations and enforce end-users to use only one `@EnableMessageHistory` or ``. This is actually a preferences for many other `@Enable...` in Spring portfolio. Plus we got a benefit with a Spring Native compatibility * Fix `MessageHistoryRegistrar` to parse only one `@EnableMessageHistory`. Register `MessageHistoryConfigurer` function way for Spring Native compatibility * Clean up `PublisherRegistrar` for better readability * Fix message history tests which exposed several configurations * Add JavaDoc into `EnableMessageHistory` * Refactor `MessageHistoryConfigurer` to let to override patterns configuration at runtime * Clean up `message-history.adoc` --- .../config/EnableMessageHistory.java | 11 ++- .../config/MessageHistoryRegistrar.java | 87 +++++++++++-------- .../config/PublisherRegistrar.java | 45 +++++----- .../history/MessageHistoryConfigurer.java | 30 +------ .../EnableIntegrationTests-context.xml | 2 - .../configuration/EnableIntegrationTests.java | 1 - .../MessageHistoryIntegrationTests.java | 11 ++- src/reference/asciidoc/message-history.adoc | 53 ++++++++--- 8 files changed, 129 insertions(+), 111 deletions(-) diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/EnableMessageHistory.java b/spring-integration-core/src/main/java/org/springframework/integration/config/EnableMessageHistory.java index 12cae7f7fe..54843ea131 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/EnableMessageHistory.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/EnableMessageHistory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2021 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. @@ -25,9 +25,11 @@ import java.lang.annotation.Target; import org.springframework.context.annotation.Import; /** - * Enables {@link org.springframework.integration.history.MessageHistory} for Integration components. + * Enables {@link org.springframework.integration.history.MessageHistory} + * for Spring Integration components. * * @author Artem Bilan + * * @since 4.0 */ @Target(ElementType.TYPE) @@ -36,6 +38,11 @@ import org.springframework.context.annotation.Import; @Import(MessageHistoryRegistrar.class) public @interface EnableMessageHistory { + /** + * The list of component name patterns to track (e.g. {@code "inputChannel", "out*", "*Channel", "*Service"}). + * By default all Spring Integration components are tracked. + * @return the list of component name patterns to track + */ String[] value() default "*"; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/MessageHistoryRegistrar.java b/spring-integration-core/src/main/java/org/springframework/integration/config/MessageHistoryRegistrar.java index c4a1b6a278..b5c700e83e 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/MessageHistoryRegistrar.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/MessageHistoryRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2021 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. @@ -16,19 +16,19 @@ package org.springframework.integration.config; +import java.util.Arrays; import java.util.Map; -import java.util.Set; -import org.springframework.beans.PropertyValue; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.support.AbstractBeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.ManagedSet; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.type.AnnotationMetadata; import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.integration.history.MessageHistoryConfigurer; +import org.springframework.util.StringUtils; /** * Registers the {@link MessageHistoryConfigurer} {@link org.springframework.beans.factory.config.BeanDefinition} @@ -38,49 +38,60 @@ import org.springframework.integration.history.MessageHistoryConfigurer; * * @author Artem Bilan * @author Gary Russell + * * @since 4.0 */ public class MessageHistoryRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { - Map annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableMessageHistory.class.getName()); + if (registry.containsBeanDefinition(IntegrationContextUtils.INTEGRATION_MESSAGE_HISTORY_CONFIGURER_BEAN_NAME)) { + throw new BeanDefinitionStoreException( + "Only one @EnableMessageHistory or can be declared in the application context."); + } + + Map annotationAttributes = + importingClassMetadata.getAnnotationAttributes(EnableMessageHistory.class.getName()); Object componentNamePatterns = annotationAttributes.get("value"); // NOSONAR never null + String patterns; + if (componentNamePatterns instanceof String[]) { - StringBuilder componentNamePatternsString = new StringBuilder(); - for (String s : (String[]) componentNamePatterns) { - componentNamePatternsString.append(s).append(","); - } - componentNamePatterns = componentNamePatternsString.substring(0, componentNamePatternsString.length() - 1); - } - - if (!registry.containsBeanDefinition(IntegrationContextUtils.INTEGRATION_MESSAGE_HISTORY_CONFIGURER_BEAN_NAME)) { - Set componentNamePatternsSet = new ManagedSet(); - componentNamePatternsSet.add(componentNamePatterns); - - AbstractBeanDefinition messageHistoryConfigurer = BeanDefinitionBuilder.genericBeanDefinition(MessageHistoryConfigurer.class) - .addPropertyValue("componentNamePatternsSet", componentNamePatternsSet) - .getBeanDefinition(); - - registry.registerBeanDefinition(IntegrationContextUtils.INTEGRATION_MESSAGE_HISTORY_CONFIGURER_BEAN_NAME, messageHistoryConfigurer); - + patterns = String.join(",", (String[]) componentNamePatterns); } else { - BeanDefinition beanDefinition = registry.getBeanDefinition(IntegrationContextUtils.INTEGRATION_MESSAGE_HISTORY_CONFIGURER_BEAN_NAME); - PropertyValue propertyValue = beanDefinition - .getPropertyValues().getPropertyValue("componentNamePatternsSet"); - if (propertyValue != null) { - @SuppressWarnings("unchecked") - Set currentComponentNamePatternsSet = (Set) propertyValue.getValue(); - currentComponentNamePatternsSet.add(componentNamePatterns); // NOSONAR never null - } - else { - Set componentNamePatternsSet = new ManagedSet(); - componentNamePatternsSet.add(componentNamePatterns); - beanDefinition.getPropertyValues().addPropertyValue("componentNamePatternsSet", componentNamePatternsSet); - } + patterns = (String) componentNamePatterns; } + + registry.registerBeanDefinition(IntegrationContextUtils.INTEGRATION_MESSAGE_HISTORY_CONFIGURER_BEAN_NAME, + new RootBeanDefinition(MessageHistoryConfigurer.class, + () -> createMessageHistoryConfigurer(registry, patterns))); + } + + private MessageHistoryConfigurer createMessageHistoryConfigurer(BeanDefinitionRegistry registry, String patterns) { + MessageHistoryConfigurer messageHistoryConfigurer = new MessageHistoryConfigurer(); + if (StringUtils.hasText(patterns)) { + ConfigurableBeanFactory beanFactory = null; + if (registry instanceof ConfigurableBeanFactory) { + beanFactory = (ConfigurableBeanFactory) registry; + } + else if (registry instanceof ConfigurableApplicationContext) { + beanFactory = ((ConfigurableApplicationContext) registry).getBeanFactory(); + } + + String[] patternsToSet = StringUtils.delimitedListToStringArray(patterns, ",", " "); + if (beanFactory != null) { + patternsToSet = + Arrays.stream(patternsToSet) + .map(beanFactory::resolveEmbeddedValue) + .flatMap((pattern) -> + Arrays.stream(StringUtils.delimitedListToStringArray(pattern, ",", " "))) + .toArray(String[]::new); + } + messageHistoryConfigurer.setComponentNamePatterns(patternsToSet); + } + + return messageHistoryConfigurer; } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/PublisherRegistrar.java b/spring-integration-core/src/main/java/org/springframework/integration/config/PublisherRegistrar.java index 8590741f8b..19ec5f7d30 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/PublisherRegistrar.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/PublisherRegistrar.java @@ -47,32 +47,31 @@ public class PublisherRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { - Map annotationAttributes = - importingClassMetadata.getAnnotationAttributes(EnablePublisher.class.getName()); - - if (!registry.containsBeanDefinition(IntegrationContextUtils.PUBLISHER_ANNOTATION_POSTPROCESSOR_NAME)) { - ConfigurableBeanFactory beanFactory; - if (registry instanceof ConfigurableBeanFactory) { - beanFactory = (ConfigurableBeanFactory) registry; - } - else if (registry instanceof ConfigurableApplicationContext) { - beanFactory = ((ConfigurableApplicationContext) registry).getBeanFactory(); - } - else { - beanFactory = null; - } - BeanDefinitionBuilder builder = - BeanDefinitionBuilder.genericBeanDefinition(PublisherAnnotationBeanPostProcessor.class, - () -> createPublisherAnnotationBeanPostProcessor(annotationAttributes, beanFactory)) - .setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - - registry.registerBeanDefinition(IntegrationContextUtils.PUBLISHER_ANNOTATION_POSTPROCESSOR_NAME, - builder.getBeanDefinition()); - } - else { + if (registry.containsBeanDefinition(IntegrationContextUtils.PUBLISHER_ANNOTATION_POSTPROCESSOR_NAME)) { throw new BeanDefinitionStoreException("Only one enable publisher definition " + "(@EnablePublisher or ) can be declared in the application context."); } + Map annotationAttributes = + importingClassMetadata.getAnnotationAttributes(EnablePublisher.class.getName()); + + ConfigurableBeanFactory beanFactory; + if (registry instanceof ConfigurableBeanFactory) { + beanFactory = (ConfigurableBeanFactory) registry; + } + else if (registry instanceof ConfigurableApplicationContext) { + beanFactory = ((ConfigurableApplicationContext) registry).getBeanFactory(); + } + else { + beanFactory = null; + } + + BeanDefinitionBuilder builder = + BeanDefinitionBuilder.genericBeanDefinition(PublisherAnnotationBeanPostProcessor.class, + () -> createPublisherAnnotationBeanPostProcessor(annotationAttributes, beanFactory)) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + + registry.registerBeanDefinition(IntegrationContextUtils.PUBLISHER_ANNOTATION_POSTPROCESSOR_NAME, + builder.getBeanDefinition()); } private PublisherAnnotationBeanPostProcessor createPublisherAnnotationBeanPostProcessor( diff --git a/spring-integration-core/src/main/java/org/springframework/integration/history/MessageHistoryConfigurer.java b/spring-integration-core/src/main/java/org/springframework/integration/history/MessageHistoryConfigurer.java index 013793ff75..96e49f90d7 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/history/MessageHistoryConfigurer.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/history/MessageHistoryConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -30,7 +30,6 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; -import org.springframework.beans.factory.support.BeanDefinitionValidationException; import org.springframework.integration.support.management.IntegrationManagedResource; import org.springframework.integration.support.management.ManageableSmartLifecycle; import org.springframework.integration.support.management.TrackableComponent; @@ -57,9 +56,7 @@ public class MessageHistoryConfigurer implements ManageableSmartLifecycle, BeanF private final Set currentlyTrackedComponents = ConcurrentHashMap.newKeySet(); - private String[] componentNamePatterns = new String[]{ "*" }; - - private boolean componentNamePatternsExplicitlySet; + private String[] componentNamePatterns = { "*" }; private ListableBeanFactory beanFactory; @@ -83,13 +80,7 @@ public class MessageHistoryConfigurer implements ManageableSmartLifecycle, BeanF trimmedAndSortedComponentNamePatterns[i] = trimmedAndSortedComponentNamePatterns[i].trim(); } Arrays.sort(trimmedAndSortedComponentNamePatterns); - Assert.isTrue(!this.componentNamePatternsExplicitlySet - || Arrays.equals(this.componentNamePatterns, trimmedAndSortedComponentNamePatterns), - "When more than one message history definition " + - "(@EnableMessageHistory or )" + - " is found in the context, they all must have the same 'componentNamePatterns'"); this.componentNamePatterns = trimmedAndSortedComponentNamePatterns; - this.componentNamePatternsExplicitlySet = true; } /** @@ -118,20 +109,8 @@ public class MessageHistoryConfigurer implements ManageableSmartLifecycle, BeanF public void setComponentNamePatternsSet(Set componentNamePatternsSet) { Assert.notNull(componentNamePatternsSet, "'componentNamePatternsSet' must not be null"); Assert.state(!this.running, "'componentNamePatternsSet' cannot be changed without invoking stop() first"); - for (String s : componentNamePatternsSet) { - String[] patterns = StringUtils.delimitedListToStringArray(s, ",", " "); - Arrays.sort(patterns); - if (this.componentNamePatternsExplicitlySet - && !Arrays.equals(this.componentNamePatterns, patterns)) { - throw new BeanDefinitionValidationException("When more than one message history definition " + - "(@EnableMessageHistory or )" + - " is found in the context, they all must have the same 'componentNamePatterns'"); - } - else { - this.componentNamePatterns = patterns; - this.componentNamePatternsExplicitlySet = true; - } - } + String patterns = String.join(",", componentNamePatternsSet); + this.componentNamePatterns = StringUtils.delimitedListToStringArray(patterns, ",", " "); } @Override @@ -225,7 +204,6 @@ public class MessageHistoryConfigurer implements ManageableSmartLifecycle, BeanF }); this.currentlyTrackedComponents.clear(); - this.componentNamePatternsExplicitlySet = false; // allow pattern changes this.running = false; } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests-context.xml index 86ca52ef77..b8c8b5999d 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests-context.xml @@ -12,8 +12,6 @@ - - diff --git a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java index e03076a1b8..5e984a4514 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java @@ -800,7 +800,6 @@ public class EnableIntegrationTests { @EnableIntegration // INT-3853 // @PropertySource("classpath:org/springframework/integration/configuration/EnableIntegrationTests.properties") - @EnableMessageHistory({ "input", "publishedChannel", "annotationTestService*" }) public static class ContextConfiguration { @Bean diff --git a/spring-integration-core/src/test/java/org/springframework/integration/history/MessageHistoryIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/history/MessageHistoryIntegrationTests.java index 94a63ab3bc..9f13d9d92c 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/history/MessageHistoryIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/history/MessageHistoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -30,7 +30,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.DirectFieldAccessor; -import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.integration.channel.DirectChannel; @@ -57,9 +57,8 @@ public class MessageHistoryIntegrationTests { Map cefBeans = ac.getBeansOfType(ConsumerEndpointFactoryBean.class); for (ConsumerEndpointFactoryBean cefBean : cefBeans.values()) { DirectFieldAccessor bridgeAccessor = new DirectFieldAccessor(cefBean); - String handlerClassName = bridgeAccessor.getPropertyValue("handler").getClass().getName(); - assertThat("org.springframework.integration.config.MessageHistoryWritingMessageHandler" - .equals(handlerClassName)).isFalse(); + Boolean shouldTrack = (Boolean) bridgeAccessor.getPropertyValue("handler.shouldTrack"); + assertThat(shouldTrack).isFalse(); } ac.close(); } @@ -226,7 +225,7 @@ public class MessageHistoryIntegrationTests { @Test public void testMessageHistoryMoreThanOneNamespaceFail() { - assertThatExceptionOfType(BeanCreationException.class) + assertThatExceptionOfType(BeanDefinitionStoreException.class) .isThrownBy(() -> new ClassPathXmlApplicationContext("messageHistoryWithHistoryWriterNamespace-fail.xml", MessageHistoryIntegrationTests.class)); diff --git a/src/reference/asciidoc/message-history.adoc b/src/reference/asciidoc/message-history.adoc index e3bccc2f20..7b7c9cd8cf 100644 --- a/src/reference/asciidoc/message-history.adoc +++ b/src/reference/asciidoc/message-history.adoc @@ -12,10 +12,16 @@ Spring integration provides a simple way to configure your message flows to main [[message-history-config]] ==== Message History Configuration -To enable message history, you need only define the `message-history` element in your configuration, as shown in the following example: +To enable message history, you need only define the `message-history` element (or `@EnableMessageHistory`) in your configuration, as shown in the following example: ==== -[source,xml] +[source, java, role="primary"] +---- +@Configuration +@EnableIntegration +@EnableMessageHistory +---- +[source, xml, role="secondary"] ---- ---- @@ -28,26 +34,42 @@ Its value a `List`. Consider the following configuration example: ==== -[source,xml] +[source, java, role="primary"] +---- +@MessagingGateway(defaultRequestChannel = "bridgeInChannel") +public interface SampleGateway { + ... +} + +@Bean +@Transformer(inputChannel = "enricherChannel", outputChannel="filterChannel") +HeaderEnricher sampleEnricher() { + HeaderEnricher enricher = + new HeaderEnricher(Collections.singletonMap("baz", new StaticHeaderValueMessageProcessor("baz"))); + return enricher; +} +---- +[source, xml, role="secondary"] ---- - - + - - + ---- ==== The preceding configuration produces a simple message history structure, with output similar to the following: -[source,java] + +==== +[source] ---- [{name=sampleGateway, type=gateway, timestamp=1283281668091}, - {name=sampleChain, type=chain, timestamp=1283281668094}] + {name=sampleEnricher, type=header-enricher, timestamp=1283281668094}] ---- +==== To get access to message history, you need only access the `MessageHistory` header. The following example shows how to do so: @@ -71,7 +93,13 @@ To limit the history to certain components based on their names, you can provide The following example shows how to do so: ==== -[source,xml] +[source, java, role="primary"] +---- +@Configuration +@EnableIntegration +@EnableMessageHistory("*Gateway", "sample*", "aName") +---- +[source, xml, role="secondary"] ---- ---- @@ -79,14 +107,13 @@ The following example shows how to do so: In the preceding example, message history is maintained only for the components that end with 'Gateway', start with 'sample', or match the name, 'aName', exactly. -Starting with version 4.0, you can also use the `@EnableMessageHistory` annotation in a `@Configuration` class. In addition, the `MessageHistoryConfigurer` bean is now exposed as a JMX MBean by the `IntegrationMBeanExporter` (see <<./jmx.adoc#jmx-mbean-exporter,MBean Exporter>>), letting you change the patterns at runtime. Note, however, that the bean must be stopped (turning off message history) in order to change the patterns. This feature might be useful to temporarily turn on history to analyze a system. The MBean's object name is `:name=messageHistoryConfigurer,type=MessageHistoryConfigurer`. -IMPORTANT: If multiple beans (declared by `@EnableMessageHistory` and ``) exist, they must all have identical component name patterns (when trimmed and sorted). -Do not use a generic `` definition for the `MessageHistoryConfigurer`. +IMPORTANT: Only one `@EnableMessageHistory` (or ``) must be declared in the application context as single source for components tracking configuration. +Do not use a generic bean definition for the `MessageHistoryConfigurer`. NOTE: By definition, the message history header is immutable (you cannot re-write history). Therefore, when writing message history values, the components either create new messages (when the component is an origin) or they copy the history from a request message, modifying it and setting the new list on a reply message.