INT-3064 Change MessageHeaders.Id Strategy

Use `com.eaio.uuid` by default and document how to replace
the default strategy.

Provide JDK and a simple incrementing incrementing implementation.

Add code to detect multiple contexts in the same classloaded using
the same strategy.

Add code to detect multiple IdGenerator beans and emit a WARN
instead of DEBUG, which is the case for no beans.
This commit is contained in:
Gary Russell
2013-06-12 16:58:58 +01:00
committed by Mark Fisher
parent ba557e617d
commit 77bdbed9a7
6 changed files with 341 additions and 16 deletions

View File

@@ -27,6 +27,7 @@ allprojects {
repositories {
maven { url 'http://repo.springsource.org/libs-milestone' }
maven { url 'http://repo.springsource.org/plugins-release' }
mavenCentral()
}
}
@@ -55,6 +56,7 @@ subprojects { subproject ->
junitVersion = '4.11'
log4jVersion = '1.2.12'
mockitoVersion = '1.9.5'
eaioUUIDVersion = '3.2'
springVersionDefault = '3.1.4.RELEASE'
springVersion = project.hasProperty('springVersion') ? getProperty('springVersion') : springVersionDefault
@@ -180,6 +182,7 @@ project('spring-integration-core') {
compile "org.springframework:spring-context:$springVersion"
compile "org.springframework:spring-tx:$springVersion"
compile "org.springframework.retry:spring-retry:$springRetryVersion"
compile "com.eaio.uuid:uuid:$eaioUUIDVersion"
compile("org.codehaus.jackson:jackson-mapper-asl:$jacksonVersion", optional)
compile("com.fasterxml.jackson.core:jackson-databind:$jackson2Version", optional)
testCompile "org.aspectj:aspectjweaver:$aspectjVersion"

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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.
@@ -28,10 +28,13 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.eaio.uuid.UUIDGen;
/**
* The headers for a {@link Message}.<br>
* IMPORTANT: MessageHeaders are immutable. Any mutating operation (e.g., put(..), putAll(..) etc.)
@@ -97,8 +100,9 @@ public final class MessageHeaders implements Map<String, Object>, Serializable {
public MessageHeaders(Map<String, Object> headers) {
this.headers = (headers != null) ? new HashMap<String, Object>(headers) : new HashMap<String, Object>();
if (MessageHeaders.idGenerator == null){
this.headers.put(ID, UUID.randomUUID());
if (MessageHeaders.idGenerator == null) {
UUID uuid = new UUID(UUIDGen.newTime(), UUIDGen.getClockSeqAndNode());
this.headers.put(ID, uuid);
}
else {
this.headers.put(ID, MessageHeaders.idGenerator.generateId());
@@ -271,4 +275,31 @@ public final class MessageHeaders implements Map<String, Object>, Serializable {
public static interface IdGenerator {
UUID generateId();
}
public static class JdkIdGenerator implements IdGenerator {
@Override
public UUID generateId() {
return UUID.randomUUID();
}
}
public static class SimpleIncrementingIdGenerator implements IdGenerator {
private final AtomicLong topBits = new AtomicLong();
private final AtomicLong bottomBits = new AtomicLong();
@Override
public UUID generateId() {
long bottomBits = this.bottomBits.incrementAndGet();
if (bottomBits == 0) {
this.topBits.incrementAndGet();
}
return new UUID(this.topBits.get(), bottomBits);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2013 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.
@@ -17,6 +17,8 @@
package org.springframework.integration.config;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -35,30 +37,35 @@ import org.springframework.util.ReflectionUtils;
/**
* @author Oleg Zhurakousky
* @author Mark Fisher
* @author Gary Russell
* @since 2.0.4
*/
public final class IdGeneratorConfigurer implements ApplicationListener<ApplicationContextEvent> {
private static volatile String generatorContextId;
private static final Set<String> generatorContextId = new HashSet<String>();
private static volatile IdGenerator theIdGenerator;
private final Log logger = LogFactory.getLog(getClass());
public void onApplicationEvent(ApplicationContextEvent event) {
public synchronized void onApplicationEvent(ApplicationContextEvent event) {
ApplicationContext context = event.getApplicationContext();
if (event instanceof ContextRefreshedEvent) {
boolean contextHasIdGenerator = context.getBeanNamesForType(IdGenerator.class).length > 0;
if (contextHasIdGenerator) {
if (this.setIdGenerator(context)) {
IdGeneratorConfigurer.generatorContextId = context.getId();
IdGeneratorConfigurer.generatorContextId.add(context.getId());
}
}
}
else if (event instanceof ContextClosedEvent) {
if (context.getId().equals(IdGeneratorConfigurer.generatorContextId)) {
this.unsetIdGenerator();
IdGeneratorConfigurer.generatorContextId = null;
if (IdGeneratorConfigurer.generatorContextId.contains(context.getId())) {
if (IdGeneratorConfigurer.generatorContextId.size() == 1) {
this.unsetIdGenerator();
}
IdGeneratorConfigurer.generatorContextId.remove(context.getId());
}
}
}
}
private boolean setIdGenerator(ApplicationContext context) {
@@ -76,19 +83,34 @@ public final class IdGeneratorConfigurer implements ApplicationListener<Applicat
return false;
}
else {
// different instance has been set, not legal
throw new BeanDefinitionStoreException("'MessageHeaders.idGenerator' has already been set and can not be set again");
if (IdGeneratorConfigurer.theIdGenerator.getClass() == idGeneratorBean.getClass()) {
if (logger.isWarnEnabled()) {
logger.warn("Another instance of " + idGeneratorBean.getClass() +
" has already been established; ignoring");
}
return true;
}
else {
// different instance has been set, not legal
throw new BeanDefinitionStoreException("'MessageHeaders.idGenerator' has already been set and can not be set again");
}
}
}
if (logger.isInfoEnabled()) {
logger.info("Message IDs will be generated using custom IdGenerator [" + idGeneratorBean.getClass() + "]");
}
ReflectionUtils.setField(idGeneratorField, null, idGeneratorBean);
IdGeneratorConfigurer.theIdGenerator = idGeneratorBean;
}
catch (NoSuchBeanDefinitionException e) {
// No custom IdGenerator. We will use the default.
if (logger.isDebugEnabled()) {
logger.debug("Unable to locate MessageHeaders.IdGenerator. Will use default: UUID.randomUUID()");
int idBeans = context.getBeansOfType(IdGenerator.class).size();
if (idBeans > 1 && logger.isWarnEnabled()) {
logger.warn("Found too many 'IdGenerator' beans (" + idBeans + ") " +
"Will use the existing UUID strategy.");
}
else if (logger.isDebugEnabled()) {
logger.debug("Unable to locate MessageHeaders.IdGenerator. Will use the existing UUID strategy.");
}
return false;
}
@@ -96,7 +118,7 @@ public final class IdGeneratorConfigurer implements ApplicationListener<Applicat
// thrown from ReflectionUtils
if (logger.isWarnEnabled()) {
logger.warn("Unexpected exception occurred while accessing idGenerator of MessageHeaders." +
" Will use default: UUID.randomUUID()", e);
" Will use the existing UUID strategy.", e);
}
return false;
}
@@ -108,6 +130,7 @@ public final class IdGeneratorConfigurer implements ApplicationListener<Applicat
Field idGeneratorField = ReflectionUtils.findField(MessageHeaders.class, "idGenerator");
ReflectionUtils.makeAccessible(idGeneratorField);
idGeneratorField.set(null, null);
IdGeneratorConfigurer.theIdGenerator = null;
}
catch (Exception e) {
if (logger.isWarnEnabled()) {

View File

@@ -0,0 +1,221 @@
/*
* Copyright 2002-2013 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.xml;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import org.junit.Test;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.integration.MessageHeaders;
import org.springframework.integration.MessageHeaders.IdGenerator;
import org.springframework.integration.MessageHeaders.JdkIdGenerator;
import org.springframework.integration.MessageHeaders.SimpleIncrementingIdGenerator;
import org.springframework.integration.test.util.TestUtils;
/**
* @author Gary Russell
* @since 3.0
*
*/
public class IdGeneratorConfigurerTests {
@Test
public void testOneBean() {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBeanDefinition("bfpp", new RootBeanDefinition(DefaultConfiguringBeanFactoryPostProcessor.class));
context.registerBeanDefinition("foo", new RootBeanDefinition(MyIdGenerator.class));
context.refresh();
MessageHeaders headers = new MessageHeaders(null);
assertEquals(1, headers.getId().getMostSignificantBits());
assertEquals(2, headers.getId().getLeastSignificantBits());
context.destroy();
headers = new MessageHeaders(null);
assertNotEquals(1, headers.getId().getMostSignificantBits());
assertNotEquals(2, headers.getId().getLeastSignificantBits());
assertNull(TestUtils.getPropertyValue(headers, "idGenerator"));
}
@Test
public void testTwoBeans() throws Exception {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBeanDefinition("bfpp", new RootBeanDefinition(DefaultConfiguringBeanFactoryPostProcessor.class));
context.registerBeanDefinition("foo", new RootBeanDefinition(JdkIdGenerator.class));
context.registerBeanDefinition("bar", new RootBeanDefinition(SimpleIncrementingIdGenerator.class));
context.refresh();
// multiple beans are ignored with warning
MessageHeaders headers = new MessageHeaders(null);
assertNull(TestUtils.getPropertyValue(headers, "idGenerator"));
context.destroy();
}
@Test
public void testNoBeans() throws Exception {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBeanDefinition("bfpp", new RootBeanDefinition(DefaultConfiguringBeanFactoryPostProcessor.class));
context.refresh();
MessageHeaders headers = new MessageHeaders(null);
assertNull(TestUtils.getPropertyValue(headers, "idGenerator"));
context.destroy();
}
@Test
public void testTwoContextsSameClass() throws Exception {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBeanDefinition("bfpp", new RootBeanDefinition(DefaultConfiguringBeanFactoryPostProcessor.class));
context.registerBeanDefinition("foo", new RootBeanDefinition(MyIdGenerator.class));
context.refresh();
MessageHeaders headers = new MessageHeaders(null);
assertEquals(1, headers.getId().getMostSignificantBits());
assertEquals(2, headers.getId().getLeastSignificantBits());
GenericApplicationContext context2 = new GenericApplicationContext();
context2.registerBeanDefinition("bfpp", new RootBeanDefinition(DefaultConfiguringBeanFactoryPostProcessor.class));
context2.registerBeanDefinition("foo", new RootBeanDefinition(MyIdGenerator.class));
context2.refresh();
context.destroy();
context2.destroy();
headers = new MessageHeaders(null);
assertNotEquals(1, headers.getId().getMostSignificantBits());
assertNotEquals(2, headers.getId().getLeastSignificantBits());
assertNull(TestUtils.getPropertyValue(headers, "idGenerator"));
}
@Test
public void testTwoContextsSameClassFirstDestroyed() throws Exception {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBeanDefinition("bfpp", new RootBeanDefinition(DefaultConfiguringBeanFactoryPostProcessor.class));
context.registerBeanDefinition("foo", new RootBeanDefinition(MyIdGenerator.class));
context.refresh();
MessageHeaders headers = new MessageHeaders(null);
assertEquals(1, headers.getId().getMostSignificantBits());
assertEquals(2, headers.getId().getLeastSignificantBits());
GenericApplicationContext context2 = new GenericApplicationContext();
context2.registerBeanDefinition("bfpp", new RootBeanDefinition(DefaultConfiguringBeanFactoryPostProcessor.class));
context2.registerBeanDefinition("foo", new RootBeanDefinition(MyIdGenerator.class));
context2.refresh();
context.destroy();
// we should still use the custom strategy
headers = new MessageHeaders(null);
assertEquals(1, headers.getId().getMostSignificantBits());
assertEquals(2, headers.getId().getLeastSignificantBits());
context2.destroy();
// back to default
headers = new MessageHeaders(null);
assertNotEquals(1, headers.getId().getMostSignificantBits());
assertNotEquals(2, headers.getId().getLeastSignificantBits());
assertNull(TestUtils.getPropertyValue(headers, "idGenerator"));
}
@Test
public void testTwoContextDifferentClass() {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBeanDefinition("bfpp", new RootBeanDefinition(DefaultConfiguringBeanFactoryPostProcessor.class));
context.registerBeanDefinition("foo", new RootBeanDefinition(MyIdGenerator.class));
context.refresh();
MessageHeaders headers = new MessageHeaders(null);
assertEquals(1, headers.getId().getMostSignificantBits());
assertEquals(2, headers.getId().getLeastSignificantBits());
GenericApplicationContext context2 = new GenericApplicationContext();
context2.registerBeanDefinition("bfpp", new RootBeanDefinition(DefaultConfiguringBeanFactoryPostProcessor.class));
context2.registerBeanDefinition("foo", new RootBeanDefinition(MyIdGenerator2.class));
try {
context2.refresh();
fail("Expected exception");
}
catch (BeanDefinitionStoreException e) {
assertEquals("'MessageHeaders.idGenerator' has already been set and can not be set again",
e.getMessage());
}
context.destroy();
context2.destroy();
}
@Test
public void testJdk() {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBeanDefinition("bfpp", new RootBeanDefinition(DefaultConfiguringBeanFactoryPostProcessor.class));
context.registerBeanDefinition("foo", new RootBeanDefinition(JdkIdGenerator.class));
context.refresh();
MessageHeaders headers = new MessageHeaders(null);
assertSame(context.getBean(IdGenerator.class), TestUtils.getPropertyValue(headers, "idGenerator"));
context.destroy();
}
@Test
public void testIncrementing() {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBeanDefinition("bfpp", new RootBeanDefinition(DefaultConfiguringBeanFactoryPostProcessor.class));
context.registerBeanDefinition("foo", new RootBeanDefinition(SimpleIncrementingIdGenerator.class));
context.refresh();
IdGenerator idGenerator = context.getBean(IdGenerator.class);
MessageHeaders headers = new MessageHeaders(null);
assertEquals(0, headers.getId().getMostSignificantBits());
assertEquals(1, headers.getId().getLeastSignificantBits());
headers = new MessageHeaders(null);
assertEquals(0, headers.getId().getMostSignificantBits());
assertEquals(2, headers.getId().getLeastSignificantBits());
AtomicLong bottomBits = TestUtils.getPropertyValue(idGenerator, "bottomBits", AtomicLong.class);
bottomBits.set(0xffffffff);
headers = new MessageHeaders(null);
assertEquals(1, headers.getId().getMostSignificantBits());
assertEquals(0, headers.getId().getLeastSignificantBits());
headers = new MessageHeaders(null);
assertEquals(1, headers.getId().getMostSignificantBits());
assertEquals(1, headers.getId().getLeastSignificantBits());
context.destroy();
}
public static class MyIdGenerator implements IdGenerator {
@Override
public UUID generateId() {
return new UUID(1, 2);
}
};
public static class MyIdGenerator2 implements IdGenerator {
@Override
public UUID generateId() {
return new UUID(3, 4);
}
};
}

View File

@@ -116,6 +116,43 @@
Many inbound and outbound adapter implementations will also provide and/or expect certain headers, and additional
user-defined headers can also be configured.
</para>
<section id="message-id-generation">
<title>Message ID Generation</title>
<para>
When a message transitions through an application, each time it is
mutated (e.g. by a transformer) a new message id is assigned. The message id is
a <code>UUID</code>. Beginning with Spring Integration 3.0, the default strategy
used for id generation is to use the <code>com.eaio.uuid</code> package to
generate Type 1 UUIDs. This is much more efficient than the previous
<code>java.util.UUID.randomUUID()</code> implementation.
</para>
<para>
A different UUID generation strategy can be selected by declaring a bean that implements
<interfacename>MessageHeaders.IdGenerator</interfacename> in the application context.
</para>
<important>
Only one UUID generation strategy can be used in a classloader. This means that if
two or more application contexts are running in the same classloader, they will share
the same strategy. If one of the contexts changes the strategy, it will be used by
all contexts. If two or more contexts in the same classloader declare a bean of type
<interfacename>MessageHeaders.IdGenerator</interfacename>, they must all be an instance
of the same class, otherwise the context attempting to replace a custom strategy will
fail to initialize. If the strategy is the same, but parameterized, the strategy in the
first context to initialize will be used.
</important>
<para>
In addition to the default strategy, two additional <interfacename>IdGenerators</interfacename>
are provided; <classname>MessageHeaders.JdkIdGenerator</classname> uses the previous
<code>UUID.randomUUID()</code> mechanism; <classname>MessageHeaders.SimpleIncrementingIdGenerator</classname>
can be used in cases where a UUID is not really needed and a simple incrementing
value is sufficient.
</para>
<important>
The default strategy of creating Type 1 UUIDs may present security concerns for some users
because the UUID contains the MAC address of a network interface on the platform. For these
users, an alternate strategy should be selected.
</important>
</section>
</section>
<section id="message-implementations">

View File

@@ -234,5 +234,15 @@
<classname>ImapIdleExceptionEvent</classname> or one of its super classes.
</para>
</section>
<section id="3.0-message-id">
<title>Message ID Generation</title>
<para>
Previously, message ids were generated using the JDK <code>UUID.randomUUID()</code> method. With this
release, the default mechanism has been changed to use the <code>com.eaio.uuid</code> package which
generates Type 1 UUIDs, and is significantly faster. In addition, the ability to change
the strategy used to generate message ids has been added.
For more information see <xref linkend="message-id-generation"/>.
</para>
</section>
</section>
</chapter>