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 new file mode 100644 index 0000000000..b1712cf9ca --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/EnableMessageHistory.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014 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.config; + +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; + +import org.springframework.context.annotation.Import; + +/** + * Enables {@link org.springframework.integration.history.MessageHistory} for Integration components. + * + * @author Artem Bilan + * @since 4.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(MessageHistoryRegistrar.class) +public @interface EnableMessageHistory { + + 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 new file mode 100644 index 0000000000..5c47be3ed4 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/MessageHistoryRegistrar.java @@ -0,0 +1,85 @@ +/* + * Copyright 2014 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.config; + +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.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.ManagedSet; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.integration.context.IntegrationContextUtils; +import org.springframework.integration.history.MessageHistoryConfigurer; + +/** + * Registers the {@link MessageHistoryConfigurer} {@link org.springframework.beans.factory.config.BeanDefinition} + * for {@link org.springframework.integration.history.MessageHistory}. + * This registrar is applied from {@code @EnableMessageHistory} on the {@code Configuration} class + * or from {@code MessageHistoryParser}. + * + * @author Artem Bilan + * @since 4.0 + */ +public class MessageHistoryRegistrar implements ImportBeanDefinitionRegistrar { + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + Map annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableMessageHistory.class.getName()); + Object componentNamePatterns = annotationAttributes.get("value"); + + 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)) { + Set componentNamePatternsSet = new ManagedSet(); + componentNamePatternsSet.add(componentNamePatterns); + + AbstractBeanDefinition messageHistoryConfigurer = BeanDefinitionBuilder.genericBeanDefinition(MessageHistoryConfigurer.class) + .addPropertyValue("componentNamePatternsSet", componentNamePatternsSet) + .getBeanDefinition(); + + registry.registerBeanDefinition(IntegrationContextUtils.INTEGRATION_MESSAGE_HISTORY_CONFIGURER, messageHistoryConfigurer); + + } + else { + BeanDefinition beanDefinition = registry.getBeanDefinition(IntegrationContextUtils.INTEGRATION_MESSAGE_HISTORY_CONFIGURER); + PropertyValue propertyValue = beanDefinition + .getPropertyValues().getPropertyValue("componentNamePatternsSet"); + if (propertyValue != null) { + @SuppressWarnings("unchecked") + Set currentComponentNamePatternsSet = (Set) propertyValue.getValue(); + currentComponentNamePatternsSet.add(componentNamePatterns); + } + else { + Set componentNamePatternsSet = new ManagedSet(); + componentNamePatternsSet.add(componentNamePatterns); + beanDefinition.getPropertyValues().addPropertyValue("componentNamePatternsSet", componentNamePatternsSet); + } + } + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/MessageHistoryParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/MessageHistoryParser.java index 247ebf52b3..8d4ac615fb 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/MessageHistoryParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/MessageHistoryParser.java @@ -16,47 +16,41 @@ package org.springframework.integration.config.xml; -import org.springframework.beans.factory.BeanDefinitionStoreException; -import org.springframework.beans.factory.support.AbstractBeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.xml.AbstractSimpleBeanDefinitionParser; -import org.springframework.beans.factory.xml.ParserContext; +import java.util.Collections; +import java.util.Map; + import org.w3c.dom.Element; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.core.type.StandardAnnotationMetadata; +import org.springframework.integration.config.MessageHistoryRegistrar; + /** + * The {@code } parser. + * Delegates the {@link BeanDefinition} registration to the {@link MessageHistoryRegistrar}. + * * @author Oleg Zhurakousky * @author Mark Fisher + * @author Artem Bilan + * * @since 2.0 */ -public class MessageHistoryParser extends AbstractSimpleBeanDefinitionParser { - - private static final String CONFIGURER_CLASSNAME = "org.springframework.integration.history.MessageHistoryConfigurer"; +public class MessageHistoryParser implements BeanDefinitionParser { + private final MessageHistoryRegistrar messageHistoryRegistrar = new MessageHistoryRegistrar(); @Override - protected String getBeanClassName(Element element) { - return CONFIGURER_CLASSNAME; - } + public BeanDefinition parse(final Element element, ParserContext parserContext) { + this.messageHistoryRegistrar.registerBeanDefinitions(new StandardAnnotationMetadata(MessageHistoryParser.class) { - @Override - protected boolean shouldGenerateId() { - return false; - } + @Override + public Map getAnnotationAttributes(String annotationType) { + return Collections.singletonMap("value", element.getAttribute("tracked-components")); + } - protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) { - if (parserContext.getRegistry().containsBeanDefinition(CONFIGURER_CLASSNAME)) { - throw new BeanDefinitionStoreException("At most one MessageHistoryConfigurer may be registered within a context."); - } - return CONFIGURER_CLASSNAME; + }, parserContext.getRegistry()); + return null; } - - protected void postProcess(BeanDefinitionBuilder beanDefinition, Element element) { - - } - - protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { - IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "tracked-components", "componentNamePatterns"); - postProcess(builder, element); - } - } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java index 1ddb005f56..030fa49fbd 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java @@ -66,6 +66,8 @@ public abstract class IntegrationContextUtils { public static final String INTEGRATION_CONFIGURATION_POST_PROCESSOR_BEAN_NAME = "IntegrationConfigurationBeanFactoryPostProcessor"; + public static final String INTEGRATION_MESSAGE_HISTORY_CONFIGURER = "messageHistoryConfigurer"; + /** * @param beanFactory BeanFactory for lookup, must not be null. * @return The {@link MetadataStore} bean whose name is "metadataStore". 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 d84a92aec7..8c9ddab00c 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-2010 the original author or authors. + * Copyright 2002-2014 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,6 +16,7 @@ package org.springframework.integration.history; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -28,20 +29,31 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionValidationException; import org.springframework.context.SmartLifecycle; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; import org.springframework.util.Assert; import org.springframework.util.PatternMatchUtils; +import org.springframework.util.StringUtils; /** * @author Mark Fisher + * @author Artem Bilan + * @author Gary Russell + * * @since 2.0 */ +@ManagedResource public class MessageHistoryConfigurer implements SmartLifecycle, BeanFactoryAware { private final Log logger = LogFactory.getLog(this.getClass()); private volatile String[] componentNamePatterns = new String[] { "*" }; + private volatile boolean componentNamePatternsExplicitlySet; + private final Set currentlyTrackedComponentNames = new HashSet(); private volatile BeanFactory beanFactory; @@ -50,16 +62,76 @@ public class MessageHistoryConfigurer implements SmartLifecycle, BeanFactoryAwar private volatile boolean autoStartup = true; - private int phase = Integer.MIN_VALUE; + private final int phase = Integer.MIN_VALUE; private final Object lifecycleMonitor = new Object(); + /** + * The patterns for which components will be tracked; default '*' (all trackable + * components). Cannot be changed if {@link #isRunning()}; invoke {@link #stop()} first. + * @param componentNamePatterns The patterns. + */ public void setComponentNamePatterns(String[] componentNamePatterns) { Assert.notEmpty(componentNamePatterns, "componentNamePatterns must not be empty"); - this.componentNamePatterns = componentNamePatterns; + Assert.state(!this.running, "'componentNamePatterns' cannot be changed without invoking stop() first"); + String[] trimmedAndSortedComponentNamePatterns = componentNamePatterns.clone(); + for (int i = 0; i < componentNamePatterns.length; i++) { + 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; } + /** + * A comma-delimited list of patterns for which components will be tracked; default '*' (all trackable + * components). Cannot be changed if {@link #isRunning()}; invoke {@link #stop()} first. + * @param componentNamePatterns The patterns. + */ + @ManagedAttribute(description="comma-delimited list of patterns; must invoke stop() before changing.") + public void setComponentNamePatternsString(String componentNamePatterns) { + this.setComponentNamePatterns(StringUtils.delimitedListToStringArray(componentNamePatterns, ",", " ")); + } + + @ManagedAttribute + public String getComponentNamePatternsString() { + return StringUtils.arrayToCommaDelimitedString(this.componentNamePatterns); + } + + + /** + * The patterns for which components will be tracked; default '*' (all trackable + * components). Cannot be changed if {@link #isRunning()}; invoke {@link #stop()} first. + * All members of the set must canonically represent the same patterns - allows multiple + * EnableMessageHistory annotations as long they all have the same patterns. + * @param componentNamePatternsSet A set of lists of comma-delimited patterns. + */ + 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[] componentNamePatterns = StringUtils.delimitedListToStringArray(s, "," , " "); + Arrays.sort(componentNamePatterns); + if (this.componentNamePatternsExplicitlySet + && !Arrays.equals(this.componentNamePatterns, componentNamePatterns)) { + 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 = componentNamePatterns; + this.componentNamePatternsExplicitlySet = true; + } + } + } + + @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } @@ -73,18 +145,23 @@ public class MessageHistoryConfigurer implements SmartLifecycle, BeanFactoryAwar * SmartLifecycle implementation */ + @Override public boolean isRunning() { return this.running; } + @Override public boolean isAutoStartup() { return this.autoStartup; } + @Override public int getPhase() { return this.phase; } + @ManagedOperation + @Override public void start() { synchronized (this.lifecycleMonitor) { if (!this.running && this.beanFactory instanceof ListableBeanFactory) { @@ -104,6 +181,8 @@ public class MessageHistoryConfigurer implements SmartLifecycle, BeanFactoryAwar } } + @ManagedOperation + @Override public void stop() { synchronized (this.lifecycleMonitor) { if (this.running && this.beanFactory instanceof ListableBeanFactory) { @@ -118,10 +197,12 @@ public class MessageHistoryConfigurer implements SmartLifecycle, BeanFactoryAwar } this.currentlyTrackedComponentNames.clear(); this.running = false; + this.componentNamePatternsExplicitlySet = false; // allow pattern changes } } } + @Override public void stop(Runnable callback) { this.stop(); callback.run(); 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 new file mode 100644 index 0000000000..141d30333a --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests-context.xml @@ -0,0 +1,21 @@ + + + + + + + + + input + publishedChannel + + + + + 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 c177e2da28..f63824d275 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 @@ -16,9 +16,13 @@ package org.springframework.integration.configuration; +import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import org.hamcrest.Matchers; import org.junit.Test; import org.junit.runner.RunWith; @@ -26,6 +30,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; +import org.springframework.context.annotation.PropertySource; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.integration.annotation.MessageEndpoint; import org.springframework.integration.annotation.Payload; import org.springframework.integration.annotation.Publisher; @@ -33,10 +40,15 @@ import org.springframework.integration.annotation.ServiceActivator; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.config.EnableIntegration; +import org.springframework.integration.config.EnableMessageHistory; +import org.springframework.integration.history.MessageHistory; +import org.springframework.integration.history.MessageHistoryConfigurer; import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.test.util.TestUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.AnnotationConfigContextLoader; @@ -45,7 +57,7 @@ import org.springframework.test.context.support.AnnotationConfigContextLoader; * @author Artem Bilan * @since 4.0 */ -@ContextConfiguration(loader = AnnotationConfigContextLoader.class) +@ContextConfiguration(loader = AnnotationConfigContextLoader.class, classes = {EnableIntegrationTests.ContextConfiguration.class, EnableIntegrationTests.ContextConfiguration2.class}) @RunWith(SpringJUnit4ClassRunner.class) public class EnableIntegrationTests { @@ -58,6 +70,9 @@ public class EnableIntegrationTests { @Autowired private PollableChannel publishedChannel; + @Autowired + private MessageHistoryConfigurer configurer; + @Test public void testAnnotatedServiceActivator() { this.input.send(MessageBuilder.withPayload("Foo").build()); @@ -65,15 +80,43 @@ public class EnableIntegrationTests { assertNotNull(receive); assertEquals("FOO", receive.getPayload()); + MessageHistory messageHistory = receive.getHeaders().get(MessageHistory.HEADER_NAME, MessageHistory.class); + assertNotNull(messageHistory); + String messageHistoryString = messageHistory.toString(); + assertThat(messageHistoryString, Matchers.containsString("input")); + assertThat(messageHistoryString, Matchers.not(Matchers.containsString("output"))); + receive = this.publishedChannel.receive(1000); assertNotNull(receive); assertEquals("foo", receive.getPayload()); + + messageHistory = receive.getHeaders().get(MessageHistory.HEADER_NAME, MessageHistory.class); + assertNotNull(messageHistory); + messageHistoryString = messageHistory.toString(); + assertThat(messageHistoryString, Matchers.not(Matchers.containsString("input"))); + assertThat(messageHistoryString, Matchers.not(Matchers.containsString("output"))); + assertThat(messageHistoryString, Matchers.containsString("publishedChannel")); } + @Test @DirtiesContext + public void testChangePatterns() { + try { + this.configurer.setComponentNamePatterns(new String[] {"*"}); + fail("ExpectedException"); + } + catch (IllegalStateException e) { + assertThat(e.getMessage(), containsString("cannot be changed")); + } + this.configurer.stop(); + this.configurer.setComponentNamePatterns(new String[] {"*"}); + assertEquals("*", TestUtils.getPropertyValue(this.configurer, "componentNamePatterns", String[].class)[0]); + } @Configuration @ComponentScan(basePackageClasses = EnableIntegrationTests.class) @EnableIntegration + @PropertySource("classpath:org/springframework/integration/configuration/EnableIntegrationTests.properties") + @EnableMessageHistory({"input", "publishedChannel"}) public static class ContextConfiguration { @Bean @@ -86,6 +129,19 @@ public class EnableIntegrationTests { return new QueueChannel(); } + } + + @Configuration + @EnableIntegration + @ImportResource("classpath:org/springframework/integration/configuration/EnableIntegrationTests-context.xml") + @EnableMessageHistory("${message.history.tracked.components}") + public static class ContextConfiguration2 { + + @Bean + public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { + return new PropertySourcesPlaceholderConfigurer(); + } + @Bean public PollableChannel publishedChannel() { return new QueueChannel(); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.properties b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.properties new file mode 100644 index 0000000000..35117148f8 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.properties @@ -0,0 +1 @@ +message.history.tracked.components=input, publishedChannel 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 d1870789d5..038340fb47 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-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -29,11 +29,11 @@ import java.util.Properties; import org.junit.Ignore; import org.junit.Test; import org.mockito.Mockito; + import org.springframework.beans.DirectFieldAccessor; -import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.BeanCreationException; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.messaging.MessageHandlingException; import org.springframework.integration.MessageRejectedException; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.config.ConsumerEndpointFactoryBean; @@ -41,6 +41,7 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageDeliveryException; import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.MessageHandlingException; import org.springframework.util.StopWatch; /** @@ -200,7 +201,7 @@ public class MessageHistoryIntegrationTests { Mockito.verify(handler, Mockito.times(1)).handleMessage(Mockito.any(Message.class)); } - @Test(expected=BeanDefinitionParsingException.class) + @Test(expected=BeanCreationException.class) public void testMessageHistoryMoreThanOneNamespaceFail() { new ClassPathXmlApplicationContext("messageHistoryWithHistoryWriterNamespace-fail.xml", MessageHistoryIntegrationTests.class); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/history/messageHistoryWithHistoryWriter-fail.xml b/spring-integration-core/src/test/java/org/springframework/integration/history/messageHistoryWithHistoryWriter-fail.xml deleted file mode 100644 index 07faabe4b7..0000000000 --- a/spring-integration-core/src/test/java/org/springframework/integration/history/messageHistoryWithHistoryWriter-fail.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/spring-integration-core/src/test/java/org/springframework/integration/history/messageHistoryWithHistoryWriterNamespace-fail.xml b/spring-integration-core/src/test/java/org/springframework/integration/history/messageHistoryWithHistoryWriterNamespace-fail.xml index 69b7be70e6..e39adef894 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/history/messageHistoryWithHistoryWriterNamespace-fail.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/history/messageHistoryWithHistoryWriterNamespace-fail.xml @@ -6,6 +6,6 @@ http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd"> - + diff --git a/spring-integration-jmx/src/main/java/org/springframework/integration/monitor/IntegrationMBeanExporter.java b/spring-integration-jmx/src/main/java/org/springframework/integration/monitor/IntegrationMBeanExporter.java index 91efa883df..94cbb49994 100644 --- a/spring-integration-jmx/src/main/java/org/springframework/integration/monitor/IntegrationMBeanExporter.java +++ b/spring-integration-jmx/src/main/java/org/springframework/integration/monitor/IntegrationMBeanExporter.java @@ -57,14 +57,12 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.context.Lifecycle; import org.springframework.context.SmartLifecycle; import org.springframework.core.task.support.ExecutorServiceAdapter; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessagingException; import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.integration.context.OrderlyShutdownCapable; -import org.springframework.messaging.MessageHandler; import org.springframework.integration.core.MessageSource; -import org.springframework.messaging.PollableChannel; import org.springframework.integration.endpoint.AbstractEndpoint; +import org.springframework.integration.history.MessageHistoryConfigurer; import org.springframework.integration.support.context.NamedComponent; import org.springframework.jmx.export.MBeanExporter; import org.springframework.jmx.export.UnableToRegisterMBeanException; @@ -76,6 +74,10 @@ import org.springframework.jmx.export.annotation.ManagedResource; import org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler; import org.springframework.jmx.export.naming.MetadataNamingStrategy; import org.springframework.jmx.support.MetricType; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.MessagingException; +import org.springframework.messaging.PollableChannel; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.Assert; @@ -179,6 +181,8 @@ public class IntegrationMBeanExporter extends MBeanExporter implements BeanPostP private final AtomicBoolean shuttingDown = new AtomicBoolean(); + private MessageHistoryConfigurer messageHistoryConfigurer; + public IntegrationMBeanExporter() { super(); // Shouldn't be necessary, but to be on the safe side... @@ -225,6 +229,7 @@ public class IntegrationMBeanExporter extends MBeanExporter implements BeanPostP this.beanFactory = (ListableBeanFactory) beanFactory; } + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { Assert.notNull(applicationContext, "ApplicationContext may not be null"); @@ -236,6 +241,7 @@ public class IntegrationMBeanExporter extends MBeanExporter implements BeanPostP this.shutdownExecutor = shutdownExecutor; } + @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof Advised) { @@ -249,6 +255,12 @@ public class IntegrationMBeanExporter extends MBeanExporter implements BeanPostP } } + if (IntegrationContextUtils.INTEGRATION_MESSAGE_HISTORY_CONFIGURER.equals(beanName) + && bean instanceof MessageHistoryConfigurer) { + this.messageHistoryConfigurer = (MessageHistoryConfigurer) bean; + return bean; + } + if (bean instanceof MessageHandler) { if (this.handlerInAnonymousWrapper(bean) != null) { if (logger.isDebugEnabled()) { @@ -364,18 +376,22 @@ public class IntegrationMBeanExporter extends MBeanExporter implements BeanPostP } } + @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } + @Override public final boolean isAutoStartup() { return this.autoStartup; } + @Override public final int getPhase() { return this.phase; } + @Override public final boolean isRunning() { this.lifecycleLock.lock(); try { @@ -386,6 +402,7 @@ public class IntegrationMBeanExporter extends MBeanExporter implements BeanPostP } } + @Override public final void start() { this.lifecycleLock.lock(); try { @@ -402,6 +419,7 @@ public class IntegrationMBeanExporter extends MBeanExporter implements BeanPostP } } + @Override public final void stop() { this.lifecycleLock.lock(); try { @@ -418,6 +436,7 @@ public class IntegrationMBeanExporter extends MBeanExporter implements BeanPostP } } + @Override public final void stop(Runnable callback) { this.lifecycleLock.lock(); try { @@ -441,6 +460,10 @@ public class IntegrationMBeanExporter extends MBeanExporter implements BeanPostP registerHandlers(); registerSources(); registerEndpoints(); + if (this.messageHistoryConfigurer != null) { + this.registerBeanInstance(this.messageHistoryConfigurer, + IntegrationContextUtils.INTEGRATION_MESSAGE_HISTORY_CONFIGURER); + } } @Override @@ -499,6 +522,7 @@ public class IntegrationMBeanExporter extends MBeanExporter implements BeanPostP * Perform orderly shutdown - called or executed from * {@link #stopActiveComponents(boolean, long)}. */ + @Override public void run() { try { this.orderlyShutdownCapableComponentsBefore(); diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/NotificationPublishingChannelAdapterParserTests-context.xml b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/NotificationPublishingChannelAdapterParserTests-context.xml index 4c99f3a89a..224e54637b 100644 --- a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/NotificationPublishingChannelAdapterParserTests-context.xml +++ b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/NotificationPublishingChannelAdapterParserTests-context.xml @@ -13,10 +13,12 @@ http://www.springframework.org/schema/integration/jmx http://www.springframework.org/schema/integration/jmx/spring-integration-jmx.xsd"> - + + + mbeans = server.queryMBeans(null, null); + boolean tested = false; + for (ObjectInstance mbean : mbeans) { + if (mbean.toString().contains("MessageHistoryConfigurer")) { + ObjectName objectName = mbean.getObjectName(); + try{ + server.setAttribute(objectName, new Attribute("ComponentNamePatternsString", "foo, bar")); + fail("Exception expected"); + } + catch (MBeanException e) { + assertThat(e.getTargetException(), instanceOf(IllegalStateException.class)); + assertThat(e.getTargetException().getMessage(), containsString("cannot be changed")); + } + catch (Exception e) { + throw e; + } + server.invoke(objectName, "stop", new Object[]{}, new String[]{}); + server.setAttribute(objectName, new Attribute("ComponentNamePatternsString", "foo, bar")); + assertEquals("bar,foo", server.getAttribute(objectName, "ComponentNamePatternsString")); + tested = true; + break; + } + } + assertTrue(tested); + } + private static class TestData { } diff --git a/src/reference/docbook/message-history.xml b/src/reference/docbook/message-history.xml index 29fb6c60cf..99272d8062 100644 --- a/src/reference/docbook/message-history.xml +++ b/src/reference/docbook/message-history.xml @@ -59,6 +59,19 @@ assertEquals("sampleChain", chainHistory.get("name"));]]> In the above example, Message History will only be maintained for all of the components that end with 'Gateway', start with 'sample', or match the name 'foo' 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 ), allowing the patterns to be changed 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". + + + If multiple beans (declared by @EnableMessageHistory and/or <message-history/>) they + all must have identical component name patterns (when trimmed and sorted). + Remember that by definition the Message History header is immutable (you can't re-write history, although some try). Therefore, when writing Message History values, the components are either creating brand new Messages (when the component is an origin), or they are copying the diff --git a/src/reference/docbook/whats-new.xml b/src/reference/docbook/whats-new.xml index 6bad8d8e8e..5d170c2748 100644 --- a/src/reference/docbook/whats-new.xml +++ b/src/reference/docbook/whats-new.xml @@ -35,7 +35,7 @@ >Migration Guide. -
+
@EnableConfiguration The @EnableIntegration annotation has been added, to permit declaration of @@ -43,6 +43,14 @@ for more information.
+
+ @EnableMessageHistory + + Message history can now be enabled with the @EnableMessageHistory annotation in a + @Configuration class; in addition the message history settings can be modified + by a JMX MBean. For more information, see . + +
Header Type for XPath Header Enricher