Revise MessageHistory configuration

Currently the `MessageHistoryRegistrar` can parse several sources for message history -
`@EnableMessageHistory` and/or `<message-history>`.
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 `<message-history>`.
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`
This commit is contained in:
Artem Bilan
2021-04-16 13:40:47 -04:00
committed by Gary Russell
parent 0fba06b206
commit b35dc9d754
8 changed files with 129 additions and 111 deletions

View File

@@ -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 "*";
}

View File

@@ -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<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableMessageHistory.class.getName());
if (registry.containsBeanDefinition(IntegrationContextUtils.INTEGRATION_MESSAGE_HISTORY_CONFIGURER_BEAN_NAME)) {
throw new BeanDefinitionStoreException(
"Only one @EnableMessageHistory or <message-history/> can be declared in the application context.");
}
Map<String, Object> 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<Object> componentNamePatternsSet = new ManagedSet<Object>();
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<Object> currentComponentNamePatternsSet = (Set<Object>) propertyValue.getValue();
currentComponentNamePatternsSet.add(componentNamePatterns); // NOSONAR never null
}
else {
Set<Object> componentNamePatternsSet = new ManagedSet<Object>();
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;
}
}

View File

@@ -47,32 +47,31 @@ public class PublisherRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Map<String, Object> 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 <annotation-config>) can be declared in the application context.");
}
Map<String, Object> 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(

View File

@@ -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<TrackableComponent> 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 <message-history>)" +
" 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<String> 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 <message-history>)" +
" 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;
}
}

View File

@@ -12,8 +12,6 @@
<!--INT-3853 -->
<context:property-placeholder location="classpath:org/springframework/integration/configuration/EnableIntegrationTests.properties"/>
<message-history tracked-components="publishedChannel,input,annotationTestService*"/>
<channel-interceptor pattern="none">
<wire-tap channel="bar" />
</channel-interceptor>

View File

@@ -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

View File

@@ -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<String, ConsumerEndpointFactoryBean> 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));

View File

@@ -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"]
----
<int:message-history/>
----
@@ -28,26 +34,42 @@ Its value a `List<Properties>`.
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"]
----
<int:gateway id="sampleGateway"
service-interface="org.springframework.integration.history.sample.SampleGateway"
default-request-channel="bridgeInChannel"/>
<int:chain id="sampleChain" input-channel="chainChannel" output-channel="filterChannel">
<int:header-enricher>
<int:header-enricher id="sampleEnricher" input-channel="enricherChannel" output-channel="filterChannel">
<int:header name="baz" value="baz"/>
</int:header-enricher>
</int:chain>
</int:header-enricher>
----
====
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"]
----
<int:message-history tracked-components="*Gateway, sample*, aName"/>
----
@@ -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 `<domain>:name=messageHistoryConfigurer,type=MessageHistoryConfigurer`.
IMPORTANT: If multiple beans (declared by `@EnableMessageHistory` and `<message-history/>`) exist, they must all have identical component name patterns (when trimmed and sorted).
Do not use a generic `<bean/>` definition for the `MessageHistoryConfigurer`.
IMPORTANT: Only one `@EnableMessageHistory` (or `<message-history/>`) 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.