diff --git a/src/main/java/org/springframework/data/redis/listener/adapter/MessageListenerAdapter.java b/src/main/java/org/springframework/data/redis/listener/adapter/MessageListenerAdapter.java index 38cdec9bf..8dcadd215 100644 --- a/src/main/java/org/springframework/data/redis/listener/adapter/MessageListenerAdapter.java +++ b/src/main/java/org/springframework/data/redis/listener/adapter/MessageListenerAdapter.java @@ -16,6 +16,10 @@ package org.springframework.data.redis.listener.adapter; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -27,8 +31,10 @@ import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.util.Assert; -import org.springframework.util.MethodInvoker; import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.MethodCallback; +import org.springframework.util.ReflectionUtils.MethodFilter; /** * Message listener adapter that delegates the handling of messages to target @@ -36,58 +42,132 @@ import org.springframework.util.ObjectUtils; * Allows listener methods to operate on message content types, completely * independent from the Redis API. * - *
Modeled as much as possible after the JMS MessageListenerAdapter in - * Spring Framework. - * - *By default, the content of incoming Redis messages gets extracted before + *
+ * Modeled as much as possible after the JMS MessageListenerAdapter in Spring + * Framework. + * + *
+ * By default, the content of incoming Redis messages gets extracted before
* being passed into the target listener method, to let the target method
- * operate on message content types such as String or byte array instead of
- * the raw {@link Message}. Message type conversion is delegated to a Spring
- * Data {@link RedisSerializer}. By default, the {@link JdkSerializationRedisSerializer}
- * will be used. (If you do not want such automatic message conversion taking
- * place, then be sure to set the {@link #setSerializer Serializer}
- * to null.)
- *
- *
Find below some examples of method signatures compliant with this
- * adapter class. This first example handles all Message types
- * and gets passed the contents of each Message type as an
- * argument.
- *
- *
public interface MessageContentsDelegate {
- * void handleMessage(String text);
- * void handleMessage(byte[] bytes);
- * void handleMessage(Person obj);
- * }
+ * operate on message content types such as String or byte array instead of the
+ * raw {@link Message}. Message type conversion is delegated to a Spring Data
+ * {@link RedisSerializer}. By default, the
+ * {@link JdkSerializationRedisSerializer} will be used. (If you do not want
+ * such automatic message conversion taking place, then be sure to set the
+ * {@link #setSerializer Serializer} to null.)
+ *
+ *
+ * Find below some examples of method signatures compliant with this adapter
+ * class. This first example handles all Message types and gets
+ * passed the contents of each Message type as an argument.
+ *
+ *
+ * public interface MessageContentsDelegate {
+ * void handleMessage(String text);
+ *
+ * void handleMessage(byte[] bytes);
+ *
+ * void handleMessage(Person obj);
+ * }
+ *
+ *
+ * + * In addition, the channel or pattern to which a message is sent can be passed in + * to the method as a second argument of type String: * + *
+ * public interface MessageContentsDelegate {
+ * void handleMessage(String text, String channel);
+ *
+ * void handleMessage(byte[] bytes, String pattern);
+ * }
+ *
+ *
+ *
* For further examples and discussion please do refer to the Spring Data
* reference documentation which describes this class (and its attendant
* configuration) in detail.
*
- * Important: Due to the nature of messages, the default serializer used by
- * the adapter is {@link StringRedisSerializer}. If the messages are of a different type,
- * change them accordingly through {@link #setSerializer(RedisSerializer)}.
- *
+ * Important: Due to the nature of messages, the default serializer used
+ * by the adapter is {@link StringRedisSerializer}. If the messages are of a
+ * different type, change them accordingly through
+ * {@link #setSerializer(RedisSerializer)}.
+ *
* @author Juergen Hoeller
* @author Costin Leau
* @see org.springframework.jms.listener.adapter.MessageListenerAdapter
*/
public class MessageListenerAdapter implements MessageListener {
+ private class MethodInvoker {
+ private final Object delegate;
+ ListIf no explicit delegate object has been specified, listener - * methods are expected to present on this adapter instance, that is, - * on a custom subclass of this adapter, defining listener methods. + * Set a target object to delegate message listening to. Specified listener + * methods have to be present on this target object. + *
+ * If no explicit delegate object has been specified, listener methods are + * expected to present on this adapter instance, that is, on a custom + * subclass of this adapter, defining listener methods. * - * @param delegate delegate object + * @param delegate + * delegate object */ public void setDelegate(Object delegate) { Assert.notNull(delegate, "Delegate must not be null"); this.delegate = delegate; + this.invoker = null; } /** @@ -132,13 +215,16 @@ public class MessageListenerAdapter implements MessageListener { } /** - * Specify the name of the default listener method to delegate to, - * for the case where no specific listener method has been determined. - * Out-of-the-box value is {@link #ORIGINAL_DEFAULT_LISTENER_METHOD "handleMessage"}. + * Specify the name of the default listener method to delegate to, for the + * case where no specific listener method has been determined. + * Out-of-the-box value is {@link #ORIGINAL_DEFAULT_LISTENER_METHOD + * "handleMessage"}. + * * @see #getListenerMethodName */ public void setDefaultListenerMethod(String defaultListenerMethod) { this.defaultListenerMethod = defaultListenerMethod; + this.invoker = null; } /** @@ -151,26 +237,41 @@ public class MessageListenerAdapter implements MessageListener { /** * Set the serializer that will convert incoming raw Redis messages to * listener method arguments. - *
The default converter is a {@link StringRedisSerializer}. + *
+ * The default converter is a {@link StringRedisSerializer}. + * + * @param serializer */ public void setSerializer(RedisSerializer> serializer) { this.serializer = serializer; } + /** + * Sets the serializer used for converting the channel/pattern to a String. + * + *
+ * The default converter is a {@link StringRedisSerializer}.
+ *
+ * @param serializer
+ */
+ public void setStringSerializer(RedisSerializer Delegates the message to the target listener method, with appropriate
+ *
+ * Delegates the message to the target listener method, with appropriate
* conversion of the message argument. In case of an exception, the
* {@link #handleListenerException(Throwable)} method will be invoked.
*
- * @param message the incoming Redis message
+ * @param message
+ * the incoming Redis message
* @see #handleListenerException
*/
@Override
- @SuppressWarnings("unchecked")
public void onMessage(Message message, byte[] pattern) {
try {
-
// Check whether the delegate is a MessageListener impl itself.
// In that case, the adapter will simply act as a pass-through.
if (delegate != this) {
@@ -181,15 +282,25 @@ public class MessageListenerAdapter implements MessageListener {
// Regular case: find a handler method reflectively.
Object convertedMessage = extractMessage(message);
+ String convertedChannel = stringSerializer.deserialize(pattern);
+ // Invoke the handler method with appropriate arguments.
+ Object[] listenerArguments = new Object[] { convertedMessage,
+ convertedChannel };
+
String methodName = getListenerMethodName(message, convertedMessage);
+
if (methodName == null) {
- throw new InvalidDataAccessApiUsageException("No default listener method specified: "
- + "Either specify a non-null value for the 'defaultListenerMethod' property or "
- + "override the 'getListenerMethodName' method.");
+ throw new InvalidDataAccessApiUsageException(
+ "No default listener method specified: "
+ + "Either specify a non-null value for the 'defaultListenerMethod' property or "
+ + "override the 'getListenerMethodName' method.");
}
- // Invoke the handler method with appropriate arguments.
- Object[] listenerArguments = buildListenerArguments(convertedMessage);
+ if (invoker == null) {
+ invoker = new MethodInvoker(delegate, methodName);
+ }
+
+
invokeListenerMethod(methodName, listenerArguments);
} catch (Throwable th) {
handleListenerException(th);
@@ -203,13 +314,17 @@ public class MessageListenerAdapter implements MessageListener {
* @see JdkSerializationRedisSerializer
*/
protected void initDefaultStrategies() {
- setSerializer(new StringRedisSerializer());
+ RedisSerializer The default implementation simply returns the configured
- * default listener method, if any.
- * @param originalMessage the Redis request message
- * @param extractedMessage the converted Redis request message,
- * to be passed into the listener method as argument
+ * Determine the name of the listener method that is supposed to handle the
+ * given message.
+ *
+ * The default implementation simply returns the configured default listener
+ * method, if any.
+ *
+ * @param originalMessage
+ * the Redis request message
+ * @param extractedMessage
+ * the converted Redis request message, to be passed into the
+ * listener method as argument
* @return the name of the listener method (never The default implementation builds an array with the given message object
- * as sole element. This means that the extracted message will always be passed
- * into a single method argument, even if it is an array, with the target
- * method having a corresponding single argument of the array's type declared.
- * This can be overridden to treat special message content such as arrays
- * differently, for example passing in each element of the message array
- * as distinct method argument.
- * @param extractedMessage the content of the message
- * @return the array of arguments to be passed into the
- * listener method (each element of the array corresponding
- * to a distinct method argument)
- */
- protected Object[] buildListenerArguments(Object extractedMessage) {
- return new Object[] { extractedMessage };
- }
-
/**
* Invoke the specified listener method.
- * @param methodName the name of the listener method
- * @param arguments the message arguments to be passed in
- * @return the result returned from the listener method
+ *
+ * @param methodName
+ * the name of the listener method
+ * @param arguments
+ * the message arguments to be passed in
* @see #getListenerMethodName
* @see #buildListenerArguments
*/
- protected Object invokeListenerMethod(String methodName, Object[] arguments) {
+ protected void invokeListenerMethod(String methodName, Object[] arguments) {
try {
- MethodInvoker methodInvoker = new MethodInvoker();
- methodInvoker.setTargetObject(getDelegate());
- methodInvoker.setTargetMethod(methodName);
- methodInvoker.setArguments(arguments);
- methodInvoker.prepare();
- return methodInvoker.invoke();
+ invoker.invoke(arguments);
} catch (InvocationTargetException ex) {
Throwable targetEx = ex.getTargetException();
if (targetEx instanceof DataAccessException) {
throw (DataAccessException) targetEx;
- }
- else {
- throw new RedisListenerExecutionFailedException("Listener method '" + methodName + "' threw exception",
+ } else {
+ throw new RedisListenerExecutionFailedException(
+ "Listener method '" + methodName + "' threw exception",
targetEx);
}
} catch (Throwable ex) {
- throw new RedisListenerExecutionFailedException("Failed to invoke target method '" + methodName
- + "' with arguments " + ObjectUtils.nullSafeToString(arguments), ex);
+ throw new RedisListenerExecutionFailedException(
+ "Failed to invoke target method '" + methodName
+ + "' with arguments "
+ + ObjectUtils.nullSafeToString(arguments), ex);
}
}
}
\ No newline at end of file
diff --git a/src/test/java/org/springframework/data/redis/listener/adapter/MessageListenerTest.java b/src/test/java/org/springframework/data/redis/listener/adapter/MessageListenerTest.java
index ad8d91e5a..ba584acc7 100644
--- a/src/test/java/org/springframework/data/redis/listener/adapter/MessageListenerTest.java
+++ b/src/test/java/org/springframework/data/redis/listener/adapter/MessageListenerTest.java
@@ -41,7 +41,8 @@ public class MessageListenerTest {
private static final byte[] RAW_CHANNEL = serializer.serialize(CHANNEL);
private static final String PAYLOAD = "do re mi";
private static final byte[] RAW_PAYLOAD = serializer.serialize(PAYLOAD);
- private static final Message STRING_MSG = new DefaultMessage(RAW_CHANNEL, RAW_PAYLOAD);
+ private static final Message STRING_MSG = new DefaultMessage(RAW_CHANNEL,
+ RAW_PAYLOAD);
private MessageListenerAdapter adapter;
@@ -49,6 +50,8 @@ public class MessageListenerTest {
void handleMessage(String argument);
void customMethod(String arg);
+
+ void customMethodWithChannel(String arg, String channel);
}
@Mock
@@ -67,20 +70,31 @@ public class MessageListenerTest {
}
@Test
- public void testThatTheDefaultMessageHandlingMethodNameIsTheConstantDefault() throws Exception {
- assertEquals(MessageListenerAdapter.ORIGINAL_DEFAULT_LISTENER_METHOD, adapter.getDefaultListenerMethod());
+ public void testThatTheDefaultMessageHandlingMethodNameIsTheConstantDefault()
+ throws Exception {
+ assertEquals(MessageListenerAdapter.ORIGINAL_DEFAULT_LISTENER_METHOD,
+ adapter.getDefaultListenerMethod());
}
- @Test
+ @Test(expected = IllegalStateException.class)
+ // TODO: revisit the exception/method discovery during invocation
+ // TODO: potentially move the discovery early on
public void testAdapterWithListenerAndDefaultMessage() throws Exception {
MessageListener mock = mock(MessageListener.class);
- MessageListenerAdapter adapter = new MessageListenerAdapter(mock);
+ MessageListenerAdapter adapter = new MessageListenerAdapter(mock) {
+
+ @Override
+ protected void handleListenerException(Throwable ex) {
+ throw new IllegalStateException(ex);
+ }
+ };
+
adapter.onMessage(STRING_MSG, null);
verify(mock).onMessage(STRING_MSG, null);
}
-
+ @Test
public void testRawMessage() throws Exception {
MessageListenerAdapter adapter = new MessageListenerAdapter(target);
adapter.onMessage(STRING_MSG, null);
@@ -88,7 +102,7 @@ public class MessageListenerTest {
verify(target).handleMessage(PAYLOAD);
}
-
+ @Test
public void testCustomMethod() throws Exception {
MessageListenerAdapter adapter = new MessageListenerAdapter(target);
adapter.setDefaultListenerMethod("customMethod");
@@ -96,4 +110,13 @@ public class MessageListenerTest {
verify(target).customMethod(PAYLOAD);
}
+
+ @Test
+ public void testCustomMethodWithChannel() throws Exception {
+ MessageListenerAdapter adapter = new MessageListenerAdapter(target);
+ adapter.setDefaultListenerMethod("customMethodWithChannel");
+ adapter.onMessage(STRING_MSG, RAW_CHANNEL);
+
+ verify(target).customMethodWithChannel(PAYLOAD, CHANNEL);
+ }
}
\ No newline at end of file
Message
- * @return the content of the message, to be passed into the
- * listener method as argument
+ *
+ * @param message
+ * the Redis Message
+ * @return the content of the message, to be passed into the listener method
+ * as argument
*/
protected Object extractMessage(Message message) {
if (serializer != null) {
@@ -229,67 +346,52 @@ public class MessageListenerAdapter implements MessageListener {
}
/**
- * Determine the name of the listener method that is supposed to
- * handle the given message.
- * null)
* @see #setDefaultListenerMethod
*/
- protected String getListenerMethodName(Message originalMessage, Object extractedMessage) {
+ protected String getListenerMethodName(Message originalMessage,
+ Object extractedMessage) {
return getDefaultListenerMethod();
}
- /**
- * Build an array of arguments to be passed into the target listener method.
- * Allows for multiple method arguments to be built from a single message object.
- *