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
This commit is contained in:
Gary Russell
2017-01-27 10:16:31 -05:00
committed by Artem Bilan
parent 1f9d07ae24
commit c5c874f563
2 changed files with 181 additions and 14 deletions

View File

@@ -31,6 +31,7 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
@@ -314,7 +315,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) {
@@ -333,6 +334,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,
@@ -949,12 +952,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().equals(Optional.class)) {
return "#requiredHeader(headers, '" + headerName + "')" + relativeExpression;
}
else if (!StringUtils.hasLength(relativeExpression)) {
return headerRetrievalExpression + " ?: null";
}
else {
return headerRetrievalExpression + " != null ? " + fullHeaderExpression + " : null";
}
}
private void setExclusiveTargetParameterType(TypeDescriptor targetParameterType,
@@ -999,6 +1006,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.");

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;
@@ -67,7 +68,7 @@ import org.springframework.util.StopWatch;
* @author Gunnar Hillert
* @author Artem Bilan
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@SuppressWarnings({ "rawtypes", "unchecked" })
public class MethodInvokingMessageProcessorTests {
private static final Log logger = LogFactory.getLog(MethodInvokingMessageProcessorTests.class);
@@ -78,6 +79,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();
@@ -85,10 +87,12 @@ public class MethodInvokingMessageProcessorTests {
}
class B extends A {
}
@SuppressWarnings("unused")
class C extends B {
}
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(new B(), "myMethod");
@@ -99,6 +103,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();
@@ -106,6 +111,7 @@ public class MethodInvokingMessageProcessorTests {
}
class B extends A {
@Override
public Message<String> myMethod(Message<String> msg) {
return MessageBuilder.fromMessage(msg).setHeader("B", "B").build();
@@ -114,6 +120,7 @@ public class MethodInvokingMessageProcessorTests {
@SuppressWarnings("unused")
class C extends B {
}
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(new B(), "myMethod");
@@ -123,6 +130,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();
@@ -130,6 +138,7 @@ public class MethodInvokingMessageProcessorTests {
}
class B extends A {
@Override
public Message<String> myMethod(Message<String> msg) {
return MessageBuilder.fromMessage(msg).setHeader("B", "B").build();
@@ -137,6 +146,7 @@ public class MethodInvokingMessageProcessorTests {
}
class C extends B {
@Override
public Message<String> myMethod(Message<String> msg) {
return MessageBuilder.fromMessage(msg).setHeader("C", "C").build();
@@ -150,6 +160,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();
@@ -157,9 +168,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();
@@ -347,6 +360,110 @@ 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);
processor.setUseSpelInvoker(true);
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);
processor.setUseSpelInvoker(true);
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);
processor.setUseSpelInvoker(true);
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);
processor.setUseSpelInvoker(true);
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();
@@ -601,10 +718,7 @@ public class MethodInvokingMessageProcessorTests {
}
stopWatch.stop();
// Update the parser configuration compiler mode
SpelParserConfiguration config = TestUtils.getPropertyValue(processor,
"delegate.handlerMethod.EXPRESSION_PARSER.configuration", SpelParserConfiguration.class);
new DirectFieldAccessor(config).setPropertyValue("compilerMode", SpelCompilerMode.IMMEDIATE);
DirectFieldAccessor compilerConfigAccessor = compileImmediate(processor);
processor = new MethodInvokingMessageProcessor(service, method);
processor.setUseSpelInvoker(true);
@@ -616,6 +730,16 @@ public class MethodInvokingMessageProcessorTests {
stopWatch.stop();
logger.warn(stopWatch.prettyPrint());
compilerConfigAccessor.setPropertyValue("compilerMode", SpelCompilerMode.OFF);
}
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> {
@@ -748,12 +872,17 @@ public class MethodInvokingMessageProcessorTests {
return num;
}
public Integer requiredHeader(@Header(value = "num", required = true) Integer num) {
public Integer requiredHeader(@Header(value = "num") Integer num) {
return num;
}
public String optionalAndRequiredHeader(@Header(required = false) String prop,
@Header(value = "num", required = true) Integer num) {
@Header(value = "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;
}
@@ -845,4 +974,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;
}
}
}