Java tutorial
/* * Copyright 2002-2019 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 * * https://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.amqp.rabbit.listener.adapter; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import org.springframework.amqp.AmqpIOException; import org.springframework.amqp.AmqpIllegalStateException; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.util.Assert; import org.springframework.util.MethodInvoker; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import com.rabbitmq.client.Channel; /** * Message listener adapter that delegates the handling of messages to target listener methods via reflection, with * flexible message type conversion. Allows listener methods to operate on message content types, completely independent * from the Rabbit API. * * <p> * By default, the content of incoming Rabbit 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 AMQ {@link MessageConverter}. By default, a * {@link org.springframework.amqp.support.converter.SimpleMessageConverter} will be used. * (If you do not want such automatic message conversion taking place, then * be sure to set the {@link #setMessageConverter MessageConverter} to <code>null</code>.) * * <p> * If a target listener method returns a non-null object (typically of a message content type such as * <code>String</code> or byte array), it will get wrapped in a Rabbit <code>Message</code> and sent to the exchange of * the incoming message with the routingKey that comes from the Rabbit ReplyTo property or via * {@link #setResponseRoutingKey(String) specified routingKey}). * * <p> * <b>Note:</b> The sending of response messages is only available when using the {@link ChannelAwareMessageListener} * entry point (typically through a Spring message listener container). Usage as {@link MessageListener} does <i>not</i> * support the generation of response messages. * * <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. No * <code>Message</code> will be sent back as all of these methods return <code>void</code>. * * <pre class="code"> * public interface MessageContentsDelegate { * void handleMessage(String text); * * void handleMessage(Map map); * * void handleMessage(byte[] bytes); * * void handleMessage(Serializable obj); * } * </pre> * * This next example handle a <code>Message</code> type and gets passed the actual (raw) <code>Message</code> as an * argument. Again, no <code>Message</code> will be sent back as all of these methods return <code>void</code>. * * <pre class="code"> * public interface RawMessageDelegate { * void handleMessage(Message message); * } * </pre> * * This next example illustrates a <code>Message</code> delegate that just consumes the <code>String</code> contents of * {@link Message Messages}. Notice also how the name of the <code>Message</code> handling method is different from the * {@link #ORIGINAL_DEFAULT_LISTENER_METHOD original} (this will have to be configured in the attandant bean * definition). Again, no <code>Message</code> will be sent back as the method returns <code>void</code>. * * <pre class="code"> * public interface TextMessageContentDelegate { * void onMessage(String text); * } * </pre> * * This final example illustrates a <code>Message</code> delegate that just consumes the <code>String</code> contents of * {@link Message Messages}. Notice how the return type of this method is <code>String</code>: This will result in the * configured {@link MessageListenerAdapter} sending a {@link Message} in response. * * <pre class="code"> * public interface ResponsiveTextMessageContentDelegate { * String handleMessage(String text); * } * </pre> * * For further examples and discussion please do refer to the Spring reference documentation which describes this class * (and its attendant XML configuration) in detail. * * @author Juergen Hoeller * @author Mark Pollack * @author Mark Fisher * @author Dave Syer * @author Gary Russell * @author Greg Turnquist * @author Cai Kun * * @see #setDelegate * @see #setDefaultListenerMethod * @see #setResponseRoutingKey(String) * @see #setMessageConverter * @see org.springframework.amqp.support.converter.SimpleMessageConverter * @see org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener * @see org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer#setMessageListener(MessageListener) */ public class MessageListenerAdapter extends AbstractAdaptableMessageListener { private final Map<String, String> queueOrTagToMethodName = new HashMap<String, String>(); /** * Out-of-the-box value for the default listener method: "handleMessage". */ public static final String ORIGINAL_DEFAULT_LISTENER_METHOD = "handleMessage"; private Object delegate; private String defaultListenerMethod = ORIGINAL_DEFAULT_LISTENER_METHOD; /** * Create a new {@link MessageListenerAdapter} with default settings. */ public MessageListenerAdapter() { this.delegate = this; } /** * Create a new {@link MessageListenerAdapter} for the given delegate. * @param delegate the delegate object */ public MessageListenerAdapter(Object delegate) { doSetDelegate(delegate); } /** * Create a new {@link MessageListenerAdapter} for the given delegate. * @param delegate the delegate object * @param messageConverter the message converter to use */ public MessageListenerAdapter(Object delegate, MessageConverter messageConverter) { doSetDelegate(delegate); super.setMessageConverter(messageConverter); } /** * Create a new {@link MessageListenerAdapter} for the given delegate while also * declaring its POJO method. * @param delegate the delegate object * @param defaultListenerMethod name of the POJO method to call upon message receipt */ public MessageListenerAdapter(Object delegate, String defaultListenerMethod) { this(delegate); this.defaultListenerMethod = defaultListenerMethod; } /** * 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 The delegate listener or POJO. */ public void setDelegate(Object delegate) { doSetDelegate(delegate); } private void doSetDelegate(Object delegate) { Assert.notNull(delegate, "Delegate must not be null"); this.delegate = delegate; } /** * @return The target object to delegate message listening to. */ protected Object getDelegate() { return this.delegate; } /** * 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"}. * @param defaultListenerMethod The name of the default listener method. * @see #getListenerMethodName */ public void setDefaultListenerMethod(String defaultListenerMethod) { this.defaultListenerMethod = defaultListenerMethod; } /** * @return The name of the default listener method to delegate to. */ protected String getDefaultListenerMethod() { return this.defaultListenerMethod; } /** * Set the mapping of queue name or consumer tag to method name. The first lookup * is by queue name, if that returns null, we lookup by consumer tag, if that * returns null, the {@link #setDefaultListenerMethod(String) defaultListenerMethod} * is used. * @param queueOrTagToMethodName the map. * @since 1.5 */ public void setQueueOrTagToMethodName(Map<String, String> queueOrTagToMethodName) { this.queueOrTagToMethodName.putAll(queueOrTagToMethodName); } /** * Add the mapping of a queue name or consumer tag to a method name. The first lookup * is by queue name, if that returns null, we lookup by consumer tag, if that * returns null, the {@link #setDefaultListenerMethod(String) defaultListenerMethod} * is used. * @param queueOrTag The queue name or consumer tag. * @param methodName The method name. * @since 1.5 */ public void addQueueOrTagToMethodName(String queueOrTag, String methodName) { this.queueOrTagToMethodName.put(queueOrTag, methodName); } /** * Remove the mapping of a queue name or consumer tag to a method name. * @param queueOrTag The queue name or consumer tag. * @return the method name that was removed, or null. * @since 1.5 */ public String removeQueueOrTagToMethodName(String queueOrTag) { return this.queueOrTagToMethodName.remove(queueOrTag); } /** * Spring {@link ChannelAwareMessageListener} entry point. * <p> * Delegates the message to the target listener method, with appropriate conversion of the message argument. If the * target method returns a non-null object, wrap in a Rabbit message and send it back. * @param message the incoming Rabbit message * @param channel the Rabbit channel to operate on * @throws Exception if thrown by Rabbit API methods */ @Override public void onMessage(Message message, Channel channel) throws Exception { // NOSONAR // Check whether the delegate is a MessageListener impl itself. // In that case, the adapter will simply act as a pass-through. Object delegateListener = getDelegate(); if (delegateListener != this) { if (delegateListener instanceof ChannelAwareMessageListener) { ((ChannelAwareMessageListener) delegateListener).onMessage(message, channel); return; } else if (delegateListener instanceof MessageListener) { ((MessageListener) delegateListener).onMessage(message); return; } } // Regular case: find a handler method reflectively. Object convertedMessage = extractMessage(message); String methodName = getListenerMethodName(message, convertedMessage); if (methodName == null) { throw new AmqpIllegalStateException("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, channel, message); Object result = invokeListenerMethod(methodName, listenerArguments, message); if (result != null) { handleResult(new InvocationResult(result, null, null), message, channel); } else { logger.trace("No result object given - no result to handle"); } } /** * Determine the name of the listener method that will handle the given message. * <p> * The default implementation first consults the * {@link #setQueueOrTagToMethodName(Map) queueOrTagToMethodName} map looking for a * match on the consumer queue or consumer tag; if no match found, it simply returns * the configured default listener method, or "handleMessage" if not configured. * @param originalMessage the Rabbit request message * @param extractedMessage the converted Rabbit request message, to be passed into the * listener method as argument * @return the name of the listener method (never <code>null</code>) * @see #setDefaultListenerMethod * @see #setQueueOrTagToMethodName */ protected String getListenerMethodName(Message originalMessage, Object extractedMessage) { if (this.queueOrTagToMethodName.size() > 0) { MessageProperties props = originalMessage.getMessageProperties(); String methodName = this.queueOrTagToMethodName.get(props.getConsumerQueue()); if (methodName == null) { methodName = this.queueOrTagToMethodName.get(props.getConsumerTag()); } if (methodName != null) { return methodName; } } 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) * @deprecated use @{@link #buildListenerArguments(Object, Channel, Message)} to get complete arguments */ @Deprecated protected Object[] buildListenerArguments(Object extractedMessage) { return new Object[] { extractedMessage }; } /** * Build an array of arguments to be passed into the target listener method. Allows for multiple method arguments to * be built from message object with channel, More detail about {@code extractedMessage} in the method * {@link #buildListenerArguments(java.lang.Object)}. * This can be overridden to treat special message content such as arrays differently, and add argument in case of * receiving Channel and original Message object to invoke basicAck method in the listener by manual acknowledge * mode. * @param extractedMessage the content of the message * @param channel the Rabbit channel to operate on * @param message the incoming Rabbit message * @return the array of arguments to be passed into the listener method (each element of the array corresponding to * a distinct method argument) */ @SuppressWarnings("deprecation") protected Object[] buildListenerArguments(Object extractedMessage, Channel channel, Message message) { return buildListenerArguments(extractedMessage); } /** * Invoke the specified listener method. * @param methodName the name of the listener method * @param arguments the message arguments to be passed in * @param originalMessage the original message * @return the result returned from the listener method * @see #getListenerMethodName * @see #buildListenerArguments */ protected Object invokeListenerMethod(String methodName, Object[] arguments, Message originalMessage) { try { MethodInvoker methodInvoker = new MethodInvoker(); methodInvoker.setTargetObject(getDelegate()); methodInvoker.setTargetMethod(methodName); methodInvoker.setArguments(arguments); methodInvoker.prepare(); return methodInvoker.invoke(); } catch (InvocationTargetException ex) { Throwable targetEx = ex.getTargetException(); if (targetEx instanceof IOException) { throw new AmqpIOException((IOException) targetEx); // NOSONAR lost stack trace } else { throw new ListenerExecutionFailedException("Listener method '" // NOSONAR lost stack trace + methodName + "' threw exception", targetEx, originalMessage); } } catch (Exception ex) { ArrayList<String> arrayClass = new ArrayList<>(); if (arguments != null) { for (Object argument : arguments) { arrayClass.add(argument.getClass().toString()); } } throw new ListenerExecutionFailedException("Failed to invoke target method '" + methodName + "' with argument type = [" + StringUtils.collectionToCommaDelimitedString(arrayClass) + "], value = [" + ObjectUtils.nullSafeToString(arguments) + "]", ex, originalMessage); } } }