DATAREDIS-77
This commit is contained in:
@@ -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.
|
||||
*
|
||||
* <p/>Modeled as much as possible after the JMS MessageListenerAdapter in
|
||||
* Spring Framework.
|
||||
*
|
||||
* <p>By default, the content of incoming Redis messages gets extracted before
|
||||
* <p/>
|
||||
* Modeled as much as possible after the JMS MessageListenerAdapter in Spring
|
||||
* Framework.
|
||||
*
|
||||
* <p>
|
||||
* 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 <code>null</code>.)
|
||||
*
|
||||
* <p>Find below some examples of method signatures compliant with this
|
||||
* adapter class. This first example handles all <code>Message</code> types
|
||||
* and gets passed the contents of each <code>Message</code> type as an
|
||||
* argument.
|
||||
*
|
||||
* <pre class="code">public interface MessageContentsDelegate {
|
||||
* void handleMessage(String text);
|
||||
* void handleMessage(byte[] bytes);
|
||||
* void handleMessage(Person obj);
|
||||
* }</pre>
|
||||
* 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 <code>null</code>.)
|
||||
*
|
||||
* <p>
|
||||
* Find below some examples of method signatures compliant with this adapter
|
||||
* class. This first example handles all <code>Message</code> types and gets
|
||||
* passed the contents of each <code>Message</code> type as an argument.
|
||||
*
|
||||
* <pre class="code">
|
||||
* public interface MessageContentsDelegate {
|
||||
* void handleMessage(String text);
|
||||
*
|
||||
* void handleMessage(byte[] bytes);
|
||||
*
|
||||
* void handleMessage(Person obj);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* 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:
|
||||
*
|
||||
* <pre class="code">
|
||||
* public interface MessageContentsDelegate {
|
||||
* void handleMessage(String text, String channel);
|
||||
*
|
||||
* void handleMessage(byte[] bytes, String pattern);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
*
|
||||
* For further examples and discussion please do refer to the Spring Data
|
||||
* reference documentation which describes this class (and its attendant
|
||||
* configuration) in detail.
|
||||
*
|
||||
* <b>Important:</b> 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)}.
|
||||
*
|
||||
* <b>Important:</b> 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<Method> methods;
|
||||
|
||||
MethodInvoker(Object delegate, final String methodName) {
|
||||
this.delegate = delegate;
|
||||
|
||||
Class<?> c = delegate.getClass();
|
||||
|
||||
methods = new ArrayList<Method>();
|
||||
|
||||
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<String> 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.
|
||||
* <p>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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>The default converter is a {@link StringRedisSerializer}.
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* The default converter is a {@link StringRedisSerializer}.
|
||||
*
|
||||
* @param serializer
|
||||
*/
|
||||
public void setStringSerializer(RedisSerializer<String> serializer) {
|
||||
this.stringSerializer = serializer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard Redis {@link MessageListener} entry point.
|
||||
* <p>Delegates the message to the target listener method, with appropriate
|
||||
* <p>
|
||||
* 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<String> 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 <code>Message</code>
|
||||
* @return the content of the message, to be passed into the
|
||||
* listener method as argument
|
||||
*
|
||||
* @param message
|
||||
* the Redis <code>Message</code>
|
||||
* @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.
|
||||
* <p>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.
|
||||
* <p>
|
||||
* 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 <code>null</code>)
|
||||
* @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.
|
||||
* <p>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 <i>single</i> method argument, even if it is an array, with the target
|
||||
* method having a corresponding single argument of the array's type declared.
|
||||
* <p>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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user