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; + List methods; + + MethodInvoker(Object delegate, final String methodName) { + this.delegate = delegate; + + Class c = delegate.getClass(); + + methods = new ArrayList(); + + ReflectionUtils.doWithMethods(c, new MethodCallback() { + + public void doWith(Method method) + throws IllegalArgumentException, IllegalAccessException { + ReflectionUtils.makeAccessible(method); + methods.add(method); + } + + }, new MethodFilter() { + public boolean matches(Method method) { + if (Modifier.isPublic(method.getModifiers()) + && methodName.equals(method.getName())) { + // check out the argument numbers + Class[] parameterTypes = method.getParameterTypes(); + + return ((parameterTypes.length == 2 && String.class + .equals(parameterTypes[1])) || parameterTypes.length == 1); + } + return false; + } + }); + + Assert.isTrue(!methods.isEmpty(), + "Cannot find a suitable method named [" + + c.getName() + + "#" + + methodName + + "] - is the method public and has the proper arguments?"); + } + + void invoke(Object[] arguments) throws InvocationTargetException, IllegalAccessException { + + Object[] message = new Object[] {arguments[0]}; + for (Method m : methods) { + Class[] types = m.getParameterTypes(); + Object[] args = (types.length == 2 ? arguments: message); + m.invoke(delegate, args); + } + } + } + /** * Out-of-the-box value for the default listener method: "handleMessage". */ public static final String ORIGINAL_DEFAULT_LISTENER_METHOD = "handleMessage"; - /** Logger available to subclasses */ protected final Log logger = LogFactory.getLog(getClass()); private Object delegate; + private MethodInvoker invoker; + private String defaultListenerMethod = ORIGINAL_DEFAULT_LISTENER_METHOD; private RedisSerializer serializer; + private RedisSerializer stringSerializer; /** * Create a new {@link MessageListenerAdapter} with default settings. @@ -100,26 +180,29 @@ public class MessageListenerAdapter implements MessageListener { /** * Create a new {@link MessageListenerAdapter} for the given delegate. * - * @param delegate the delegate object + * @param delegate + * the delegate object */ public MessageListenerAdapter(Object delegate) { initDefaultStrategies(); setDelegate(delegate); } - /** - * 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. + * 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 serializer) { + this.stringSerializer = serializer; + } + /** * Standard Redis {@link MessageListener} entry point. - *

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 serializer = new StringRedisSerializer(); + setSerializer(serializer); + setStringSerializer(serializer); } /** - * Handle the given exception that arose during listener execution. - * The default implementation logs the exception at error level. - * @param ex the exception to handle + * Handle the given exception that arose during listener execution. The + * default implementation logs the exception at error level. + * + * @param ex + * the exception to handle */ protected void handleListenerException(Throwable ex) { logger.error("Listener execution failed", ex); @@ -217,9 +332,11 @@ public class MessageListenerAdapter implements MessageListener { /** * Extract the message body from the given Redis message. - * @param message the Redis 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. - *

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 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. - *

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