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:
Gary Russell
2017-01-27 10:16:31 -05:00
committed by Artem Bilan
parent 2f00285c3b
commit d9c6ffee74
2 changed files with 181 additions and 13 deletions

View File

@@ -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;

View File

@@ -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;
}
}
}