INT-4218: MessagingMethodInvokerHelp Improvements

JIRA: https://jira.spring.io/browse/INT-4218

* Add `InvocableHandlerMethod` invocation threshold logic to give up eventually in favor of permanent expression evaluation
* Add `ParametersWrapper.toString()` for better logging messages
* Log `InvocableHandlerMethod` invocation failure message in favor of SpEL only once when `failedAttempts` is exceeded already
* Switch to `spelOnly` mode after that
* Refactor `MessagingMethodInvokerHelper.processInternal()` to separate SpEL or Ivocable logic via their own invocation methods
* Move `MessagingMethodInvokerHelper.HandlerMethod` static fields to the `MessagingMethodInvokerHelper` level since it doesn't matter from this class perspective but can be reused in other places from top level of the `MessagingMethodInvokerHelper` class
* Catch `IllegalArgumentException` with the `java.lang.ClassCastException@...` message.
See http://stackoverflow.com/questions/16042591/reflections-illegalargumentexception-causes for more info

Address PR comments

* Make `handlerMethod.failedAttempts` conditional expression as `>=` to avoid race conditions in multi-threaded environment
* Analyze StackTrace for the class of the `IllegalStateExpception` avoid SpEL fall back in case of user exception, not reflection invocation
* Fix race condition in the `TcpNioConnectionTests`, when atomic value is set after `latch.countDown()`
This commit is contained in:
Artem Bilan
2017-02-02 18:06:20 -05:00
committed by Gary Russell
parent 802061985c
commit 9839b5fcaa
4 changed files with 150 additions and 67 deletions

View File

@@ -108,6 +108,25 @@ public class MessagingMethodInvokerHelper<T> extends AbstractExpressionEvaluator
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 SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER =
new LocalVariableTableParameterNameDiscoverer();
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);
private final DefaultMessageHandlerMethodFactory messageHandlerMethodFactory =
new DefaultMessageHandlerMethodFactory();
@@ -390,37 +409,13 @@ public class MessagingMethodInvokerHelper<T> extends AbstractExpressionEvaluator
Expression expression = candidate.expression;
Assert.notNull(candidate, "No candidate methods found for messages.");
T result = null;
try {
if (this.useSpelInvoker || candidate.spelOnly) {
result = invokeExpression(expression, parameters);
}
else {
result = candidate.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
&& !"argument type mismatch".equals(e.getCause().getMessage())) {
throw e;
}
}
if (logger.isInfoEnabled()) {
logger.info("Failed to invoke [ " + candidate.invocableHandlerMethod +
"] with provided arguments [ " + parameters + " ]. \n" +
"Falling back to SpEL invocation for expression [ " +
expression.getExpressionString() + " ]");
}
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)
@@ -432,6 +427,44 @@ public class MessagingMethodInvokerHelper<T> extends AbstractExpressionEvaluator
}
}
@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 {
@@ -745,21 +778,6 @@ public class MessagingMethodInvokerHelper<T> extends AbstractExpressionEvaluator
*/
private static class HandlerMethod {
private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER =
new LocalVariableTableParameterNameDiscoverer();
private static final TypeDescriptor messageTypeDescriptor = TypeDescriptor.valueOf(Message.class);
private static final TypeDescriptor messageListTypeDescriptor = new TypeDescriptor(
ReflectionUtils.findField(HandlerMethod.class, "dummyMessages"));
private static final TypeDescriptor messageArrayTypeDescriptor = TypeDescriptor.valueOf(Message[].class);
@SuppressWarnings("unused")
private static final Collection<Message<?>> dummyMessages = Collections.emptyList();
private final Expression expression;
private final InvocableHandlerMethod invocableHandlerMethod;
@@ -774,6 +792,11 @@ public class MessagingMethodInvokerHelper<T> extends AbstractExpressionEvaluator
private volatile boolean spelOnly;
// 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;
@@ -1050,6 +1073,20 @@ public class MessagingMethodInvokerHelper<T> extends AbstractExpressionEvaluator
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")

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -17,6 +17,7 @@
package org.springframework.integration.handler;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -38,6 +39,7 @@ import org.junit.Assert;
import org.junit.Test;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.MessageHeaders;
@@ -52,6 +54,7 @@ import org.springframework.messaging.support.GenericMessage;
* @author Iwein Fuld
* @author Oleg Zhurakousky
* @author Gary Russell
* @author Artem Bilan
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public class MethodInvokingMessageProcessorAnnotationTests {
@@ -102,7 +105,7 @@ public class MethodInvokingMessageProcessorAnnotationTests {
Method method = TestService.class.getMethod("requiredHeader", Integer.class);
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(testService, method);
Message<String> messageWithHeader = MessageBuilder.withPayload("foo")
.setHeader("num", new Integer(123)).build();
.setHeader("num", 123).build();
GenericMessage<String> messageWithoutHeader = new GenericMessage<String>("foo");
processor.processMessage(messageWithHeader);
@@ -113,7 +116,7 @@ public class MethodInvokingMessageProcessorAnnotationTests {
public void fromMessageWithRequiredHeaderProvided() throws Exception {
Method method = TestService.class.getMethod("requiredHeader", Integer.class);
Message<String> message = MessageBuilder.withPayload("foo")
.setHeader("num", new Integer(123)).build();
.setHeader("num", 123).build();
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(testService, method);
Object result = processor.processMessage(message);
assertEquals(123, result);
@@ -133,7 +136,7 @@ public class MethodInvokingMessageProcessorAnnotationTests {
Method method = TestService.class.getMethod("optionalAndRequiredHeader", String.class, Integer.class);
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(testService, method);
Message<String> message = MessageBuilder.withPayload("foo")
.setHeader("num", new Integer(123)).build();
.setHeader("num", 123).build();
Object result = processor.processMessage(message);
assertEquals("null123", result);
}
@@ -143,7 +146,7 @@ public class MethodInvokingMessageProcessorAnnotationTests {
Method method = TestService.class.getMethod("optionalAndRequiredHeader", String.class, Integer.class);
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(testService, method);
Message<String> message = MessageBuilder.withPayload("foo")
.setHeader("num", new Integer(123))
.setHeader("num", 123)
.setHeader("prop", "bar")
.build();
Object result = processor.processMessage(message);
@@ -156,10 +159,21 @@ public class MethodInvokingMessageProcessorAnnotationTests {
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(testService, method);
Message<String> message = MessageBuilder.withPayload("test")
.setHeader("prop1", "foo").setHeader("prop2", "bar").build();
assertFalse(TestUtils.getPropertyValue(processor, "delegate.handlerMethod.spelOnly", Boolean.class));
for (int i = 0; i < 99; i++) {
Object result = processor.processMessage(message);
Properties props = (Properties) result;
assertEquals("foo", props.getProperty("prop1"));
assertEquals("bar", props.getProperty("prop2"));
assertFalse(TestUtils.getPropertyValue(processor, "delegate.handlerMethod.spelOnly", Boolean.class));
}
Object result = processor.processMessage(message);
Properties props = (Properties) result;
assertEquals("foo", props.getProperty("prop1"));
assertEquals("bar", props.getProperty("prop2"));
assertTrue(TestUtils.getPropertyValue(processor, "delegate.handlerMethod.spelOnly", Boolean.class));
}
@Test
@@ -210,8 +224,8 @@ public class MethodInvokingMessageProcessorAnnotationTests {
Method method = TestService.class.getMethod("mapHeaders", Map.class);
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(testService, method);
Message<String> message = MessageBuilder.withPayload("test")
.setHeader("attrib1", new Integer(123))
.setHeader("attrib2", new Integer(456)).build();
.setHeader("attrib1", 123)
.setHeader("attrib2", 456).build();
Map<String, Object> result = (Map<String, Object>) processor.processMessage(message);
assertEquals(123, result.get("attrib1"));
assertEquals(456, result.get("attrib2"));
@@ -225,8 +239,8 @@ public class MethodInvokingMessageProcessorAnnotationTests {
payload.put("attrib1", 88);
payload.put("attrib2", 99);
Message<Map<String, Integer>> message = MessageBuilder.withPayload(payload)
.setHeader("attrib1", new Integer(123))
.setHeader("attrib2", new Integer(456)).build();
.setHeader("attrib1", 123)
.setHeader("attrib2", 456).build();
Map<String, Integer> result = (Map<String, Integer>) processor.processMessage(message);
assertEquals(2, result.size());
assertEquals(new Integer(88), result.get("attrib1"));
@@ -337,11 +351,19 @@ public class MethodInvokingMessageProcessorAnnotationTests {
}
private Message<?> getMessage() {
MessageBuilder<Employee> builder = MessageBuilder.withPayload(employee);
builder.setHeader("day", "monday");
builder.setHeader("month", "September");
return builder.build();
}
@SuppressWarnings("unused")
private static class MultipleMappingAnnotationTestBean {
public void test(@Payload("payload") @Header("foo") String s) {
}
}
@@ -379,7 +401,8 @@ public class MethodInvokingMessageProcessorAnnotationTests {
return lastName + ", " + firstName;
}
public String optionalAndRequiredHeader(@Header(required = false) String prop, @Header(value = "num", required = true) Integer num) {
public String optionalAndRequiredHeader(@Header(required = false) String prop,
@Header(value = "num", required = true) Integer num) {
return prop + num;
}
@@ -444,13 +467,7 @@ public class MethodInvokingMessageProcessorAnnotationTests {
ids.add(id);
return "foo";
}
}
private Message<?> getMessage() {
MessageBuilder<Employee> builder = MessageBuilder.withPayload(employee);
builder.setHeader("day", "monday");
builder.setHeader("month", "September");
return builder.build();
}
@@ -472,6 +489,7 @@ public class MethodInvokingMessageProcessorAnnotationTests {
public String getLname() {
return lname;
}
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.integration.handler;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -244,13 +245,13 @@ public class MethodInvokingMessageProcessorTests {
public void payloadAndHeaderAnnotationMethodParametersAndObjectAsReturnValue() {
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(new TestBean(),
"acceptPayloadAndHeaderAndReturnObject");
Message<?> request = MessageBuilder.withPayload("testing").setHeader("number", new Integer(123)).build();
Message<?> request = MessageBuilder.withPayload("testing").setHeader("number", 123).build();
Object result = processor.processMessage(request);
assertEquals("testing-123", result);
}
@Test
public void testVoidMethodsIncludedbyDefault() {
public void testVoidMethodsIncludedByDefault() {
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(new TestBean(),
"testVoidReturningMethods");
assertNull(processor.processMessage(MessageBuilder.withPayload("Something").build()));
@@ -271,8 +272,8 @@ public class MethodInvokingMessageProcessorTests {
AnnotatedTestService service = new AnnotatedTestService();
Method method = service.getClass().getMethod("integerMethod", Integer.class);
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(service, method);
Object result = processor.processMessage(new GenericMessage<Integer>(new Integer(123)));
assertEquals(new Integer(123), result);
Object result = processor.processMessage(new GenericMessage<>(123));
assertEquals(123, result);
}
@Test
@@ -281,7 +282,7 @@ public class MethodInvokingMessageProcessorTests {
Method method = service.getClass().getMethod("integerMethod", Integer.class);
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(service, method);
Object result = processor.processMessage(new GenericMessage<String>("456"));
assertEquals(new Integer(456), result);
assertEquals(456, result);
}
@Test(expected = MessageHandlingException.class)
@@ -733,10 +734,37 @@ public class MethodInvokingMessageProcessorTests {
compilerConfigAccessor.setPropertyValue("compilerMode", SpelCompilerMode.OFF);
}
@Test
public void testNoSpElFallbackWhenUserException() {
class A {
@SuppressWarnings("unused")
public void myMethod(Object payload) {
throw new IllegalStateException(new IllegalArgumentException("argument type mismatch"));
}
}
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(new A(), "myMethod");
try {
processor.processMessage(new GenericMessage<>("foo"));
}
catch (Exception e) {
assertThat(e.getCause(), instanceOf(IllegalStateException.class));
assertThat(e.getCause().getCause(), instanceOf(IllegalArgumentException.class));
assertEquals(A.class.getName(), e.getCause().getStackTrace()[0].getClassName());
}
assertEquals(0,
TestUtils.getPropertyValue(processor, "delegate.handlerMethod.failedAttempts"));
}
private DirectFieldAccessor compileImmediate(MethodInvokingMessageProcessor processor) {
// Update the parser configuration compiler mode
SpelParserConfiguration config = TestUtils.getPropertyValue(processor,
"delegate.handlerMethod.EXPRESSION_PARSER.configuration", SpelParserConfiguration.class);
"delegate.EXPRESSION_PARSER.configuration", SpelParserConfiguration.class);
DirectFieldAccessor accessor = new DirectFieldAccessor(config);
accessor.setPropertyValue("compilerMode", SpelCompilerMode.IMMEDIATE);
return accessor;

View File

@@ -716,8 +716,8 @@ public class TcpNioConnectionTests {
@Override
public boolean onMessage(Message<?> message) {
if (!(message instanceof ErrorMessage)) {
assemblerLatch.countDown();
assembler.set(Thread.currentThread());
assemblerLatch.countDown();
}
return false;
}