/* * Copyright 2002-2017 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.amqp.rabbit.listener.adapter; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.WildcardType; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.listener.RabbitListenerErrorHandler; import org.springframework.amqp.rabbit.listener.exception.ListenerExecutionFailedException; import org.springframework.amqp.support.AmqpHeaderMapper; import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.MessagingMessageConverter; import org.springframework.core.MethodParameter; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.remoting.support.RemoteInvocationResult; import org.springframework.util.Assert; import com.rabbitmq.client.Channel; /** * A {@link org.springframework.amqp.core.MessageListener MessageListener} * adapter that invokes a configurable {@link HandlerAdapter}. * * <p>Wraps the incoming {@link org.springframework.amqp.core.Message * AMQP Message} to Spring's {@link Message} abstraction, copying the * standard headers using a configurable * {@link org.springframework.amqp.support.AmqpHeaderMapper AmqpHeaderMapper}. * * <p>The original {@link org.springframework.amqp.core.Message Message} and * the {@link Channel} are provided as additional arguments so that these can * be injected as method arguments if necessary. * * @author Stephane Nicoll * @author Gary Russell * @author Artem Bilan * * @since 1.4 */ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageListener { private HandlerAdapter handlerMethod; private final MessagingMessageConverterAdapter messagingMessageConverter; private final boolean returnExceptions; private final RabbitListenerErrorHandler errorHandler; public MessagingMessageListenerAdapter() { this(null, null); } public MessagingMessageListenerAdapter(Object bean, Method method) { this(bean, method, false, null); } public MessagingMessageListenerAdapter(Object bean, Method method, boolean returnExceptions, RabbitListenerErrorHandler errorHandler) { this.messagingMessageConverter = new MessagingMessageConverterAdapter(bean, method); this.returnExceptions = returnExceptions; this.errorHandler = errorHandler; } /** * Set the {@link HandlerAdapter} to use to invoke the method * processing an incoming {@link org.springframework.amqp.core.Message}. * @param handlerMethod {@link HandlerAdapter} instance. */ public void setHandlerMethod(HandlerAdapter handlerMethod) { this.handlerMethod = handlerMethod; } /** * Set the {@link AmqpHeaderMapper} implementation to use to map the standard * AMQP headers. By default, a {@link org.springframework.amqp.support.SimpleAmqpHeaderMapper * SimpleAmqpHeaderMapper} is used. * @param headerMapper the {@link AmqpHeaderMapper} instance. * @see org.springframework.amqp.support.SimpleAmqpHeaderMapper */ public void setHeaderMapper(AmqpHeaderMapper headerMapper) { Assert.notNull(headerMapper, "HeaderMapper must not be null"); this.messagingMessageConverter.setHeaderMapper(headerMapper); } /** * @return the {@link MessagingMessageConverter} for this listener, * being able to convert {@link org.springframework.messaging.Message}. */ protected final MessagingMessageConverter getMessagingMessageConverter() { return this.messagingMessageConverter; } @Override public void onMessage(org.springframework.amqp.core.Message amqpMessage, Channel channel) throws Exception { Message<?> message = toMessagingMessage(amqpMessage); if (logger.isDebugEnabled()) { logger.debug("Processing [" + message + "]"); } try { Object result = invokeHandler(amqpMessage, channel, message); if (result != null) { handleResult(result, amqpMessage, channel, message); } else { logger.trace("No result object given - no result to handle"); } } catch (ListenerExecutionFailedException e) { if (this.errorHandler != null) { try { Object result = this.errorHandler.handleError(amqpMessage, message, e); if (result != null) { handleResult(result, amqpMessage, channel, message); } else { logger.trace("Error handler returned no result"); } } catch (Exception ex) { returnOrThrow(amqpMessage, channel, message, ex, ex); } } else { returnOrThrow(amqpMessage, channel, message, e.getCause(), e); } } } private void returnOrThrow(org.springframework.amqp.core.Message amqpMessage, Channel channel, Message<?> message, Throwable throwableToReturn, Exception exceptionToThrow) throws Exception { if (!this.returnExceptions) { throw exceptionToThrow; } try { handleResult(new RemoteInvocationResult(throwableToReturn), amqpMessage, channel, message); } catch (ReplyFailureException rfe) { if (void.class.equals(this.handlerMethod.getReturnType(message.getPayload()))) { throw exceptionToThrow; } else { throw rfe; } } } protected Message<?> toMessagingMessage(org.springframework.amqp.core.Message amqpMessage) { return (Message<?>) getMessagingMessageConverter().fromMessage(amqpMessage); } /** * Invoke the handler, wrapping any exception to a {@link ListenerExecutionFailedException} * with a dedicated error message. * @param amqpMessage the raw message. * @param channel the channel. * @param message the messaging message. * @return the result of invoking the handler. */ private Object invokeHandler(org.springframework.amqp.core.Message amqpMessage, Channel channel, Message<?> message) { try { return this.handlerMethod.invoke(message, amqpMessage, channel); } catch (MessagingException ex) { throw new ListenerExecutionFailedException(createMessagingErrorMessage("Listener method could not " + "be invoked with the incoming message", message.getPayload()), ex, amqpMessage); } catch (Exception ex) { throw new ListenerExecutionFailedException("Listener method '" + this.handlerMethod.getMethodAsString(message.getPayload()) + "' threw exception", ex, amqpMessage); } } private String createMessagingErrorMessage(String description, Object payload) { return description + "\n" + "Endpoint handler details:\n" + "Method [" + this.handlerMethod.getMethodAsString(payload) + "]\n" + "Bean [" + this.handlerMethod.getBean() + "]"; } /** * Build a Rabbit message to be sent as response based on the given result object. * @param channel the Rabbit Channel to operate on * @param result the content of the message, as returned from the listener method * @return the Rabbit <code>Message</code> (never <code>null</code>) * @throws Exception if thrown by Rabbit API methods * @see #setMessageConverter */ @Override protected org.springframework.amqp.core.Message buildMessage(Channel channel, Object result) throws Exception { MessageConverter converter = getMessageConverter(); if (converter != null && !(result instanceof org.springframework.amqp.core.Message)) { if (result instanceof org.springframework.messaging.Message) { return this.messagingMessageConverter.toMessage(result, new MessageProperties()); } else { return converter.toMessage(result, new MessageProperties()); } } else { if (!(result instanceof org.springframework.amqp.core.Message)) { throw new MessageConversionException("No MessageConverter specified - cannot handle message [" + result + "]"); } return (org.springframework.amqp.core.Message) result; } } /** * Delegates payload extraction to * {@link #extractMessage(org.springframework.amqp.core.Message message)} * to enforce backward compatibility. Uses this listener adapter's converter instead of * the one configured in the converter adapter. * If the inbound message has no type information and the configured message converter * supports it, we attempt to infer the conversion type from the method signature. */ private final class MessagingMessageConverterAdapter extends MessagingMessageConverter { private final Object bean; private final Method method; private final Type inferredArgumentType; MessagingMessageConverterAdapter(Object bean, Method method) { this.bean = bean; this.method = method; this.inferredArgumentType = determineInferredType(); if (logger.isDebugEnabled() && this.inferredArgumentType != null) { logger.debug("Inferred argument type for " + method.toString() + " is " + this.inferredArgumentType); } } @Override protected Object extractPayload(org.springframework.amqp.core.Message message) { MessageProperties messageProperties = message.getMessageProperties(); if (this.bean != null) { messageProperties.setTargetBean(this.bean); } if (this.method != null) { messageProperties.setTargetMethod(this.method); if (this.inferredArgumentType != null) { messageProperties.setInferredArgumentType(this.inferredArgumentType); } } return extractMessage(message); } private Type determineInferredType() { if (this.method == null) { return null; } Type genericParameterType = null; for (int i = 0; i < this.method.getParameterCount(); i++) { MethodParameter methodParameter = new MethodParameter(this.method, i); /* * We're looking for a single non-annotated parameter, or one annotated with @Payload. * We ignore parameters with type Message because they are not involved with conversion. */ if (isEligibleParameter(methodParameter) && (methodParameter.getParameterAnnotations().length == 0 || methodParameter.hasParameterAnnotation(Payload.class))) { if (genericParameterType == null) { genericParameterType = methodParameter.getGenericParameterType(); if (genericParameterType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) genericParameterType; if (parameterizedType.getRawType().equals(Message.class)) { genericParameterType = ((ParameterizedType) genericParameterType) .getActualTypeArguments()[0]; } } } else { if (MessagingMessageListenerAdapter.this.logger.isDebugEnabled()) { MessagingMessageListenerAdapter.this.logger .debug("Ambiguous parameters for target payload for method " + this.method + "; no inferred type header added"); } return null; } } } return genericParameterType; } /* * Don't consider parameter types that are available after conversion. * Message, Message<?> and Channel. */ private boolean isEligibleParameter(MethodParameter methodParameter) { Type parameterType = methodParameter.getGenericParameterType(); if (parameterType.equals(Channel.class) || parameterType.equals(org.springframework.amqp.core.Message.class)) { return false; } if (parameterType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) parameterType; if (parameterizedType.getRawType().equals(Message.class)) { return !(parameterizedType.getActualTypeArguments()[0] instanceof WildcardType); } } return !parameterType.equals(Message.class); // could be Message without a generic type } } }