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>