INT-4217: SpEL Compilable for Required Header Args
JIRA: https://jira.spring.io/browse/INT-4217 Previously, SpEL method calls with required header parameters had the following form: #target.messageAndHeader(message, headers['number'] != null ? headers['number'] : T(org.springframework.util.Assert).isTrue(false, 'required header not available: number')) The SpEL compiler cannot compile this because the else clause of the ternary has no `exitDescriptor` to indicate the type. Change the expression to use a function for required headers. Also use an Elvis operator when possible. Some examples of new expressions: #target.optionalAndRequiredHeader(headers['prop'] ?: null, #requiredHeader(headers, 'num')) #target.optionalAndRequiredDottedHeader(headers['dot1'] != null ? headers['dot1'].foo : null, #requiredHeader(headers, 'dot2').baz In the second case, we can't use an Elvis because we're accessing a `foo` property of the header. __cherry-pick to 4.3.x__ - make `ParametersWrapper` `static` - minor conflicts in imports - remove the perf test * Polishing: remove redundant annotation args for their default values usage * Revert `java.util.Optional` import in favor of literal for Java < 8 * Make `AnnotatedTestService` as `public` to get compilable SpELs * Remove performance test from the `MethodInvokingMessageProcessorTests`
This commit is contained in:
committed by
Artem Bilan
parent
2f00285c3b
commit
d9c6ffee74
@@ -262,7 +262,7 @@ public class MessagingMethodInvokerHelper<T> extends AbstractExpressionEvaluator
|
||||
this.displayString = sb.toString() + "]";
|
||||
}
|
||||
|
||||
private void prepareEvaluationContext() {
|
||||
private void prepareEvaluationContext() throws Exception {
|
||||
StandardEvaluationContext context = getEvaluationContext(false);
|
||||
Class<?> targetType = AopUtils.getTargetClass(this.targetObject);
|
||||
if (this.method != null) {
|
||||
@@ -282,6 +282,8 @@ public class MessagingMethodInvokerHelper<T> extends AbstractExpressionEvaluator
|
||||
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,
|
||||
@@ -845,12 +847,16 @@ public class MessagingMethodInvokerHelper<T> extends AbstractExpressionEvaluator
|
||||
+ "disabled or header name is not explicitly provided via @Header annotation.");
|
||||
String headerRetrievalExpression = "headers['" + headerName + "']";
|
||||
String fullHeaderExpression = headerRetrievalExpression + relativeExpression;
|
||||
String fallbackExpression = (annotationAttributes.getBoolean("required")
|
||||
&& !methodParameter.getParameterType().getName().equals("java.util.Optional"))
|
||||
? "T(org.springframework.util.Assert).isTrue(false, 'required header not available: "
|
||||
+ headerName + "')"
|
||||
: "null";
|
||||
return headerRetrievalExpression + " != null ? " + fullHeaderExpression + " : " + fallbackExpression;
|
||||
if (annotationAttributes.getBoolean("required")
|
||||
&& !methodParameter.getParameterType().getName().equals("java.util.Optional")) {
|
||||
return "#requiredHeader(headers, '" + headerName + "')" + relativeExpression;
|
||||
}
|
||||
else if (!StringUtils.hasLength(relativeExpression)) {
|
||||
return headerRetrievalExpression + " ?: null";
|
||||
}
|
||||
else {
|
||||
return headerRetrievalExpression + " != null ? " + fullHeaderExpression + " : null";
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void setExclusiveTargetParameterType(TypeDescriptor targetParameterType,
|
||||
@@ -871,7 +877,7 @@ public class MessagingMethodInvokerHelper<T> extends AbstractExpressionEvaluator
|
||||
}
|
||||
}
|
||||
|
||||
public class ParametersWrapper {
|
||||
public static class ParametersWrapper {
|
||||
|
||||
private final Object payload;
|
||||
|
||||
@@ -895,6 +901,21 @@ public class MessagingMethodInvokerHelper<T> extends AbstractExpressionEvaluator
|
||||
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;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.integration.handler;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
@@ -40,12 +41,16 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.expression.spel.SpelCompilerMode;
|
||||
import org.springframework.expression.spel.SpelEvaluationException;
|
||||
import org.springframework.expression.spel.SpelParserConfiguration;
|
||||
import org.springframework.integration.annotation.ServiceActivator;
|
||||
import org.springframework.integration.gateway.GatewayProxyFactoryBean;
|
||||
import org.springframework.integration.gateway.RequestReplyExchanger;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
import org.springframework.integration.util.MessagingMethodInvokerHelper;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHandlingException;
|
||||
@@ -62,7 +67,7 @@ import org.springframework.messaging.support.GenericMessage;
|
||||
* @author Gunnar Hillert
|
||||
* @author Artem Bilan
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public class MethodInvokingMessageProcessorTests {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(MethodInvokingMessageProcessorTests.class);
|
||||
@@ -73,6 +78,7 @@ public class MethodInvokingMessageProcessorTests {
|
||||
@Test
|
||||
public void testHandlerInheritanceMethodImplInSuper() {
|
||||
class A {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Message<String> myMethod(final Message<String> msg) {
|
||||
return MessageBuilder.fromMessage(msg).setHeader("A", "A").build();
|
||||
@@ -80,10 +86,12 @@ public class MethodInvokingMessageProcessorTests {
|
||||
}
|
||||
|
||||
class B extends A {
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
class C extends B {
|
||||
|
||||
}
|
||||
|
||||
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(new B(), "myMethod");
|
||||
@@ -94,6 +102,7 @@ public class MethodInvokingMessageProcessorTests {
|
||||
@Test
|
||||
public void testHandlerInheritanceMethodImplInLatestSuper() {
|
||||
class A {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Message<String> myMethod(Message<String> msg) {
|
||||
return MessageBuilder.fromMessage(msg).setHeader("A", "A").build();
|
||||
@@ -101,6 +110,7 @@ public class MethodInvokingMessageProcessorTests {
|
||||
}
|
||||
|
||||
class B extends A {
|
||||
|
||||
@Override
|
||||
public Message<String> myMethod(Message<String> msg) {
|
||||
return MessageBuilder.fromMessage(msg).setHeader("B", "B").build();
|
||||
@@ -109,6 +119,7 @@ public class MethodInvokingMessageProcessorTests {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
class C extends B {
|
||||
|
||||
}
|
||||
|
||||
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(new B(), "myMethod");
|
||||
@@ -118,6 +129,7 @@ public class MethodInvokingMessageProcessorTests {
|
||||
|
||||
public void testHandlerInheritanceMethodImplInSubClass() {
|
||||
class A {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Message<String> myMethod(Message<String> msg) {
|
||||
return MessageBuilder.fromMessage(msg).setHeader("A", "A").build();
|
||||
@@ -125,6 +137,7 @@ public class MethodInvokingMessageProcessorTests {
|
||||
}
|
||||
|
||||
class B extends A {
|
||||
|
||||
@Override
|
||||
public Message<String> myMethod(Message<String> msg) {
|
||||
return MessageBuilder.fromMessage(msg).setHeader("B", "B").build();
|
||||
@@ -132,6 +145,7 @@ public class MethodInvokingMessageProcessorTests {
|
||||
}
|
||||
|
||||
class C extends B {
|
||||
|
||||
@Override
|
||||
public Message<String> myMethod(Message<String> msg) {
|
||||
return MessageBuilder.fromMessage(msg).setHeader("C", "C").build();
|
||||
@@ -145,6 +159,7 @@ public class MethodInvokingMessageProcessorTests {
|
||||
|
||||
public void testHandlerInheritanceMethodImplInSubClassAndSuper() {
|
||||
class A {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Message<String> myMethod(Message<String> msg) {
|
||||
return MessageBuilder.fromMessage(msg).setHeader("A", "A").build();
|
||||
@@ -152,9 +167,11 @@ public class MethodInvokingMessageProcessorTests {
|
||||
}
|
||||
|
||||
class B extends A {
|
||||
|
||||
}
|
||||
|
||||
class C extends B {
|
||||
|
||||
@Override
|
||||
public Message<String> myMethod(Message<String> msg) {
|
||||
return MessageBuilder.fromMessage(msg).setHeader("C", "C").build();
|
||||
@@ -341,6 +358,106 @@ public class MethodInvokingMessageProcessorTests {
|
||||
assertEquals("bar-42", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void optionalAndRequiredWithAnnotatedMethod() throws Exception {
|
||||
AnnotatedTestService service = new AnnotatedTestService();
|
||||
Method method = service.getClass().getMethod("optionalAndRequiredHeader", String.class, Integer.class);
|
||||
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(service, method);
|
||||
optionalAndRequiredWithAnnotatedMethodGuts(processor, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compiledOptionalAndRequiredWithAnnotatedMethod() throws Exception {
|
||||
AnnotatedTestService service = new AnnotatedTestService();
|
||||
Method method = service.getClass().getMethod("optionalAndRequiredHeader", String.class, Integer.class);
|
||||
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(service, method);
|
||||
DirectFieldAccessor compilerConfigAccessor = compileImmediate(processor);
|
||||
optionalAndRequiredWithAnnotatedMethodGuts(processor, true);
|
||||
assertNotNull(TestUtils.getPropertyValue(processor, "delegate.handlerMethod.expression.compiledAst"));
|
||||
optionalAndRequiredWithAnnotatedMethodGuts(processor, true);
|
||||
compilerConfigAccessor.setPropertyValue("compilerMode", SpelCompilerMode.OFF);
|
||||
}
|
||||
|
||||
private void optionalAndRequiredWithAnnotatedMethodGuts(MethodInvokingMessageProcessor processor,
|
||||
boolean compiled) {
|
||||
Message<String> message = MessageBuilder.withPayload("foo")
|
||||
.setHeader("num", 42)
|
||||
.build();
|
||||
Object result = processor.processMessage(message);
|
||||
assertEquals("null42", result);
|
||||
message = MessageBuilder.withPayload("foo")
|
||||
.setHeader("prop", "bar")
|
||||
.setHeader("num", 42)
|
||||
.build();
|
||||
result = processor.processMessage(message);
|
||||
assertEquals("bar42", result);
|
||||
message = MessageBuilder.withPayload("foo")
|
||||
.setHeader("prop", "bar")
|
||||
.build();
|
||||
try {
|
||||
result = processor.processMessage(message);
|
||||
fail("Expected MessageHandlingException");
|
||||
}
|
||||
catch (MessageHandlingException e) {
|
||||
if (compiled) {
|
||||
assertThat(e.getCause().getMessage(), equalTo("required header not available: num"));
|
||||
}
|
||||
else {
|
||||
assertThat(e.getCause().getCause().getMessage(), equalTo("required header not available: num"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void optionalAndRequiredDottedWithAnnotatedMethod() throws Exception {
|
||||
AnnotatedTestService service = new AnnotatedTestService();
|
||||
Method method = service.getClass().getMethod("optionalAndRequiredDottedHeader", String.class, Integer.class);
|
||||
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(service, method);
|
||||
optionalAndRequiredDottedWithAnnotatedMethodGuts(processor, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compiledOptionalAndRequiredDottedWithAnnotatedMethod() throws Exception {
|
||||
AnnotatedTestService service = new AnnotatedTestService();
|
||||
Method method = service.getClass().getMethod("optionalAndRequiredDottedHeader", String.class, Integer.class);
|
||||
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(service, method);
|
||||
DirectFieldAccessor compilerConfigAccessor = compileImmediate(processor);
|
||||
optionalAndRequiredDottedWithAnnotatedMethodGuts(processor, true);
|
||||
assertNotNull(TestUtils.getPropertyValue(processor, "delegate.handlerMethod.expression.compiledAst"));
|
||||
optionalAndRequiredDottedWithAnnotatedMethodGuts(processor, true);
|
||||
compilerConfigAccessor.setPropertyValue("compilerMode", SpelCompilerMode.OFF);
|
||||
}
|
||||
|
||||
private void optionalAndRequiredDottedWithAnnotatedMethodGuts(MethodInvokingMessageProcessor processor,
|
||||
boolean compiled) {
|
||||
Message<String> message = MessageBuilder.withPayload("hello")
|
||||
.setHeader("dot2", new DotBean())
|
||||
.build();
|
||||
Object result = processor.processMessage(message);
|
||||
assertEquals("null42", result);
|
||||
message = MessageBuilder.withPayload("hello")
|
||||
.setHeader("dot1", new DotBean())
|
||||
.setHeader("dot2", new DotBean())
|
||||
.build();
|
||||
result = processor.processMessage(message);
|
||||
assertEquals("bar42", result);
|
||||
message = MessageBuilder.withPayload("hello")
|
||||
.setHeader("dot1", new DotBean())
|
||||
.build();
|
||||
try {
|
||||
result = processor.processMessage(message);
|
||||
fail("Expected MessageHandlingException");
|
||||
}
|
||||
catch (MessageHandlingException e) {
|
||||
if (compiled) {
|
||||
assertThat(e.getCause().getMessage(), equalTo("required header not available: dot2"));
|
||||
}
|
||||
else { // interpreted
|
||||
assertThat(e.getCause().getCause().getMessage(), equalTo("required header not available: dot2"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOverloadedNonVoidReturningMethodsWithExactMatchForType() {
|
||||
AmbiguousMethodBean bean = new AmbiguousMethodBean();
|
||||
@@ -527,7 +644,7 @@ public class MethodInvokingMessageProcessorTests {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void optionalHeaders(Optional<String> foo, @Header(value = "foo", required = false) String foo1,
|
||||
@Header(value = "foo") Optional<String> foo2) {
|
||||
@Header("foo") Optional<String> foo2) {
|
||||
this.arguments.put("foo", (foo.isPresent() ? foo.get() : null));
|
||||
this.arguments.put("foo1", foo1);
|
||||
this.arguments.put("foo2", (foo2.isPresent() ? foo2.get() : null));
|
||||
@@ -550,6 +667,15 @@ public class MethodInvokingMessageProcessorTests {
|
||||
assertEquals("FOO", targetObject.arguments.get("foo2"));
|
||||
}
|
||||
|
||||
private DirectFieldAccessor compileImmediate(MethodInvokingMessageProcessor processor) {
|
||||
// Update the parser configuration compiler mode
|
||||
SpelParserConfiguration config = TestUtils.getPropertyValue(processor,
|
||||
"delegate.handlerMethod.EXPRESSION_PARSER.configuration", SpelParserConfiguration.class);
|
||||
DirectFieldAccessor accessor = new DirectFieldAccessor(config);
|
||||
accessor.setPropertyValue("compilerMode", SpelCompilerMode.IMMEDIATE);
|
||||
return accessor;
|
||||
}
|
||||
|
||||
private static class ExceptionCauseMatcher extends TypeSafeMatcher<Exception> {
|
||||
private Throwable cause;
|
||||
|
||||
@@ -640,7 +766,7 @@ public class MethodInvokingMessageProcessorTests {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class AnnotatedTestService {
|
||||
public static class AnnotatedTestService {
|
||||
|
||||
public String messageOnly(Message<?> message) {
|
||||
return (String) message.getPayload();
|
||||
@@ -658,12 +784,17 @@ public class MethodInvokingMessageProcessorTests {
|
||||
return num;
|
||||
}
|
||||
|
||||
public Integer requiredHeader(@Header(value = "num", required = true) Integer num) {
|
||||
public Integer requiredHeader(@Header("num") Integer num) {
|
||||
return num;
|
||||
}
|
||||
|
||||
public String optionalAndRequiredHeader(@Header(required = false) String prop,
|
||||
@Header(value = "num", required = true) Integer num) {
|
||||
@Header("num") Integer num) {
|
||||
return prop + num;
|
||||
}
|
||||
|
||||
public String optionalAndRequiredDottedHeader(@Header(name = "dot1.foo", required = false) String prop,
|
||||
@Header(name = "dot2.baz") Integer num) {
|
||||
return prop + num;
|
||||
}
|
||||
|
||||
@@ -743,4 +874,20 @@ public class MethodInvokingMessageProcessorTests {
|
||||
|
||||
}
|
||||
|
||||
public static class DotBean {
|
||||
|
||||
private final String foo = "bar";
|
||||
|
||||
private final Integer baz = 42;
|
||||
|
||||
public String getFoo() {
|
||||
return this.foo;
|
||||
}
|
||||
|
||||
public Integer getBaz() {
|
||||
return this.baz;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user