Log error for class cast exception on lambda

**cherry-pick to 5.0.x**

* Fix test

* Polish log message.

* Polishing - docs and javadocs.

# Conflicts:
#	spring-integration-core/src/main/java/org/springframework/integration/dsl/IntegrationFlowDefinition.java
This commit is contained in:
Gary Russell
2018-11-19 16:56:35 -05:00
committed by Artem Bilan
parent 47a4b28f1f
commit ffaa4ee780
4 changed files with 140 additions and 46 deletions

View File

@@ -460,7 +460,7 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* @see GenericEndpointSpec
*/
public B controlBus(Consumer<GenericEndpointSpec<ServiceActivatingHandler>> endpointConfigurer) {
return this.handle(new ServiceActivatingHandler(new ExpressionCommandMessageProcessor(
return handle(new ServiceActivatingHandler(new ExpressionCommandMessageProcessor(
new ControlBusMethodFilter())), endpointConfigurer);
}
@@ -537,7 +537,9 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
}
/**
* Populate the {@link MessageTransformingHandler} instance for the provided {@link GenericTransformer}.
* Populate the {@link MessageTransformingHandler} instance for the provided
* {@link GenericTransformer}. Use {@link #transform(Class, GenericTransformer)} if
* you need to access the entire message.
* @param genericTransformer the {@link GenericTransformer} to populate.
* @param <S> the source type - 'transform from'.
* @param <T> the target type - 'transform to'.
@@ -589,11 +591,16 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
}
/**
* Populate the {@link MessageTransformingHandler} instance for the provided {@link GenericTransformer}
* for the specific {@code payloadType} to convert at runtime.
* @param payloadType the {@link Class} for expected payload type.
* Populate the {@link MessageTransformingHandler} instance for the provided
* {@link GenericTransformer} for the specific {@code payloadType} to convert at
* runtime.
* Use {@link #transform(Class, GenericTransformer)} if you need access to the
* entire message.
* @param payloadType the {@link Class} for expected payload type. It can also be
* {@code Message.class} if you wish to access the entire message in the transformer.
* Conversion to this type will be attempted, if necessary.
* @param genericTransformer the {@link GenericTransformer} to populate.
* @param <P> the payload type - 'transform from'.
* @param <P> the payload type - 'transform from' or {@code Message.class}.
* @param <T> the target type - 'transform to'.
* @return the current {@link IntegrationFlowDefinition}.
* @see MethodInvokingTransformer
@@ -604,10 +611,14 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
}
/**
* Populate the {@link MessageTransformingHandler} instance for the provided {@link GenericTransformer}.
* In addition accept options for the integration endpoint using {@link GenericEndpointSpec}.
* Populate the {@link MessageTransformingHandler} instance for the provided
* {@link GenericTransformer}. In addition accept options for the integration endpoint
* using {@link GenericEndpointSpec}. Use
* {@link #transform(Class, GenericTransformer, Consumer)} if you need to access the
* entire message.
* @param genericTransformer the {@link GenericTransformer} to populate.
* @param endpointConfigurer the {@link Consumer} to provide integration endpoint options.
* @param endpointConfigurer the {@link Consumer} to provide integration endpoint
* options.
* @param <S> the source type - 'transform from'.
* @param <T> the target type - 'transform to'.
* @return the current {@link IntegrationFlowDefinition}.
@@ -617,17 +628,20 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
*/
public <S, T> B transform(GenericTransformer<S, T> genericTransformer,
Consumer<GenericEndpointSpec<MessageTransformingHandler>> endpointConfigurer) {
return this.transform(null, genericTransformer, endpointConfigurer);
return transform(null, genericTransformer, endpointConfigurer);
}
/**
* Populate the {@link MessageTransformingHandler} instance for the provided {@link GenericTransformer}
* for the specific {@code payloadType} to convert at runtime.
* In addition accept options for the integration endpoint using {@link GenericEndpointSpec}.
* @param payloadType the {@link Class} for expected payload type.
* @param payloadType the {@link Class} for expected payload type. It can also be
* {@code Message.class} if you wish to access the entire message in the transformer.
* Conversion to this type will be attempted, if necessary.
* @param genericTransformer the {@link GenericTransformer} to populate.
* @param endpointConfigurer the {@link Consumer} to provide integration endpoint options.
* @param <P> the payload type - 'transform from'.
* @param <P> the payload type - 'transform from', or {@code Message.class}.
* @param <T> the target type - 'transform to'.
* @return the current {@link IntegrationFlowDefinition}.
* @see MethodInvokingTransformer
@@ -724,6 +738,8 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* .filter("World"::equals)
* }
* </pre>
* Use {@link #filter(Class, GenericSelector)} if you need to access the entire
* message.
* @param genericSelector the {@link GenericSelector} to use.
* @param <P> the source payload type.
* @return the current {@link IntegrationFlowDefinition}.
@@ -779,14 +795,16 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* .filter(Date.class, p -> p.after(new Date()))
* }
* </pre>
* @param payloadType the {@link Class} for desired {@code payload} type.
* @param payloadType the {@link Class} for expected payload type. It can also be
* {@code Message.class} if you wish to access the entire message in the selector.
* Conversion to this type will be attempted, if necessary.
* @param genericSelector the {@link GenericSelector} to use.
* @param <P> the source payload type.
* @param <P> the source payload type or {@code Message.class}.
* @return the current {@link IntegrationFlowDefinition}.
* @see LambdaMessageProcessor
*/
public <P> B filter(Class<P> payloadType, GenericSelector<P> genericSelector) {
return this.filter(payloadType, genericSelector, null);
return filter(payloadType, genericSelector, null);
}
/**
@@ -799,6 +817,8 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* .filter("World"::equals, e -> e.autoStartup(false))
* }
* </pre>
* Use {@link #filter(Class, GenericSelector, Consumer)} if you need to access the entire
* message.
* @param genericSelector the {@link GenericSelector} to use.
* @param endpointConfigurer the {@link Consumer} to provide integration endpoint options.
* @param <P> the source payload type.
@@ -819,10 +839,12 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* .filter(Date.class, p -> p.after(new Date()), e -> e.autoStartup(false))
* }
* </pre>
* @param payloadType the {@link Class} for desired {@code payload} type.
* @param payloadType the {@link Class} for expected payload type. It can also be
* {@code Message.class} if you wish to access the entire message in the selector.
* Conversion to this type will be attempted, if necessary.
* @param genericSelector the {@link GenericSelector} to use.
* @param endpointConfigurer the {@link Consumer} to provide integration endpoint options.
* @param <P> the source payload type.
* @param <P> the source payload type or {@code Message.class}.
* @return the current {@link IntegrationFlowDefinition}.
* @see LambdaMessageProcessor
* @see FilterEndpointSpec
@@ -880,7 +902,7 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* @return the current {@link IntegrationFlowDefinition}.
*/
public B handle(String beanName, String methodName) {
return this.handle(beanName, methodName, null);
return handle(beanName, methodName, null);
}
/**
@@ -955,6 +977,8 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* .<Integer>handle((p, h) -> p / 2)
* }
* </pre>
* Use {@link #handle(Class, GenericHandler)} if you need to access the entire
* message.
* @param handler the handler to invoke.
* @param <P> the payload type to expect.
* @return the current {@link IntegrationFlowDefinition}.
@@ -975,6 +999,8 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* .<Integer>handle((p, h) -> p / 2, e -> e.autoStartup(false))
* }
* </pre>
* Use {@link #handle(Class, GenericHandler, Consumer)} if you need to access the entire
* message.
* @param handler the handler to invoke.
* @param endpointConfigurer the {@link Consumer} to provide integration endpoint options.
* @param <P> the payload type to expect.
@@ -984,7 +1010,7 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
*/
public <P> B handle(GenericHandler<P> handler,
Consumer<GenericEndpointSpec<ServiceActivatingHandler>> endpointConfigurer) {
return this.handle(null, handler, endpointConfigurer);
return handle(null, handler, endpointConfigurer);
}
/**
@@ -997,15 +1023,16 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* .handle(Integer.class, (p, h) -> p / 2)
* }
* </pre>
* @param payloadType the expected payload type.
* The accepted payload can be converted to this one at runtime
* @param payloadType the {@link Class} for expected payload type. It can also be
* {@code Message.class} if you wish to access the entire message in the handler.
* Conversion to this type will be attempted, if necessary.
* @param handler the handler to invoke.
* @param <P> the payload type to expect.
* @param <P> the payload type to expect, or {@code Message.class}.
* @return the current {@link IntegrationFlowDefinition}.
* @see LambdaMessageProcessor
*/
public <P> B handle(Class<P> payloadType, GenericHandler<P> handler) {
return this.handle(payloadType, handler, null);
return handle(payloadType, handler, null);
}
/**
@@ -1019,11 +1046,12 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* .handle(Integer.class, (p, h) -> p / 2, e -> e.autoStartup(false))
* }
* </pre>
* @param payloadType the expected payload type.
* The accepted payload can be converted to this one at runtime
* @param payloadType the {@link Class} for expected payload type. It can also be
* {@code Message.class} if you wish to access the entire message in the handler.
* Conversion to this type will be attempted, if necessary.
* @param handler the handler to invoke.
* @param endpointConfigurer the {@link Consumer} to provide integration endpoint options.
* @param <P> the payload type to expect.
* @param <P> the payload type to expect or {@code Message.class}.
* @return the current {@link IntegrationFlowDefinition}.
* @see LambdaMessageProcessor
*/
@@ -1036,7 +1064,7 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
else {
serviceActivatingHandler = new ServiceActivatingHandler(handler, ClassUtils.HANDLER_HANDLE_METHOD);
}
return this.handle(serviceActivatingHandler, endpointConfigurer);
return handle(serviceActivatingHandler, endpointConfigurer);
}
/**
@@ -1241,9 +1269,9 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
/**
* Accept a {@link Map} of values to be used for the
* {@link org.springframework.messaging.Message} header enrichment.
* {@link Message} header enrichment.
* {@code values} can apply an {@link org.springframework.expression.Expression}
* to be evaluated against a request {@link org.springframework.messaging.Message}.
* to be evaluated against a request {@link Message}.
* @param headers the Map of headers to enrich.
* @return the current {@link IntegrationFlowDefinition}.
*/
@@ -1253,9 +1281,9 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
/**
* Accept a {@link Map} of values to be used for the
* {@link org.springframework.messaging.Message} header enrichment.
* {@link Message} header enrichment.
* {@code values} can apply an {@link org.springframework.expression.Expression}
* to be evaluated against a request {@link org.springframework.messaging.Message}.
* to be evaluated against a request {@link Message}.
* @param headers the Map of headers to enrich.
* @param endpointConfigurer the {@link Consumer} to provide integration endpoint options.
* @return the current {@link IntegrationFlowDefinition}.
@@ -1296,7 +1324,7 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* @return the current {@link IntegrationFlowDefinition}.
*/
public B split() {
return this.split((Consumer<SplitterEndpointSpec<DefaultMessageSplitter>>) null);
return split((Consumer<SplitterEndpointSpec<DefaultMessageSplitter>>) null);
}
/**
@@ -1314,7 +1342,7 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* @see SplitterEndpointSpec
*/
public B split(Consumer<SplitterEndpointSpec<DefaultMessageSplitter>> endpointConfigurer) {
return this.split(new DefaultMessageSplitter(), endpointConfigurer);
return split(new DefaultMessageSplitter(), endpointConfigurer);
}
/**
@@ -1398,7 +1426,7 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* @return the current {@link IntegrationFlowDefinition}.
*/
public B split(String beanName, String methodName) {
return this.split(beanName, methodName, null);
return split(beanName, methodName, null);
}
/**
@@ -1474,9 +1502,11 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* new Foo(rs.getInt(1), rs.getString(2)))))
* }
* </pre>
* @param payloadType the expected payload type. Used at runtime to convert received payload type to.
* @param payloadType the {@link Class} for expected payload type. It can also be
* {@code Message.class} if you wish to access the entire message in the splitter.
* Conversion to this type will be attempted, if necessary.
* @param splitter the splitter {@link Function}.
* @param <P> the payload type.
* @param <P> the payload type or {@code Message.class}.
* @return the current {@link IntegrationFlowDefinition}.
* @see LambdaMessageProcessor
*/
@@ -1528,20 +1558,23 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* , e -> e.applySequence(false))
* }
* </pre>
* @param payloadType the expected payload type. Used at runtime to convert received payload type to.
* @param payloadType the {@link Class} for expected payload type. It can also be
* {@code Message.class} if you wish to access the entire message in the splitter.
* Conversion to this type will be attempted, if necessary.
* @param splitter the splitter {@link Function}.
* @param endpointConfigurer the {@link Consumer} to provide integration endpoint options.
* @param <P> the payload type.
* @param <P> the payload type or {@code Message.class}.
* @return the current {@link IntegrationFlowDefinition}.
* @see LambdaMessageProcessor
* @see SplitterEndpointSpec
*/
public <P> B split(Class<P> payloadType, Function<P, ?> splitter,
Consumer<SplitterEndpointSpec<MethodInvokingSplitter>> endpointConfigurer) {
MethodInvokingSplitter split = isLambda(splitter)
? new MethodInvokingSplitter(new LambdaMessageProcessor(splitter, payloadType))
: new MethodInvokingSplitter(splitter, ClassUtils.FUNCTION_APPLY_METHOD);
return this.split(split, endpointConfigurer);
return split(split, endpointConfigurer);
}
/**
@@ -1631,7 +1664,7 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
*/
public B headerFilter(HeaderFilter headerFilter,
Consumer<GenericEndpointSpec<MessageTransformingHandler>> endpointConfigurer) {
return this.transform(headerFilter, endpointConfigurer);
return transform(headerFilter, endpointConfigurer);
}
/**
@@ -1655,7 +1688,7 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
*/
public B claimCheckIn(MessageStore messageStore,
Consumer<GenericEndpointSpec<MessageTransformingHandler>> endpointConfigurer) {
return this.transform(new ClaimCheckInTransformer(messageStore), endpointConfigurer);
return transform(new ClaimCheckInTransformer(messageStore), endpointConfigurer);
}
/**
@@ -1696,7 +1729,7 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
Consumer<GenericEndpointSpec<MessageTransformingHandler>> endpointConfigurer) {
ClaimCheckOutTransformer claimCheckOutTransformer = new ClaimCheckOutTransformer(messageStore);
claimCheckOutTransformer.setRemoveMessage(removeMessage);
return this.transform(claimCheckOutTransformer, endpointConfigurer);
return transform(claimCheckOutTransformer, endpointConfigurer);
}
/**
@@ -1861,6 +1894,7 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* .route(p -> p.equals("foo") || p.equals("bar") ? new String[] {"foo", "bar"} : null)
* }
* </pre>
* Use {@link #route(Class, Function)} if you need to access the entire message.
* @param router the {@link Function} to use.
* @param <S> the source payload type.
* @param <T> the target result type.
@@ -1879,9 +1913,11 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* .route(Integer.class, p -> p % 2 == 0)
* }
* </pre>
* @param payloadType the expected payload type.
* @param payloadType the {@link Class} for expected payload type. It can also be
* {@code Message.class} if you wish to access the entire message in the splitter.
* Conversion to this type will be attempted, if necessary.
* @param router the {@link Function} to use.
* @param <S> the source payload type.
* @param <S> the source payload type or {@code Message.class}.
* @param <T> the target result type.
* @return the current {@link IntegrationFlowDefinition}.
* @see LambdaMessageProcessor
@@ -1904,6 +1940,7 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* .applySequence(false))
* }
* </pre>
* Use {@link #route(Class, Function, Consumer)} if you need to access the entire message.
* @param router the {@link Function} to use.
* @param routerConfigurer the {@link Consumer} to provide {@link MethodInvokingRouter} options.
* @param <S> the source payload type.
@@ -1928,16 +1965,19 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
* .applySequence(false))
* }
* </pre>
* @param payloadType the expected payload type.
* @param payloadType the {@link Class} for expected payload type. It can also be
* {@code Message.class} if you wish to access the entire message in the splitter.
* Conversion to this type will be attempted, if necessary.
* @param router the {@link Function} to use.
* @param routerConfigurer the {@link Consumer} to provide {@link MethodInvokingRouter} options.
* @param <P> the source payload type.
* @param <P> the source payload type or {@code Message.class}.
* @param <T> the target result type.
* @return the current {@link IntegrationFlowDefinition}.
* @see LambdaMessageProcessor
*/
public <P, T> B route(Class<P> payloadType, Function<P, T> router,
Consumer<RouterSpec<T, MethodInvokingRouter>> routerConfigurer) {
MethodInvokingRouter methodInvokingRouter = isLambda(router)
? new MethodInvokingRouter(new LambdaMessageProcessor(router, payloadType))
: new MethodInvokingRouter(router, ClassUtils.FUNCTION_APPLY_METHOD);

View File

@@ -22,6 +22,9 @@ import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
@@ -39,11 +42,14 @@ import org.springframework.util.ReflectionUtils;
* - functional interface implementations.
*
* @author Artem Bilan
* @author Gary Russell
*
* @since 5.0
*/
public class LambdaMessageProcessor implements MessageProcessor<Object>, BeanFactoryAware {
private static final Log logger = LogFactory.getLog(LambdaMessageProcessor.class);
private final Object target;
private final Method method;
@@ -128,6 +134,12 @@ public class LambdaMessageProcessor implements MessageProcessor<Object>, BeanFac
return this.method.invoke(this.target, args);
}
catch (InvocationTargetException e) {
if (e.getTargetException() instanceof ClassCastException) {
logger.error("Could not invoke the method due to a class cast exception, if using a lambda in the DSL, "
+ "consider using an overloaded EIP method that takes a Class<?> argument to explicitly "
+ "specify the type. An example of when this often occurs is if the lambda is configured to "
+ "receive a Message<?> argument.", e.getCause());
}
throw new MessageHandlingException(message, e.getCause());
}
catch (Exception e) {

View File

@@ -16,6 +16,7 @@
package org.springframework.integration.dsl;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
@@ -67,6 +68,15 @@ public class LambdaMessageProcessorTests {
assertSame(testMessage, result);
}
@Test
public void testMessageAsArgumentLambda() {
LambdaMessageProcessor lmp = new LambdaMessageProcessor(
(GenericTransformer<Message<?>, Message<?>>) source -> messageTransformer(source), null);
lmp.setBeanFactory(mock(BeanFactory.class));
GenericMessage<String> testMessage = new GenericMessage<>("foo");
assertThatThrownBy(() -> lmp.processMessage(testMessage)).hasCauseExactlyInstanceOf(ClassCastException.class);
}
private void handle(GenericHandler<?> h) {
LambdaMessageProcessor lmp = new LambdaMessageProcessor(h, String.class);
lmp.setBeanFactory(mock(BeanFactory.class));

View File

@@ -108,6 +108,30 @@ The above example composes a sequence of `Filter -> Transformer -> Service Activ
The flow is 'one way', that is it does not provide a a reply message but simply prints the payload to STDOUT.
The endpoints are automatically wired together using direct channels.
[[java-dsl-class-cast]]
.Lambdas And `Message<?>` Arguments
IMPORTANT: When using lambdas in EIP methods, the "input" argument is generally the message payload.
If you wish to access the entire message, use one of the overloaded methods that take a `Class<?>` as the first parameter.
For example, this won't work:
====
[source, java]
----
.<Message<?>, Foo>transform(m -> newFooFromMessage(m))
----
====
This will fail at runtime with a `ClassCastException` because the lambda doesn't retain the argument type and the framework will attempt to cast the payload to a `Message<?>`.
Instead, use:
====
[source, java]
----
.(Message.class, m -> newFooFromMessage(m))
----
====
[[java-dsl-channels]]
=== Message Channels
@@ -258,6 +282,8 @@ Nevertheless, the DSL parser takes care about bean declarations for inline objec
See `Transformers` Java Docs for more information and supported factory methods.
Also see <<java-dsl-class-cast>>.
[[java-dsl-inbound-adapters]]
=== Inbound Channel Adapters
@@ -360,6 +386,8 @@ public IntegrationFlow recipientListFlow() {
The `.defaultOutputToParentFlow()` of the `.routeToRecipients()` allows to make the router's `defaultOutput` as a gateway to continue a process for the unmatched messages in the main flow.
Also see <<java-dsl-class-cast>>.
[[java-dsl-splitters]]
=== Splitters
@@ -383,6 +411,8 @@ public IntegrationFlow splitFlow() {
This creates a splitter that splits a message containing a comma delimited String.
Note: the `getT2()` method comes from `Tuple` `Collection` which is the result of `EndpointSpec.get()` and represents a pair of `ConsumerEndpointFactoryBean` and `DefaultMessageSplitter` for the example above.
Also see <<java-dsl-class-cast>>.
[[java-dsl-aggregators]]
=== Aggregators and Resequencers
@@ -458,6 +488,8 @@ public IntegrationFlow integerFlow() {
Of course we register some custom `BytesToIntegerConverter` within `ConversionService` and get rid of that additional `.transform()`.
Also see <<java-dsl-class-cast>>.
[[java-dsl-log]]
=== Operator log()