diff --git a/spring-messaging/src/main/java/org/springframework/messaging/MessageHeaders.java b/spring-messaging/src/main/java/org/springframework/messaging/MessageHeaders.java index 3a6c9bd6c1..ef635e703a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/MessageHeaders.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/MessageHeaders.java @@ -20,6 +20,8 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; +import java.math.BigInteger; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -28,6 +30,7 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.Set; import java.util.UUID; @@ -56,6 +59,7 @@ import org.apache.commons.logging.LogFactory; * @author Arjen Poutsma * @author Mark Fisher * @author Gary Russell + * @author Rossen Stoyanchev * @since 4.0 * @see org.springframework.messaging.support.MessageBuilder */ @@ -65,9 +69,10 @@ public final class MessageHeaders implements Map, Serializable { private static final Log logger = LogFactory.getLog(MessageHeaders.class); - private static volatile IdGenerator idGenerator = null; + private static volatile IdGenerator defaultIdGenerator = new AlternativeJdkIdGenerator(); + /** * The key for the Message ID. This is an automatically generated UUID and * should never be explicitly set in the header map except in the @@ -92,13 +97,8 @@ public final class MessageHeaders implements Map, Serializable { public MessageHeaders(Map headers) { this.headers = (headers != null) ? new HashMap(headers) : new HashMap(); - if (MessageHeaders.idGenerator == null){ - this.headers.put(ID, UUID.randomUUID()); - } - else { - this.headers.put(ID, MessageHeaders.idGenerator.generateId()); - } - + IdGenerator generatorToUse = (idGenerator != null) ? idGenerator : defaultIdGenerator; + this.headers.put(ID, generatorToUse.generateId()); this.headers.put(TIMESTAMP, new Long(System.currentTimeMillis())); } @@ -247,4 +247,37 @@ public final class MessageHeaders implements Map, Serializable { public static interface IdGenerator { UUID generateId(); } + + /** + * A variation of {@link UUID#randomUUID()} that uses {@link SecureRandom} only for + * the initial seed and {@link Random} thereafter, which provides better performance + * in exchange for less securely random id's. + */ + public static class AlternativeJdkIdGenerator implements IdGenerator { + + private final Random random; + + public AlternativeJdkIdGenerator() { + byte[] seed = new SecureRandom().generateSeed(8); + this.random = new Random(new BigInteger(seed).longValue()); + } + + public UUID generateId() { + + byte[] randomBytes = new byte[16]; + this.random.nextBytes(randomBytes); + + long mostSigBits = 0; + for (int i = 0; i < 8; i++) { + mostSigBits = (mostSigBits << 8) | (randomBytes[i] & 0xff); + } + long leastSigBits = 0; + for (int i = 8; i < 16; i++) { + leastSigBits = (leastSigBits << 8) | (randomBytes[i] & 0xff); + } + + return new UUID(mostSigBits, leastSigBits); + } + } + } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/MessageHeadersTests.java b/spring-messaging/src/test/java/org/springframework/messaging/MessageHeadersTests.java new file mode 100644 index 0000000000..7b7446caa8 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/MessageHeadersTests.java @@ -0,0 +1,154 @@ +/* + * 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.messaging; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Test fixture for {@link MessageHeaders}. + * + * @author Rossen Stoyanchev + */ +public class MessageHeadersTests { + + + @Test + public void testTimestamp() { + MessageHeaders headers = new MessageHeaders(null); + assertNotNull(headers.getTimestamp()); + } + + @Test + public void testTimestampOverwritten() throws Exception { + MessageHeaders headers1 = new MessageHeaders(null); + Thread.sleep(50L); + MessageHeaders headers2 = new MessageHeaders(headers1); + assertNotSame(headers1.getTimestamp(), headers2.getTimestamp()); + } + + @Test + public void testIdOverwritten() throws Exception { + MessageHeaders headers1 = new MessageHeaders(null); + MessageHeaders headers2 = new MessageHeaders(headers1); + assertNotSame(headers1.getId(), headers2.getId()); + } + + @Test + public void testId() { + MessageHeaders headers = new MessageHeaders(null); + assertNotNull(headers.getId()); + } + + @Test + public void testNonTypedAccessOfHeaderValue() { + Integer value = new Integer(123); + Map map = new HashMap(); + map.put("test", value); + MessageHeaders headers = new MessageHeaders(map); + assertEquals(value, headers.get("test")); + } + + @Test + public void testTypedAccessOfHeaderValue() { + Integer value = new Integer(123); + Map map = new HashMap(); + map.put("test", value); + MessageHeaders headers = new MessageHeaders(map); + assertEquals(value, headers.get("test", Integer.class)); + } + + @Test(expected = IllegalArgumentException.class) + public void testHeaderValueAccessWithIncorrectType() { + Integer value = new Integer(123); + Map map = new HashMap(); + map.put("test", value); + MessageHeaders headers = new MessageHeaders(map); + assertEquals(value, headers.get("test", String.class)); + } + + @Test + public void testNullHeaderValue() { + Map map = new HashMap(); + MessageHeaders headers = new MessageHeaders(map); + assertNull(headers.get("nosuchattribute")); + } + + @Test + public void testNullHeaderValueWithTypedAccess() { + Map map = new HashMap(); + MessageHeaders headers = new MessageHeaders(map); + assertNull(headers.get("nosuchattribute", String.class)); + } + + @Test + public void testHeaderKeys() { + Map map = new HashMap(); + map.put("key1", "val1"); + map.put("key2", new Integer(123)); + MessageHeaders headers = new MessageHeaders(map); + Set keys = headers.keySet(); + assertTrue(keys.contains("key1")); + assertTrue(keys.contains("key2")); + } + + @Test + public void serializeWithAllSerializableHeaders() throws Exception { + Map map = new HashMap(); + map.put("name", "joe"); + map.put("age", 42); + MessageHeaders input = new MessageHeaders(map); + MessageHeaders output = (MessageHeaders) serializeAndDeserialize(input); + assertEquals("joe", output.get("name")); + assertEquals(42, output.get("age")); + } + + @Test + public void serializeWithNonSerializableHeader() throws Exception { + Object address = new Object(); + Map map = new HashMap(); + map.put("name", "joe"); + map.put("address", address); + MessageHeaders input = new MessageHeaders(map); + MessageHeaders output = (MessageHeaders) serializeAndDeserialize(input); + assertEquals("joe", output.get("name")); + assertNull(output.get("address")); + } + + + private static Object serializeAndDeserialize(Object object) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream(baos); + out.writeObject(object); + out.close(); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream in = new ObjectInputStream(bais); + Object result = in.readObject(); + in.close(); + return result; + } + +}