Miscellaneous improvement for end-user experience (#3500)

* Miscellaneous improvement for end-user experience

Related to https://stackoverflow.com/questions/66428239/ineligiblemethodexception-found-more-than-one-parameter-type-candidate-spring
Related to https://stackoverflow.com/questions/66397350/spring-integration-how-do-i-call-the-spring-integration-from-spring-controller

The exception thrown from gateway method call for `null` arg is not clear that it cannot be `null`
* Improve `GatewayMethodInboundMessageMapper.mapArgumentsToMessage()` exception message to show args
and explain that it cannot be resolved to `null`

When more than one params are candidates for method invocation, the `MessagingMethodInvokerHelper`
throws an `IneligibleMethodException` which outcome is not clear what to do
* Add `@Payload` annotation recommendation into an exception message to make the reason more cleaner
* Some other code style clean up in the `MessagingMethodInvokerHelper`

* * Fix `PseudoTransactionalMessageSourceTests.testCommit()` for better coverage

* Apply suggestions from code review

Co-authored-by: Gary Russell <grussell@vmware.com>

Co-authored-by: Gary Russell <grussell@vmware.com>
This commit is contained in:
Artem Bilan
2021-03-03 13:48:32 -05:00
committed by GitHub
parent cb74a9494c
commit 9d8393eb3e
3 changed files with 47 additions and 65 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@@ -197,7 +197,7 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]
try {
return this.argsMapper.toMessage(new MethodArgsHolder(this.method, arguments), headers);
}
catch (MessagingException e) { // NOSONAR fto avoid if..else
catch (MessagingException e) { // NOSONAR to avoid if..else
throw e;
}
catch (Exception e) {
@@ -334,8 +334,9 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]
}
Assert.isTrue(messageOrPayload != null,
() -> "unable to determine a Message or payload parameter on method ["
+ GatewayMethodInboundMessageMapper.this.method + "]");
() -> "The 'payload' (or 'Message') for gateway [" + GatewayMethodInboundMessageMapper.this.method +
"] method call cannot be determined (must not be 'null') from the provided arguments: " +
Arrays.toString(arguments));
populateSendAndReplyTimeoutHeaders(methodInvocationEvaluationContext, holder, headersToPopulate);
return buildMessage(holder, headersToPopulate, messageOrPayload, methodInvocationEvaluationContext);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@@ -37,7 +37,6 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.Advised;
@@ -58,6 +57,7 @@ import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.log.LogAccessor;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
@@ -127,7 +127,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator im
private static final String CANDIDATE_MESSAGE_METHODS = "CANDIDATE_MESSAGE_METHODS";
private static final Log LOGGER = LogFactory.getLog(MessagingMethodInvokerHelper.class);
private static final LogAccessor LOGGER = new LogAccessor(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;
@@ -186,20 +186,20 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator im
private final HandlerMethod handlerMethod;
private final String displayString;
private final boolean requiresReply;
private HandlerMethod defaultHandlerMethod;
private BeanExpressionResolver resolver = new StandardBeanExpressionResolver();
private BeanExpressionContext expressionContext;
private volatile String displayString;
private volatile boolean requiresReply;
private boolean useSpelInvoker;
private volatile boolean initialized;
private boolean useSpelInvoker;
public MessagingMethodInvokerHelper(Object targetObject, Method method, Class<?> expectedType,
boolean canProcessMessageList) {
@@ -257,7 +257,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator im
this.handlerMessageMethods = null;
this.handlerMethodsList.add(
Collections.singletonMap(this.handlerMethod.targetParameterType, this.handlerMethod));
setDisplayString(targetObject, method);
this.displayString = buildDisplayString(targetObject, method);
this.jsonObjectMapper = configureJsonObjectMapperIfAny();
}
@@ -314,7 +314,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator im
this.handlerMethodsList.add(this.handlerMethods);
this.handlerMethodsList.add(this.handlerMessageMethods);
setDisplayString(targetObject, methodName);
this.displayString = buildDisplayString(targetObject, methodName);
this.jsonObjectMapper = configureJsonObjectMapperIfAny();
}
@@ -401,17 +401,17 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator im
return this.messageHandlerMethodFactory.createInvocableHandlerMethod(this.targetObject, method);
}
private void setDisplayString(Object targetObject, Object targetMethod) {
StringBuilder sb = new StringBuilder(targetObject.getClass().getName());
private String buildDisplayString(Object targetObject, Object targetMethod) {
StringBuilder sb =
new StringBuilder(targetObject.getClass().getName())
.append('.');
if (targetMethod instanceof Method) {
sb.append(".")
.append(((Method) targetMethod).getName());
sb.append(((Method) targetMethod).getName());
}
else if (targetMethod instanceof String) {
sb.append(".")
.append(targetMethod);
sb.append(targetMethod);
}
this.displayString = sb.toString() + "]";
return sb.append(']').toString();
}
private void prepareEvaluationContext() {
@@ -620,12 +620,10 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator im
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() + " ]");
}
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);
@@ -692,8 +690,8 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator im
parameters.payload = targetPayload;
}
}
catch (Exception e) {
LOGGER.debug("Failed to convert from JSON", e);
catch (Exception ex) {
LOGGER.debug(ex, "Failed to convert from JSON");
}
}
@@ -790,7 +788,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator im
if (candidateMethods.isEmpty() && candidateMessageMethods.isEmpty() && fallbackMethods.isEmpty()
&& fallbackMessageMethods.isEmpty()) {
findSingleSpecifMethodOnInterfacesIfProxy(candidateMessageMethods, candidateMethods);
findSingleSpecificMethodOnInterfacesIfProxy(candidateMessageMethods, candidateMethods);
}
}
@@ -802,10 +800,8 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator im
handlerMethodToUse = createHandlerMethod(
AopUtils.selectInvocableMethod(methodToProcess, ClassUtils.getUserClass(this.targetObject)));
}
catch (Exception e) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Method [" + methodToProcess + "] is not eligible for Message handling.", e);
}
catch (Exception ex) {
LOGGER.debug(ex, "Method [" + methodToProcess + "] is not eligible for Message handling.");
return null;
}
@@ -909,7 +905,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator im
return null;
}
private void findSingleSpecifMethodOnInterfacesIfProxy(Map<Class<?>, HandlerMethod> candidateMessageMethods,
private void findSingleSpecificMethodOnInterfacesIfProxy(Map<Class<?>, HandlerMethod> candidateMessageMethods,
Map<Class<?>, HandlerMethod> candidateMethods) {
if (AopUtils.isAopProxy(this.targetObject)) {
final AtomicReference<Method> targetMethod = new AtomicReference<>();
@@ -1153,7 +1149,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator im
methodParameter, parameterTypeDescriptor, parameterType, genericParameterType,
mappingAnnotation);
}
sb.append(")");
sb.append(')');
if (this.targetParameterTypeDescriptor == null) {
this.targetParameterTypeDescriptor = TypeDescriptor.valueOf(Void.class);
}
@@ -1332,7 +1328,8 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator im
if (this.targetParameterTypeDescriptor != null) {
throw new IneligibleMethodException("Found more than one parameter type candidate: [" +
this.targetParameterTypeDescriptor + "] and [" + targetParameterType + "]");
this.targetParameterTypeDescriptor + "] and [" + targetParameterType + "].\n" +
"Consider annotating one of the parameters with '@Payload'.");
}
this.targetParameterTypeDescriptor = targetParameterType;
if (Message.class.isAssignableFrom(targetParameterType.getObjectType())) {

View File

@@ -17,29 +17,23 @@
package org.springframework.integration.endpoint;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.Mockito.mock;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.logging.Log;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.log.LogAccessor;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.channel.NullChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.integration.transaction.DefaultTransactionSynchronizationFactory;
import org.springframework.integration.transaction.ExpressionEvaluatingTransactionSynchronizationProcessor;
import org.springframework.integration.transaction.IntegrationResourceHolder;
@@ -96,19 +90,8 @@ public class PseudoTransactionalMessageSourceTests {
}
});
MessageChannel afterCommitChannel = new NullChannel();
QueueChannel afterCommitChannel = new QueueChannel();
syncProcessor.setAfterCommitChannel(afterCommitChannel);
LogAccessor logAccessor = TestUtils.getPropertyValue(afterCommitChannel, "LOG", LogAccessor.class);
Log logger = logAccessor.getLog();
logger = Mockito.spy(logger);
Mockito.when(logger.isDebugEnabled()).thenReturn(true);
DirectFieldAccessor dfa = new DirectFieldAccessor(logAccessor);
dfa.setPropertyValue("log", logger);
TransactionSynchronizationManager.initSynchronization();
TransactionSynchronizationManager.setActualTransactionActive(true);
doPoll(adapter);
@@ -118,7 +101,10 @@ public class PseudoTransactionalMessageSourceTests {
assertThat(beforeCommitMessage).isNotNull();
assertThat(beforeCommitMessage.getPayload()).isEqualTo("qox");
Mockito.verify(logger).debug(Mockito.any(CharSequence.class));
assertThat(afterCommitChannel.receive(1000))
.isNotNull()
.extracting(Message::getPayload)
.isEqualTo("qux");
TransactionSynchronizationUtils.triggerAfterCompletion(TransactionSynchronization.STATUS_COMMITTED);
TransactionSynchronizationManager.clearSynchronization();
@@ -393,14 +379,12 @@ public class PseudoTransactionalMessageSourceTests {
}
protected void doPoll(SourcePollingChannelAdapter adapter) {
try {
Method method = AbstractPollingEndpoint.class.getDeclaredMethod("doPoll");
method.setAccessible(true);
method.invoke(adapter);
}
catch (Exception e) {
fail("Failed to invoke doPoll(): " + e.toString());
}
assertThatNoException()
.isThrownBy(() -> {
Method method = AbstractPollingEndpoint.class.getDeclaredMethod("doPoll");
method.setAccessible(true);
method.invoke(adapter);
});
}
public class Bar {