INT-3286: Add @EnableMessageHistory

JIRA: https://jira.springsource.org/browse/INT-3286

Introduce `@EnableMessageHistory` and `MessageHistoryRegistrar`.
Refactoring for `MessageHistoryParser` to use `MessageHistoryRegistrar` to follow with DRY
Add test for `@EnableMessageHistory`

INT-3286: Polishing

INT-3286: Enable several MHs with the same value

Previously the Framework allowed only one `<message-history>`
independent of their `tracked-components`.
With introduction of `@EnableMessageHistory` and `MessageHistoryRegistrar`
the `MessageHistoryConfigurer` is improved to allow
several `<message-history>` or `@EnableMessageHistory`
with the same set of `componentNamePatterns`.

INT-3286 Polishing + JMX + Docs

* Allows setComponentNamePatterns and/or setComponentNamePatternsSet to
  be used as long as the settings are consistent.
* Handle the case where the `MHC` was configured as a bean (no managed set exists)
* Add support for changing the component name patterns over JMX + test case
* Docs

INT-3286 More Polishing

* Change bean name to `messageHistoryConfigurer`
* Move constant to `ICU`
* Export the MBean by the `IMBE`
This commit is contained in:
Artem Bilan
2014-02-05 17:29:58 +02:00
committed by Gary Russell
parent d8e4818c5e
commit 2fd27e3a6c
16 changed files with 415 additions and 60 deletions

View File

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

View File

@@ -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<String,Object> 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<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, messageHistoryConfigurer);
}
else {
BeanDefinition beanDefinition = registry.getBeanDefinition(IntegrationContextUtils.INTEGRATION_MESSAGE_HISTORY_CONFIGURER);
PropertyValue propertyValue = beanDefinition
.getPropertyValues().getPropertyValue("componentNamePatternsSet");
if (propertyValue != null) {
@SuppressWarnings("unchecked")
Set<Object> currentComponentNamePatternsSet = (Set<Object>) propertyValue.getValue();
currentComponentNamePatternsSet.add(componentNamePatterns);
}
else {
Set<Object> componentNamePatternsSet = new ManagedSet<Object>();
componentNamePatternsSet.add(componentNamePatterns);
beanDefinition.getPropertyValues().addPropertyValue("componentNamePatternsSet", componentNamePatternsSet);
}
}
}
}

View File

@@ -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 <message-history/>} 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<String, Object> getAnnotationAttributes(String annotationType) {
return Collections.<String, Object>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);
}
}

View File

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

View File

@@ -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<String> currentlyTrackedComponentNames = new HashSet<String>();
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 <message-history>)" +
" 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<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[] 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 <message-history>)" +
" 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();

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/integration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration
http://www.springframework.org/schema/integration/spring-integration.xsd">
<message-history tracked-components="publishedChannel,input"/>
<beans:bean id="messageHistoryConfigurer"
class="org.springframework.integration.history.MessageHistoryConfigurer">
<beans:property name="componentNamePatterns">
<beans:array>
<beans:value> input </beans:value>
<beans:value> publishedChannel </beans:value>
</beans:array>
</beans:property>
</beans:bean>
</beans:beans>

View File

@@ -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();

View File

@@ -0,0 +1 @@
message.history.tracked.components=input, publishedChannel

View File

@@ -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);
}

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int="http://www.springframework.org/schema/integration"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
<bean class="org.springframework.integration.context.MessageHistoryBeanPostProcessor"/>
<bean class="org.springframework.integration.context.MessageHistoryBeanPostProcessor"/>
</beans>

View File

@@ -6,6 +6,6 @@
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
<int:message-history/>
<int:message-history/>
<int:message-history tracked-components="foo*"/>
</beans>

View File

@@ -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();

View File

@@ -13,10 +13,12 @@
http://www.springframework.org/schema/integration/jmx
http://www.springframework.org/schema/integration/jmx/spring-integration-jmx.xsd">
<context:mbean-export/>
<context:mbean-export default-domain="#{T(java.util.UUID).randomUUID().toString()}"/>
<context:mbean-server/>
<jmx:mbean-export default-domain="#{T(java.util.UUID).randomUUID().toString()}"/>
<si:message-history/>
<si:channel id="channel"/>
<jmx:notification-publishing-channel-adapter

View File

@@ -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.
@@ -16,14 +16,22 @@
package org.springframework.integration.jmx.config;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.Set;
import javax.management.Attribute;
import javax.management.MBeanException;
import javax.management.MBeanServer;
import javax.management.Notification;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import org.junit.After;
@@ -32,12 +40,13 @@ import org.junit.runner.RunWith;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice;
import org.springframework.integration.jmx.JmxHeaders;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -132,6 +141,34 @@ public class NotificationPublishingChannelAdapterParserTests {
assertEquals(1, names.size());
}
@Test @DirtiesContext
public void changeMessageHistoryPatterns() throws Exception {
Set<ObjectInstance> 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 {
}

View File

@@ -59,6 +59,19 @@ assertEquals("sampleChain", chainHistory.get("name"));]]></programlisting>
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.
</para>
<para>
Starting with <emphasis>version 4.0</emphasis>, you can also use the <code>@EnableMessageHistory</code> annotation
in a <code>@Configuration</code> class. In addition, the <classname>MessageHistoryConfigurer</classname> bean
is now exposed as a JMX MBean by the <classname>IntegrationMBeanExporter</classname>
(see <xref linkend="jmx-mbean-exporter"/>), 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 <code>"&lt;domain&gt;:name=messageHistoryConfigurer,type=MessageHistoryConfigurer"</code>.
</para>
<important>
If multiple beans (declared by <code>@EnableMessageHistory</code> and/or <code>&lt;message-history/&gt;</code>) they
all must have identical component name patterns (when trimmed and sorted).
</important>
<note>
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

View File

@@ -35,7 +35,7 @@
>Migration Guide</ulink>.
</para>
</section>
<section>
<section id="4.0-enable-configuration">
<title>@EnableConfiguration</title>
<para>
The <code>@EnableIntegration</code> annotation has been added, to permit declaration of
@@ -43,6 +43,14 @@
<xref linkend="enable-integration"/> for more information.
</para>
</section>
<section id="4.0-message-history">
<title>@EnableMessageHistory</title>
<para>
Message history can now be enabled with the <code>@EnableMessageHistory</code> annotation in a
<code>@Configuration</code> class; in addition the message history settings can be modified
by a JMX MBean. For more information, see <xref linkend="message-history"/>.
</para>
</section>
<section id="4.0-xpath-header-enricher-header-type">
<title>Header Type for XPath Header Enricher</title>
<para>