/*
* 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.integration.util;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.config.BeanExpressionResolver;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.Lifecycle;
import org.springframework.context.expression.StandardBeanExpressionResolver;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.TypeConverter;
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.annotation.Default;
import org.springframework.integration.annotation.Payloads;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.annotation.UseSpelInvoker;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.handler.support.CollectionArgumentResolver;
import org.springframework.integration.handler.support.MapArgumentResolver;
import org.springframework.integration.handler.support.PayloadExpressionArgumentResolver;
import org.springframework.integration.handler.support.PayloadsArgumentResolver;
import org.springframework.integration.support.MutableMessage;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.converter.MessageConversionException;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import org.springframework.messaging.handler.invocation.MethodArgumentResolutionException;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.util.StringUtils;
/**
* A helper class for processors that invoke a method on a target Object using
* a combination of message payload(s) and headers as arguments.
* The Method instance or method name may be provided as a constructor argument.
* If a method name is provided, and more than one declared method has that name,
* the method-selection will be dynamic, based on the underlying SpEL method resolution.
* Alternatively, an annotation type may be provided so that the candidates for SpEL's
* method resolution are determined by the presence of that annotation rather than the method name.
*
* @author Mark Fisher
* @author Oleg Zhurakousky
* @author Dave Syer
* @author Gunnar Hillert
* @author Soby Chacko
* @author Gary Russell
* @author Artem Bilan
*
* @since 2.0
*/
public class MessagingMethodInvokerHelper<T> extends AbstractExpressionEvaluator implements Lifecycle {
private static final String CANDIDATE_METHODS = "CANDIDATE_METHODS";
private static final String CANDIDATE_MESSAGE_METHODS = "CANDIDATE_MESSAGE_METHODS";
private static final Log logger = LogFactory.getLog(MessagingMethodInvokerHelper.class);
// Number of times to try an InvocableHandlerMethod before giving up in favor of an expression.
private static final int FAILED_ATTEMPTS_THRESHOLD = 100;
private static final ExpressionParser EXPRESSION_PARSER_DEFAULT = EXPRESSION_PARSER;
private static final ExpressionParser EXPRESSION_PARSER_OFF = new SpelExpressionParser(
new SpelParserConfiguration(SpelCompilerMode.OFF, null));
private static final ExpressionParser EXPRESSION_PARSER_IMMEDIATE = new SpelExpressionParser(
new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null));
private static final ExpressionParser EXPRESSION_PARSER_MIXED = new SpelExpressionParser(
new SpelParserConfiguration(SpelCompilerMode.MIXED, null));
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER =
new LocalVariableTableParameterNameDiscoverer();
private static final Map<SpelCompilerMode, ExpressionParser> SPEL_COMPILERS = new HashMap<>();
private static final TypeDescriptor messageTypeDescriptor = TypeDescriptor.valueOf(Message.class);
@SuppressWarnings("unused")
private static final Collection<Message<?>> dummyMessages = Collections.emptyList();
private static final TypeDescriptor messageListTypeDescriptor = new TypeDescriptor(
ReflectionUtils.findField(MessagingMethodInvokerHelper.class, "dummyMessages"));
private static final TypeDescriptor messageArrayTypeDescriptor = TypeDescriptor.valueOf(Message[].class);
static {
SPEL_COMPILERS.put(SpelCompilerMode.OFF, EXPRESSION_PARSER_OFF);
SPEL_COMPILERS.put(SpelCompilerMode.IMMEDIATE, EXPRESSION_PARSER_IMMEDIATE);
SPEL_COMPILERS.put(SpelCompilerMode.MIXED, EXPRESSION_PARSER_MIXED);
}
private final DefaultMessageHandlerMethodFactory messageHandlerMethodFactory =
new DefaultMessageHandlerMethodFactory();
private final Object targetObject;
private volatile String displayString;
private volatile boolean requiresReply;
private final Map<Class<?>, HandlerMethod> handlerMethods;
private final Map<Class<?>, HandlerMethod> handlerMessageMethods;
private final List<Map<Class<?>, HandlerMethod>> handlerMethodsList;
private final HandlerMethod handlerMethod;
private final TypeDescriptor expectedType;
private final boolean canProcessMessageList;
private Class<? extends Annotation> annotationType;
private volatile boolean initialized;
private String methodName;
private Method method;
private boolean useSpelInvoker;
private HandlerMethod defaultHandlerMethod;
private BeanExpressionResolver resolver = new StandardBeanExpressionResolver();
private BeanExpressionContext expressionContext;
public MessagingMethodInvokerHelper(Object targetObject, Method method, Class<?> expectedType,
boolean canProcessMessageList) {
this(targetObject, null, method, expectedType, canProcessMessageList);
}
public MessagingMethodInvokerHelper(Object targetObject, Method method, boolean canProcessMessageList) {
this(targetObject, method, null, canProcessMessageList);
}
public MessagingMethodInvokerHelper(Object targetObject, String methodName, Class<?> expectedType,
boolean canProcessMessageList) {
this(targetObject, null, methodName, expectedType, canProcessMessageList);
}
public MessagingMethodInvokerHelper(Object targetObject, String methodName, boolean canProcessMessageList) {
this(targetObject, methodName, null, canProcessMessageList);
}
public MessagingMethodInvokerHelper(Object targetObject, Class<? extends Annotation> annotationType,
boolean canProcessMessageList) {
this(targetObject, annotationType, null, canProcessMessageList);
}
public MessagingMethodInvokerHelper(Object targetObject, Class<? extends Annotation> annotationType,
Class<?> expectedType, boolean canProcessMessageList) {
this(targetObject, annotationType, (String) null, expectedType, canProcessMessageList);
}
/**
* A {@code boolean} flag to use SpEL Expression evaluation or {@link InvocableHandlerMethod}
* for target method invocation.
* @param useSpelInvoker to use SpEL Expression evaluation or not.
* @since 5.0
*/
public void setUseSpelInvoker(boolean useSpelInvoker) {
this.useSpelInvoker = useSpelInvoker;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) {
super.setBeanFactory(beanFactory);
this.messageHandlerMethodFactory.setBeanFactory(beanFactory);
if (beanFactory instanceof ConfigurableListableBeanFactory) {
BeanExpressionResolver beanExpressionResolver = ((ConfigurableListableBeanFactory) beanFactory)
.getBeanExpressionResolver();
if (beanExpressionResolver != null) {
this.resolver = beanExpressionResolver;
}
this.expressionContext = new BeanExpressionContext((ConfigurableListableBeanFactory) beanFactory, null);
}
}
@Override
public void setConversionService(ConversionService conversionService) {
super.setConversionService(conversionService);
if (conversionService != null) {
this.messageHandlerMethodFactory.setConversionService(conversionService);
}
}
public T process(Message<?> message) throws Exception {
ParametersWrapper parameters = new ParametersWrapper(message);
return processInternal(parameters);
}
public T process(Collection<Message<?>> messages, Map<String, Object> headers) throws Exception {
ParametersWrapper parameters = new ParametersWrapper(messages, headers);
return processInternal(parameters);
}
@Override
public String toString() {
return this.displayString;
}
@Override
public void start() {
if (this.targetObject instanceof Lifecycle) {
((Lifecycle) this.targetObject).start();
}
}
@Override
public void stop() {
if (this.targetObject instanceof Lifecycle) {
((Lifecycle) this.targetObject).stop();
}
}
@Override
public boolean isRunning() {
return !(this.targetObject instanceof Lifecycle) || ((Lifecycle) this.targetObject).isRunning();
}
/*
* Private constructors for internal use
*/
private MessagingMethodInvokerHelper(Object targetObject, Class<? extends Annotation> annotationType,
Method method, Class<?> expectedType, boolean canProcessMessageList) {
this.annotationType = annotationType;
this.canProcessMessageList = canProcessMessageList;
Assert.notNull(method, "method must not be null");
this.method = method;
this.requiresReply = expectedType != null;
if (expectedType != null) {
Assert.isTrue(method.getReturnType() != Void.class && method.getReturnType() != Void.TYPE,
"method must have a return type");
this.expectedType = TypeDescriptor.valueOf(expectedType);
}
else {
this.expectedType = null;
}
Assert.notNull(targetObject, "targetObject must not be null");
this.targetObject = targetObject;
try {
InvocableHandlerMethod invocableHandlerMethod =
this.messageHandlerMethodFactory.createInvocableHandlerMethod(targetObject, method);
this.handlerMethod = new HandlerMethod(invocableHandlerMethod, canProcessMessageList);
this.defaultHandlerMethod = null;
checkSpelInvokerRequired(getTargetClass(targetObject), method, this.handlerMethod);
}
catch (IneligibleMethodException e) {
throw new IllegalArgumentException(e);
}
this.handlerMethods = null;
this.handlerMessageMethods = null;
this.handlerMethodsList = null;
this.setDisplayString(targetObject, method);
}
private MessagingMethodInvokerHelper(Object targetObject, Class<? extends Annotation> annotationType,
String methodName, Class<?> expectedType, boolean canProcessMessageList) {
this.annotationType = annotationType;
this.methodName = methodName;
this.canProcessMessageList = canProcessMessageList;
Assert.notNull(targetObject, "targetObject must not be null");
if (expectedType != null) {
this.expectedType = TypeDescriptor.valueOf(expectedType);
}
else {
this.expectedType = null;
}
this.targetObject = targetObject;
Map<String, Map<Class<?>, HandlerMethod>> handlerMethodsForTarget =
findHandlerMethodsForTarget(targetObject, annotationType, methodName, expectedType != null);
Map<Class<?>, HandlerMethod> handlerMethods = handlerMethodsForTarget.get(CANDIDATE_METHODS);
Map<Class<?>, HandlerMethod> handlerMessageMethods = handlerMethodsForTarget.get(CANDIDATE_MESSAGE_METHODS);
if ((handlerMethods.size() == 1 && handlerMessageMethods.isEmpty()) ||
(handlerMessageMethods.size() == 1 && handlerMethods.isEmpty())) {
if (handlerMethods.size() == 1) {
this.handlerMethod = handlerMethods.values().iterator().next();
}
else {
this.handlerMethod = handlerMessageMethods.values().iterator().next();
}
this.handlerMethods = null;
this.handlerMessageMethods = null;
this.handlerMethodsList = null;
}
else {
this.handlerMethod = null;
this.handlerMethods = handlerMethods;
this.handlerMessageMethods = handlerMessageMethods;
this.handlerMethodsList = new LinkedList<>();
//TODO Consider to use global option to determine a precedence of methods
this.handlerMethodsList.add(this.handlerMethods);
this.handlerMethodsList.add(this.handlerMessageMethods);
}
setDisplayString(targetObject, methodName);
}
private void setDisplayString(Object targetObject, Object targetMethod) {
StringBuilder sb = new StringBuilder(targetObject.getClass().getName());
if (targetMethod instanceof Method) {
sb.append(".")
.append(((Method) targetMethod).getName());
}
else if (targetMethod instanceof String) {
sb.append(".")
.append(targetMethod);
}
this.displayString = sb.toString() + "]";
}
private void prepareEvaluationContext() throws Exception {
StandardEvaluationContext context = getEvaluationContext(false);
Class<?> targetType = AopUtils.getTargetClass(this.targetObject);
if (this.method != null) {
context.registerMethodFilter(targetType, new FixedMethodFilter(this.method));
if (this.expectedType != null) {
Assert.state(context.getTypeConverter()
.canConvert(TypeDescriptor.valueOf((this.method).getReturnType()), this.expectedType),
"Cannot convert to expected type (" + this.expectedType + ") from " + this.method);
}
}
else {
AnnotatedMethodFilter filter = new AnnotatedMethodFilter(this.annotationType, this.methodName,
this.requiresReply);
Assert.state(canReturnExpectedType(filter, targetType, context.getTypeConverter()),
"Cannot convert to expected type (" + this.expectedType + ") from " + this.method);
context.registerMethodFilter(targetType, filter);
}
context.setVariable("target", this.targetObject);
context.registerFunction("requiredHeader", ParametersWrapper.class.getDeclaredMethod("getHeader",
Map.class, String.class));
}
private boolean canReturnExpectedType(AnnotatedMethodFilter filter, Class<?> targetType,
TypeConverter typeConverter) {
if (this.expectedType == null) {
return true;
}
List<Method> methods = filter.filter(Arrays.asList(ReflectionUtils.getAllDeclaredMethods(targetType)));
for (Method method : methods) {
if (typeConverter.canConvert(TypeDescriptor.valueOf(method.getReturnType()), this.expectedType)) {
return true;
}
}
return false;
}
@SuppressWarnings("unchecked")
private T processInternal(ParametersWrapper parameters) throws Exception {
if (!this.initialized) {
initialize();
}
HandlerMethod candidate = this.findHandlerMethodForParameters(parameters);
if (candidate == null) {
candidate = this.defaultHandlerMethod;
}
Assert.notNull(candidate, "No candidate methods found for messages.");
if (!candidate.initialized) {
initializeHandler(candidate);
}
Expression expression = candidate.expression;
T result;
if (this.useSpelInvoker || candidate.spelOnly) {
result = invokeExpression(expression, parameters);
}
else {
result = invokeHandlerMethod(candidate, parameters);
}
if (result != null && this.expectedType != null) {
return (T) getEvaluationContext(true)
.getTypeConverter()
.convertValue(result, TypeDescriptor.forObject(result), this.expectedType);
}
else {
return result;
}
}
private void initializeHandler(HandlerMethod candidate) {
ExpressionParser parser;
if (candidate.useSpelInvoker == null) {
parser = EXPRESSION_PARSER_DEFAULT;
}
else {
String compilerMode = resolveExpression(candidate.useSpelInvoker.compilerMode(),
"UseSpelInvoker.compilerMode:").toUpperCase();
parser = !StringUtils.hasText(compilerMode)
? EXPRESSION_PARSER_DEFAULT
: SPEL_COMPILERS.get(SpelCompilerMode.valueOf(compilerMode));
}
candidate.expression = parser.parseExpression(candidate.expressionString);
candidate.initialized = true;
}
private synchronized void initialize() throws Exception {
if (!this.initialized) {
PayloadExpressionArgumentResolver payloadExpressionArgumentResolver =
new PayloadExpressionArgumentResolver();
payloadExpressionArgumentResolver.setBeanFactory(getBeanFactory());
PayloadsArgumentResolver payloadsArgumentResolver = new PayloadsArgumentResolver();
payloadsArgumentResolver.setBeanFactory(getBeanFactory());
CollectionArgumentResolver collectionArgumentResolver =
new CollectionArgumentResolver(this.canProcessMessageList);
collectionArgumentResolver.setBeanFactory(getBeanFactory());
MapArgumentResolver mapArgumentResolver = new MapArgumentResolver();
mapArgumentResolver.setBeanFactory(getBeanFactory());
List<HandlerMethodArgumentResolver> customArgumentResolvers = new LinkedList<>();
customArgumentResolvers.add(payloadExpressionArgumentResolver);
customArgumentResolvers.add(payloadsArgumentResolver);
customArgumentResolvers.add(collectionArgumentResolver);
customArgumentResolvers.add(mapArgumentResolver);
this.messageHandlerMethodFactory.setCustomArgumentResolvers(customArgumentResolvers);
if (getBeanFactory() != null &&
getBeanFactory()
.containsBean(IntegrationContextUtils.ARGUMENT_RESOLVER_MESSAGE_CONVERTER_BEAN_NAME)) {
this.messageHandlerMethodFactory
.setMessageConverter(getBeanFactory()
.getBean(IntegrationContextUtils.ARGUMENT_RESOLVER_MESSAGE_CONVERTER_BEAN_NAME,
MessageConverter.class));
}
this.messageHandlerMethodFactory.afterPropertiesSet();
prepareEvaluationContext();
this.initialized = true;
}
}
@SuppressWarnings("unchecked")
private T invokeHandlerMethod(HandlerMethod handlerMethod, ParametersWrapper parameters) throws Exception {
try {
return (T) handlerMethod.invoke(parameters);
}
catch (MethodArgumentResolutionException | MessageConversionException | IllegalStateException e) {
if (e instanceof MessageConversionException) {
if (e.getCause() instanceof ConversionFailedException &&
!(e.getCause().getCause() instanceof ConverterNotFoundException)) {
throw e;
}
}
else if (e instanceof IllegalStateException) {
if (!(e.getCause() instanceof IllegalArgumentException) ||
!e.getStackTrace()[0].getClassName().equals(InvocableHandlerMethod.class.getName()) ||
(!"argument type mismatch".equals(e.getCause().getMessage()) &&
// JVM generates GeneratedMethodAccessor### after several calls with less error checking
!e.getCause().getMessage().startsWith("java.lang.ClassCastException@"))) {
throw e;
}
}
Expression expression = handlerMethod.expression;
if (++handlerMethod.failedAttempts >= FAILED_ATTEMPTS_THRESHOLD) {
handlerMethod.spelOnly = true;
if (logger.isInfoEnabled()) {
logger.info("Failed to invoke [ " + handlerMethod.invocableHandlerMethod +
"] with provided arguments [ " + parameters + " ]. \n" +
"Falling back to SpEL invocation for expression [ " +
expression.getExpressionString() + " ]");
}
}
return invokeExpression(expression, parameters);
}
}
@SuppressWarnings("unchecked")
private T invokeExpression(Expression expression, ParametersWrapper parameters) throws Exception {
try {
return (T) evaluateExpression(expression, parameters);
}
catch (Exception e) {
Throwable evaluationException = e;
if ((e instanceof EvaluationException || e instanceof MessageHandlingException)
&& e.getCause() != null) {
evaluationException = e.getCause();
}
if (evaluationException instanceof Exception) {
throw (Exception) evaluationException;
}
else {
throw new IllegalStateException("Cannot process message", evaluationException);
}
}
}
private Map<String, Map<Class<?>, HandlerMethod>> findHandlerMethodsForTarget(final Object targetObject,
final Class<? extends Annotation> annotationType, final String methodName, final boolean requiresReply) {
Map<String, Map<Class<?>, HandlerMethod>> handlerMethods = new HashMap<>();
final Map<Class<?>, HandlerMethod> candidateMethods = new HashMap<>();
final Map<Class<?>, HandlerMethod> candidateMessageMethods = new HashMap<>();
final Map<Class<?>, HandlerMethod> fallbackMethods = new HashMap<>();
final Map<Class<?>, HandlerMethod> fallbackMessageMethods = new HashMap<>();
final AtomicReference<Class<?>> ambiguousFallbackType = new AtomicReference<>();
final AtomicReference<Class<?>> ambiguousFallbackMessageGenericType = new AtomicReference<>();
final Class<?> targetClass = getTargetClass(targetObject);
MethodFilter methodFilter = new UniqueMethodFilter(targetClass);
ReflectionUtils.doWithMethods(targetClass, method1 -> {
boolean matchesAnnotation = false;
if (method1.isBridge()) {
return;
}
if (isMethodDefinedOnObjectClass(method1)) {
return;
}
if (method1.getDeclaringClass().equals(Proxy.class)) {
return;
}
if (annotationType != null && AnnotationUtils.findAnnotation(method1, annotationType) != null) {
matchesAnnotation = true;
}
else if (!Modifier.isPublic(method1.getModifiers())) {
return;
}
if (requiresReply && void.class.equals(method1.getReturnType())) {
return;
}
if (methodName != null && !methodName.equals(method1.getName())) {
return;
}
if (methodName == null
&& ObjectUtils.containsElement(new String[] { "start", "stop", "isRunning" }, method1.getName())) {
return;
}
HandlerMethod handlerMethod1;
try {
method1 = org.springframework.util.ClassUtils.getMostSpecificMethod(method1, targetObject.getClass());
InvocableHandlerMethod invocableHandlerMethod =
this.messageHandlerMethodFactory.createInvocableHandlerMethod(targetObject, method1);
handlerMethod1 = new HandlerMethod(invocableHandlerMethod, this.canProcessMessageList);
checkSpelInvokerRequired(targetClass, method1, handlerMethod1);
}
catch (IneligibleMethodException e) {
if (logger.isDebugEnabled()) {
logger.debug("Method [" + method1 + "] is not eligible for Message handling "
+ e.getMessage() + ".");
}
return;
}
catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.debug("Method [" + method1 + "] is not eligible for Message handling.", e);
}
return;
}
if (AnnotationUtils.getAnnotation(method1, Default.class) != null) {
Assert.state(this.defaultHandlerMethod == null,
() -> "Only one method can be @Default, but there are more for: " + targetObject);
this.defaultHandlerMethod = handlerMethod1;
}
Class<?> targetParameterType = handlerMethod1.getTargetParameterType();
if (matchesAnnotation || annotationType == null) {
if (handlerMethod1.isMessageMethod()) {
if (candidateMessageMethods.containsKey(targetParameterType)) {
throw new IllegalArgumentException("Found more than one method match for type " +
"[Message<" + targetParameterType + ">]");
}
candidateMessageMethods.put(targetParameterType, handlerMethod1);
}
else {
if (candidateMethods.containsKey(targetParameterType)) {
String exceptionMessage = "Found more than one method match for ";
if (Void.class.equals(targetParameterType)) {
exceptionMessage += "empty parameter for 'payload'";
}
else {
exceptionMessage += "type [" + targetParameterType + "]";
}
throw new IllegalArgumentException(exceptionMessage);
}
candidateMethods.put(targetParameterType, handlerMethod1);
}
}
else {
if (handlerMethod1.isMessageMethod()) {
if (fallbackMessageMethods.containsKey(targetParameterType)) {
// we need to check for duplicate type matches,
// but only if we end up falling back
// and we'll only keep track of the first one
ambiguousFallbackMessageGenericType.compareAndSet(null, targetParameterType);
}
fallbackMessageMethods.put(targetParameterType, handlerMethod1);
}
else {
if (fallbackMethods.containsKey(targetParameterType)) {
// we need to check for duplicate type matches,
// but only if we end up falling back
// and we'll only keep track of the first one
ambiguousFallbackType.compareAndSet(null, targetParameterType);
}
fallbackMethods.put(targetParameterType, handlerMethod1);
}
}
}, methodFilter);
if (candidateMethods.isEmpty() && candidateMessageMethods.isEmpty() && fallbackMethods.isEmpty()
&& fallbackMessageMethods.isEmpty()) {
findSingleSpecifMethodOnInterfacesIfProxy(targetObject, methodName, candidateMessageMethods,
candidateMethods);
}
if (!candidateMethods.isEmpty() || !candidateMessageMethods.isEmpty()) {
handlerMethods.put(CANDIDATE_METHODS, candidateMethods);
handlerMethods.put(CANDIDATE_MESSAGE_METHODS, candidateMessageMethods);
return handlerMethods;
}
if ((ambiguousFallbackType.get() != null
|| ambiguousFallbackMessageGenericType.get() != null)
&& ServiceActivator.class.equals(annotationType)) {
/*
* When there are ambiguous fallback methods,
* a Service Activator can finally fallback to RequestReplyExchanger.exchange(m).
* Ambiguous means > 1 method that takes the same payload type, or > 1 method
* that takes a Message with the same generic type.
*/
List<Method> frameworkMethods = new ArrayList<>();
Class<?>[] allInterfaces = org.springframework.util.ClassUtils.getAllInterfacesForClass(targetClass);
for (Class<?> iface : allInterfaces) {
try {
if ("org.springframework.integration.gateway.RequestReplyExchanger".equals(iface.getName())) {
frameworkMethods.add(targetClass.getMethod("exchange", Message.class));
if (logger.isDebugEnabled()) {
logger.debug(targetObject.getClass() +
": Ambiguous fallback methods; using RequestReplyExchanger.exchange()");
}
}
}
catch (Exception e) {
// should never happen (but would fall through to errors below)
}
}
if (frameworkMethods.size() == 1) {
Method method = org.springframework.util.ClassUtils.getMostSpecificMethod(frameworkMethods.get(0),
targetObject.getClass());
InvocableHandlerMethod invocableHandlerMethod =
this.messageHandlerMethodFactory.createInvocableHandlerMethod(targetObject,
method);
HandlerMethod handlerMethod = new HandlerMethod(invocableHandlerMethod, this.canProcessMessageList);
checkSpelInvokerRequired(targetClass, method, handlerMethod);
handlerMethods.put(CANDIDATE_METHODS, Collections.singletonMap(Object.class, handlerMethod));
handlerMethods.put(CANDIDATE_MESSAGE_METHODS, candidateMessageMethods);
return handlerMethods;
}
}
Assert.state(!fallbackMethods.isEmpty() || !fallbackMessageMethods.isEmpty(),
"Target object of type [" + this.targetObject.getClass() +
"] has no eligible methods for handling Messages.");
Assert.isNull(ambiguousFallbackType.get(), "Found ambiguous parameter type [" + ambiguousFallbackType
+ "] for method match: " + fallbackMethods.values());
Assert.isNull(ambiguousFallbackMessageGenericType.get(),
"Found ambiguous parameter type ["
+ ambiguousFallbackMessageGenericType
+ "] for method match: "
+ fallbackMethods.values());
handlerMethods.put(CANDIDATE_METHODS, fallbackMethods);
handlerMethods.put(CANDIDATE_MESSAGE_METHODS, fallbackMessageMethods);
return handlerMethods;
}
private void findSingleSpecifMethodOnInterfacesIfProxy(final Object targetObject, final String methodName,
Map<Class<?>, HandlerMethod> candidateMessageMethods,
Map<Class<?>, HandlerMethod> candidateMethods) {
if (AopUtils.isAopProxy(targetObject)) {
final AtomicReference<Method> targetMethod = new AtomicReference<>();
final AtomicReference<Class<?>> targetClass = new AtomicReference<>();
Class<?>[] interfaces = ((Advised) targetObject).getProxiedInterfaces();
for (Class<?> clazz : interfaces) {
ReflectionUtils.doWithMethods(clazz, method1 -> {
if (targetMethod.get() != null) {
throw new IllegalStateException("Ambiguous method " + methodName + " on " + targetObject);
}
else {
targetMethod.set(method1);
targetClass.set(clazz);
}
}, method12 -> method12.getName().equals(methodName));
}
Method method = targetMethod.get();
if (method != null) {
method = org.springframework.util.ClassUtils.getMostSpecificMethod(method, targetObject.getClass());
InvocableHandlerMethod invocableHandlerMethod =
this.messageHandlerMethodFactory.createInvocableHandlerMethod(targetObject, method);
HandlerMethod handlerMethod = new HandlerMethod(invocableHandlerMethod, this.canProcessMessageList);
checkSpelInvokerRequired(targetClass.get(), method, handlerMethod);
Class<?> targetParameterType = handlerMethod.getTargetParameterType();
if (handlerMethod.isMessageMethod()) {
if (candidateMessageMethods.containsKey(targetParameterType)) {
throw new IllegalArgumentException("Found more than one method match for type " +
"[Message<" + targetParameterType + ">]");
}
candidateMessageMethods.put(targetParameterType, handlerMethod);
}
else {
if (candidateMethods.containsKey(targetParameterType)) {
String exceptionMessage = "Found more than one method match for ";
if (Void.class.equals(targetParameterType)) {
exceptionMessage += "empty parameter for 'payload'";
}
else {
exceptionMessage += "type [" + targetParameterType + "]";
}
throw new IllegalArgumentException(exceptionMessage);
}
candidateMethods.put(targetParameterType, handlerMethod);
}
}
}
}
private void checkSpelInvokerRequired(final Class<?> targetClass, Method methodArg, HandlerMethod handlerMethod) {
Method method = AopUtils.getMostSpecificMethod(methodArg, targetClass);
UseSpelInvoker useSpel = AnnotationUtils.findAnnotation(method, UseSpelInvoker.class);
if (useSpel == null) {
useSpel = AnnotationUtils.findAnnotation(targetClass, UseSpelInvoker.class);
}
if (useSpel != null) {
handlerMethod.spelOnly = true;
handlerMethod.useSpelInvoker = useSpel;
}
}
private String resolveExpression(String value, String msg) {
String resolvedValue = resolve(value);
if (!(resolvedValue.startsWith("#{") && value.endsWith("}"))) {
return resolvedValue;
}
Object evaluated = this.resolver.evaluate(resolvedValue, this.expressionContext);
Assert.isInstanceOf(String.class, evaluated, msg);
return (String) evaluated;
}
private String resolve(String value) {
if (getBeanFactory() != null && getBeanFactory() instanceof ConfigurableBeanFactory) {
return ((ConfigurableBeanFactory) getBeanFactory()).resolveEmbeddedValue(value);
}
return value;
}
private Class<?> getTargetClass(Object targetObject) {
Class<?> targetClass = targetObject.getClass();
if (AopUtils.isAopProxy(targetObject)) {
targetClass = AopUtils.getTargetClass(targetObject);
if (targetClass == targetObject.getClass()) {
try {
// Maybe a proxy with no target - e.g. gateway
Class<?>[] interfaces = ((Advised) targetObject).getProxiedInterfaces();
if (interfaces != null && interfaces.length == 1) {
targetClass = interfaces[0];
}
}
catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.debug("Exception trying to extract interface", e);
}
}
}
}
else if (org.springframework.util.ClassUtils.isCglibProxyClass(targetClass)
|| targetClass.getSimpleName().contains("$MockitoMock$")) {
Class<?> superClass = targetObject.getClass().getSuperclass();
if (!Object.class.equals(superClass)) {
targetClass = superClass;
}
}
return targetClass;
}
private HandlerMethod findHandlerMethodForParameters(ParametersWrapper parameters) {
if (this.handlerMethod != null) {
return this.handlerMethod;
}
final Class<?> payloadType = parameters.getFirstParameterType();
HandlerMethod closestMatch = this.findClosestMatch(payloadType);
if (closestMatch != null) {
return closestMatch;
}
if (Iterable.class.isAssignableFrom(payloadType) && this.handlerMethods.containsKey(Iterator.class)) {
return this.handlerMethods.get(Iterator.class);
}
else {
return this.handlerMethods.get(Void.class);
}
}
private HandlerMethod findClosestMatch(Class<?> payloadType) {
for (Map<Class<?>, HandlerMethod> handlerMethods : this.handlerMethodsList) {
Set<Class<?>> candidates = handlerMethods.keySet();
Class<?> match = null;
if (!CollectionUtils.isEmpty(candidates)) {
match = ClassUtils.findClosestMatch(payloadType, candidates, true);
}
if (match != null) {
return handlerMethods.get(match);
}
}
return null;
}
private static boolean isMethodDefinedOnObjectClass(Method method) {
return method != null &&
(method.getDeclaringClass().equals(Object.class) || ReflectionUtils.isEqualsMethod(method) ||
ReflectionUtils.isHashCodeMethod(method) || ReflectionUtils.isToStringMethod(method) ||
AopUtils.isFinalizeMethod(method) || (method.getName().equals("clone")
&& method.getParameterTypes().length == 0));
}
/**
* Helper class for generating and exposing metadata for a candidate handler method. The metadata includes the SpEL
* expression and the expected payload type.
*/
private static class HandlerMethod {
private final String expressionString;
private final InvocableHandlerMethod invocableHandlerMethod;
private final boolean canProcessMessageList;
private volatile Expression expression;
private volatile TypeDescriptor targetParameterTypeDescriptor;
private volatile Class<?> targetParameterType = Void.class;
private volatile boolean messageMethod;
private volatile boolean spelOnly;
private volatile UseSpelInvoker useSpelInvoker;
private volatile boolean initialized;
// The number of times InvocableHandlerMethod was attempted and failed - enables us to eventually
// give up trying to call it when it just doesn't seem to be possible.
// Switching to spelOnly afterwards forever.
private volatile int failedAttempts = 0;
HandlerMethod(InvocableHandlerMethod invocableHandlerMethod, boolean canProcessMessageList) {
this.invocableHandlerMethod = invocableHandlerMethod;
this.canProcessMessageList = canProcessMessageList;
this.expressionString = generateExpression(this.invocableHandlerMethod.getMethod());
}
@SuppressWarnings("unchecked")
public <T> T invoke(ParametersWrapper parameters) throws Exception {
Message<?> message = parameters.getMessage();
if (this.canProcessMessageList) {
message = new MutableMessage<>(parameters.getMessages(), parameters.getHeaders());
}
return (T) this.invocableHandlerMethod.invoke(message);
}
Class<?> getTargetParameterType() {
return this.targetParameterType;
}
private boolean isMessageMethod() {
return this.messageMethod;
}
@Override
public String toString() {
return this.invocableHandlerMethod.toString();
}
private String generateExpression(Method method) {
StringBuilder sb = new StringBuilder("#target." + method.getName() + "(");
Class<?>[] parameterTypes = method.getParameterTypes();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
boolean hasUnqualifiedMapParameter = false;
for (int i = 0; i < parameterTypes.length; i++) {
if (i != 0) {
sb.append(", ");
}
MethodParameter methodParameter = new MethodParameter(method, i);
TypeDescriptor parameterTypeDescriptor = new TypeDescriptor(methodParameter);
Class<?> parameterType = parameterTypeDescriptor.getObjectType();
Annotation mappingAnnotation =
MessagingAnnotationUtils.findMessagePartAnnotation(parameterAnnotations[i], true);
if (mappingAnnotation != null) {
Class<? extends Annotation> annotationType = mappingAnnotation.annotationType();
if (annotationType.equals(Payload.class)) {
sb.append("payload");
String qualifierExpression = (String) AnnotationUtils.getValue(mappingAnnotation);
if (StringUtils.hasText(qualifierExpression)) {
sb.append(".")
.append(qualifierExpression);
}
if (!StringUtils.hasText(qualifierExpression)) {
this.setExclusiveTargetParameterType(parameterTypeDescriptor, methodParameter);
}
}
if (annotationType.equals(Payloads.class)) {
Assert.isTrue(this.canProcessMessageList,
"The @Payloads annotation can only be applied if method handler canProcessMessageList.");
Assert.isTrue(Collection.class.isAssignableFrom(parameterType),
"The @Payloads annotation can only be applied to a Collection-typed parameter.");
sb.append("messages.![payload");
String qualifierExpression = ((Payloads) mappingAnnotation).value();
if (StringUtils.hasText(qualifierExpression)) {
sb.append(".")
.append(qualifierExpression);
}
sb.append("]");
if (!StringUtils.hasText(qualifierExpression)) {
this.setExclusiveTargetParameterType(parameterTypeDescriptor, methodParameter);
}
}
else if (annotationType.equals(Headers.class)) {
Assert.isTrue(Map.class.isAssignableFrom(parameterType),
"The @Headers annotation can only be applied to a Map-typed parameter.");
sb.append("headers");
}
else if (annotationType.equals(Header.class)) {
sb.append(this.determineHeaderExpression(mappingAnnotation, methodParameter));
}
}
else if (parameterTypeDescriptor.isAssignableTo(messageTypeDescriptor)) {
this.messageMethod = true;
sb.append("message");
this.setExclusiveTargetParameterType(parameterTypeDescriptor, methodParameter);
}
else if (this.canProcessMessageList &&
(parameterTypeDescriptor.isAssignableTo(messageListTypeDescriptor)
|| parameterTypeDescriptor.isAssignableTo(messageArrayTypeDescriptor))) {
sb.append("messages");
this.setExclusiveTargetParameterType(parameterTypeDescriptor, methodParameter);
}
else if (Collection.class.isAssignableFrom(parameterType) || parameterType.isArray()) {
if (this.canProcessMessageList) {
sb.append("messages.![payload]");
}
else {
sb.append("payload");
}
this.setExclusiveTargetParameterType(parameterTypeDescriptor, methodParameter);
}
else if (Iterator.class.isAssignableFrom(parameterType)) {
if (this.canProcessMessageList) {
Type type = method.getGenericParameterTypes()[i];
Type parameterizedType = null;
if (type instanceof ParameterizedType) {
parameterizedType = ((ParameterizedType) type).getActualTypeArguments()[0];
if (parameterizedType instanceof ParameterizedType) {
parameterizedType = ((ParameterizedType) parameterizedType).getRawType();
}
}
if (parameterizedType != null && Message.class.isAssignableFrom((Class<?>) parameterizedType)) {
sb.append("messages.iterator()");
}
else {
sb.append("messages.![payload].iterator()");
}
}
else {
sb.append("payload.iterator()");
}
this.setExclusiveTargetParameterType(parameterTypeDescriptor, methodParameter);
}
else if (Map.class.isAssignableFrom(parameterType)) {
if (Properties.class.isAssignableFrom(parameterType)) {
sb.append("payload instanceof T(java.util.Map) or "
+ "(payload instanceof T(String) and payload.contains('=')) ? payload : headers");
}
else {
sb.append("(payload instanceof T(java.util.Map) ? payload : headers)");
}
Assert.isTrue(!hasUnqualifiedMapParameter,
"Found more than one Map typed parameter without any qualification. "
+ "Consider using @Payload or @Headers on at least one of the parameters.");
hasUnqualifiedMapParameter = true;
}
else {
sb.append("payload");
this.setExclusiveTargetParameterType(parameterTypeDescriptor, methodParameter);
}
}
if (hasUnqualifiedMapParameter) {
if (this.targetParameterType != null && Map.class.isAssignableFrom(this.targetParameterType)) {
throw new IllegalArgumentException(
"Unable to determine payload matching parameter due to ambiguous Map typed parameters. "
+ "Consider adding the @Payload and or @Headers annotations as appropriate.");
}
}
sb.append(")");
if (this.targetParameterTypeDescriptor == null) {
this.targetParameterTypeDescriptor = TypeDescriptor.valueOf(Void.class);
}
return sb.toString();
}
private String determineHeaderExpression(Annotation headerAnnotation, MethodParameter methodParameter) {
methodParameter.initParameterNameDiscovery(PARAMETER_NAME_DISCOVERER);
String headerName = null;
String relativeExpression = "";
AnnotationAttributes annotationAttributes =
(AnnotationAttributes) AnnotationUtils.getAnnotationAttributes(headerAnnotation);
String valueAttribute = annotationAttributes.getString(AnnotationUtils.VALUE);
if (!StringUtils.hasText(valueAttribute)) {
headerName = methodParameter.getParameterName();
}
else if (valueAttribute.indexOf('.') != -1) {
String[] tokens = valueAttribute.split("\\.", 2);
headerName = tokens[0];
if (StringUtils.hasText(tokens[1])) {
relativeExpression = "." + tokens[1];
this.spelOnly = true;
}
}
else {
headerName = valueAttribute;
}
Assert.notNull(headerName, "Cannot determine header name. Possible reasons: -debug is "
+ "disabled or header name is not explicitly provided via @Header annotation.");
String headerRetrievalExpression = "headers['" + headerName + "']";
String fullHeaderExpression = headerRetrievalExpression + relativeExpression;
if (annotationAttributes.getBoolean("required")
&& !methodParameter.getParameterType().equals(Optional.class)) {
return "#requiredHeader(headers, '" + headerName + "')" + relativeExpression;
}
else if (!StringUtils.hasLength(relativeExpression)) {
return headerRetrievalExpression + " ?: null";
}
else {
return headerRetrievalExpression + " != null ? " + fullHeaderExpression + " : null";
}
}
private void setExclusiveTargetParameterType(TypeDescriptor targetParameterType,
MethodParameter methodParameter) {
if (this.targetParameterTypeDescriptor != null) {
throw new IneligibleMethodException("Found more than one parameter type candidate: [" +
this.targetParameterTypeDescriptor + "] and [" + targetParameterType + "]");
}
this.targetParameterTypeDescriptor = targetParameterType;
if (Message.class.isAssignableFrom(targetParameterType.getObjectType())) {
methodParameter.increaseNestingLevel();
this.targetParameterType = methodParameter.getNestedParameterType();
methodParameter.decreaseNestingLevel();
}
else {
this.targetParameterType = targetParameterType.getObjectType();
}
}
}
public static class ParametersWrapper {
private final Object payload;
private final Collection<Message<?>> messages;
private final Map<String, Object> headers;
private final Message<?> message;
ParametersWrapper(Message<?> message) {
this.message = message;
this.payload = message.getPayload();
this.headers = message.getHeaders();
this.messages = null;
}
ParametersWrapper(Collection<Message<?>> messages, Map<String, Object> headers) {
this.payload = null;
this.messages = messages;
this.headers = headers;
this.message = null;
}
/**
* SpEL Function to retrieve a required header.
* @param headers the headers.
* @param header the header name
* @return the header
* @throws IllegalArgumentException if the header does not exist
*/
public static Object getHeader(Map<?, ?> headers, String header) {
Object object = headers.get(header);
if (object == null) {
throw new IllegalArgumentException("required header not available: " + header);
}
return object;
}
public Object getPayload() {
Assert.state(this.payload != null,
"Invalid method parameter for payload: was expecting collection.");
return this.payload;
}
public Collection<Message<?>> getMessages() {
Assert.state(this.messages != null,
"Invalid method parameter for messages: was expecting a single payload.");
return this.messages;
}
public Map<String, Object> getHeaders() {
return this.headers;
}
public Message<?> getMessage() {
return this.message;
}
public Class<?> getFirstParameterType() {
if (this.payload != null) {
return this.payload.getClass();
}
return this.messages.getClass();
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("ParametersWrapper{");
if (this.messages != null) {
sb.append("messages=").append(this.messages)
.append(", headers=").append(this.headers);
}
else {
sb.append("message=").append(this.message);
}
return sb.append('}')
.toString();
}
}
@SuppressWarnings("serial")
private static final class IneligibleMethodException extends RuntimeException {
IneligibleMethodException(String message) {
super(message);
}
}
}