diff --git a/spring-integration-core/src/main/java/org/springframework/integration/annotation/UseSpelInvoker.java b/spring-integration-core/src/main/java/org/springframework/integration/annotation/UseSpelInvoker.java new file mode 100644 index 0000000000..345c6cc79b --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/annotation/UseSpelInvoker.java @@ -0,0 +1,74 @@ +/* + * Copyright 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Indicates that a POJO handler method ({@code @ServiceActivator, @Transformer, } etc., + * or such methods invoked from XML definitions) should be invoked using SpEL. + *

In versions prior to 5.0, such methods were always invoked using SpEL. In 5.0, the + * framework switched to using + * {@link org.springframework.messaging.handler.invocation.InvocableHandlerMethod} instead + * which is generally more efficient than (interpreted) SpEL. + *

There may be some unanticipated corner case where it is necessary to revert to using + * SpEL. Also, for very high performance requirements, you may wish to consider using + * compiled SpEL which is often the fastest solution (when the expression is compilable). + *

Applying this annotation to those methods will cause SpEL to be used for the + * invocation. An optional {@code compilerMode} property (aliased to value) is also provided. + * + * @author Gary Russell + * @since 5.0 + */ +@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface UseSpelInvoker { + + /** + * Specify that the annotated method (or methods in the annotated class) will be + * invoked using SpEL instead of an + * {@link org.springframework.messaging.handler.invocation.InvocableHandlerMethod} + * with the specified compilerMode. If left empty, the default runtime compiler + * mode will be used. Must evaluate to a String containing a valid compiler mode. + * @return The compilerMode. + * @see org.springframework.expression.spel.SpelCompilerMode + */ + @AliasFor("compilerMode") + String value() default ""; + + /** + * Specify that the annotated method (or methods in the annotated class) will be + * invoked using SpEL instead of an + * {@link org.springframework.messaging.handler.invocation.InvocableHandlerMethod} + * with the specified compilerMode. If left empty, the default runtime compiler + * mode will be used. Must evaluate to a String containing a valid compiler mode. + * @return The compilerMode. + * @see org.springframework.expression.spel.SpelCompilerMode + */ + @AliasFor("value") + String compilerMode() default ""; + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/util/MessagingMethodInvokerHelper.java b/spring-integration-core/src/main/java/org/springframework/integration/util/MessagingMethodInvokerHelper.java index 51e1f6c134..5e103202f1 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/util/MessagingMethodInvokerHelper.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/util/MessagingMethodInvokerHelper.java @@ -42,7 +42,12 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.framework.Advised; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.Lifecycle; +import org.springframework.context.expression.StandardBeanExpressionResolver; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; @@ -54,12 +59,16 @@ import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; import org.springframework.expression.TypeConverter; +import org.springframework.expression.spel.SpelCompilerMode; +import org.springframework.expression.spel.SpelParserConfiguration; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.integration.annotation.Default; import org.springframework.integration.annotation.Payloads; import org.springframework.integration.annotation.ServiceActivator; +import org.springframework.integration.annotation.UseSpelInvoker; import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.integration.handler.support.CollectionArgumentResolver; import org.springframework.integration.handler.support.MapArgumentResolver; @@ -114,11 +123,22 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator // 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 ExpressionParser EXPRESSION_PARSER_DEFAULT = EXPRESSION_PARSER; + + private static final ExpressionParser EXPRESSION_PARSER_OFF = new SpelExpressionParser( + new SpelParserConfiguration(SpelCompilerMode.OFF, null)); + + private static final ExpressionParser EXPRESSION_PARSER_IMMEDIATE = new SpelExpressionParser( + new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null)); + + private static final ExpressionParser EXPRESSION_PARSER_MIXED = new SpelExpressionParser( + new SpelParserConfiguration(SpelCompilerMode.MIXED, null)); private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new LocalVariableTableParameterNameDiscoverer(); + private static final Map SPEL_COMPILERS = new HashMap<>(); + private static final TypeDescriptor messageTypeDescriptor = TypeDescriptor.valueOf(Message.class); @SuppressWarnings("unused") @@ -129,6 +149,11 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator private static final TypeDescriptor messageArrayTypeDescriptor = TypeDescriptor.valueOf(Message[].class); + static { + SPEL_COMPILERS.put(SpelCompilerMode.OFF, EXPRESSION_PARSER_OFF); + SPEL_COMPILERS.put(SpelCompilerMode.IMMEDIATE, EXPRESSION_PARSER_IMMEDIATE); + SPEL_COMPILERS.put(SpelCompilerMode.MIXED, EXPRESSION_PARSER_MIXED); + } private final DefaultMessageHandlerMethodFactory messageHandlerMethodFactory = new DefaultMessageHandlerMethodFactory(); @@ -163,6 +188,11 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator private HandlerMethod defaultHandlerMethod; + private BeanExpressionResolver resolver = new StandardBeanExpressionResolver(); + + private BeanExpressionContext expressionContext; + + public MessagingMethodInvokerHelper(Object targetObject, Method method, Class expectedType, boolean canProcessMessageList) { this(targetObject, null, method, expectedType, canProcessMessageList); @@ -205,6 +235,14 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator public void setBeanFactory(BeanFactory beanFactory) { super.setBeanFactory(beanFactory); this.messageHandlerMethodFactory.setBeanFactory(beanFactory); + if (beanFactory instanceof ConfigurableListableBeanFactory) { + BeanExpressionResolver beanExpressionResolver = ((ConfigurableListableBeanFactory) beanFactory) + .getBeanExpressionResolver(); + if (beanExpressionResolver != null) { + this.resolver = beanExpressionResolver; + } + this.expressionContext = new BeanExpressionContext((ConfigurableListableBeanFactory) beanFactory, null); + } } @Override @@ -276,6 +314,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator this.messageHandlerMethodFactory.createInvocableHandlerMethod(targetObject, method); this.handlerMethod = new HandlerMethod(invocableHandlerMethod, canProcessMessageList); this.defaultHandlerMethod = null; + checkSpelInvokerRequired(getTargetClass(targetObject), method, this.handlerMethod); } catch (IneligibleMethodException e) { throw new IllegalArgumentException(e); @@ -381,51 +420,16 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator @SuppressWarnings("unchecked") private T processInternal(ParametersWrapper parameters) throws Exception { if (!this.initialized) { - synchronized (this) { - if (!this.initialized) { - PayloadExpressionArgumentResolver payloadExpressionArgumentResolver = - new PayloadExpressionArgumentResolver(); - payloadExpressionArgumentResolver.setBeanFactory(getBeanFactory()); - - PayloadsArgumentResolver payloadsArgumentResolver = new PayloadsArgumentResolver(); - payloadsArgumentResolver.setBeanFactory(getBeanFactory()); - - CollectionArgumentResolver collectionArgumentResolver = - new CollectionArgumentResolver(this.canProcessMessageList); - collectionArgumentResolver.setBeanFactory(getBeanFactory()); - - MapArgumentResolver mapArgumentResolver = new MapArgumentResolver(); - mapArgumentResolver.setBeanFactory(getBeanFactory()); - - List customArgumentResolvers = new LinkedList<>(); - customArgumentResolvers.add(payloadExpressionArgumentResolver); - customArgumentResolvers.add(payloadsArgumentResolver); - customArgumentResolvers.add(collectionArgumentResolver); - customArgumentResolvers.add(mapArgumentResolver); - - this.messageHandlerMethodFactory.setCustomArgumentResolvers(customArgumentResolvers); - - if (getBeanFactory() != null && - getBeanFactory() - .containsBean(IntegrationContextUtils.ARGUMENT_RESOLVER_MESSAGE_CONVERTER_BEAN_NAME)) { - this.messageHandlerMethodFactory - .setMessageConverter(getBeanFactory() - .getBean(IntegrationContextUtils.ARGUMENT_RESOLVER_MESSAGE_CONVERTER_BEAN_NAME, - MessageConverter.class)); - } - - this.messageHandlerMethodFactory.afterPropertiesSet(); - prepareEvaluationContext(); - this.initialized = true; - } - } + initialize(); } - - HandlerMethod candidate = findHandlerMethodForParameters(parameters); + HandlerMethod candidate = this.findHandlerMethodForParameters(parameters); if (candidate == null) { candidate = this.defaultHandlerMethod; } Assert.notNull(candidate, "No candidate methods found for messages."); + if (!candidate.initialized) { + initializeHandler(candidate); + } Expression expression = candidate.expression; T result; @@ -446,6 +450,61 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator } } + private void initializeHandler(HandlerMethod candidate) { + ExpressionParser parser; + if (candidate.useSpelInvoker == null) { + parser = EXPRESSION_PARSER_DEFAULT; + } + else { + String compilerMode = resolveExpression(candidate.useSpelInvoker.compilerMode(), + "UseSpelInvoker.compilerMode:").toUpperCase(); + parser = !StringUtils.hasText(compilerMode) + ? EXPRESSION_PARSER_DEFAULT + : SPEL_COMPILERS.get(SpelCompilerMode.valueOf(compilerMode)); + } + candidate.expression = parser.parseExpression(candidate.expressionString); + candidate.initialized = true; + } + + private synchronized void initialize() throws Exception { + if (!this.initialized) { + PayloadExpressionArgumentResolver payloadExpressionArgumentResolver = + new PayloadExpressionArgumentResolver(); + payloadExpressionArgumentResolver.setBeanFactory(getBeanFactory()); + + PayloadsArgumentResolver payloadsArgumentResolver = new PayloadsArgumentResolver(); + payloadsArgumentResolver.setBeanFactory(getBeanFactory()); + + CollectionArgumentResolver collectionArgumentResolver = + new CollectionArgumentResolver(this.canProcessMessageList); + collectionArgumentResolver.setBeanFactory(getBeanFactory()); + + MapArgumentResolver mapArgumentResolver = new MapArgumentResolver(); + mapArgumentResolver.setBeanFactory(getBeanFactory()); + + List customArgumentResolvers = new LinkedList<>(); + customArgumentResolvers.add(payloadExpressionArgumentResolver); + customArgumentResolvers.add(payloadsArgumentResolver); + customArgumentResolvers.add(collectionArgumentResolver); + customArgumentResolvers.add(mapArgumentResolver); + + this.messageHandlerMethodFactory.setCustomArgumentResolvers(customArgumentResolvers); + + if (getBeanFactory() != null && + getBeanFactory() + .containsBean(IntegrationContextUtils.ARGUMENT_RESOLVER_MESSAGE_CONVERTER_BEAN_NAME)) { + this.messageHandlerMethodFactory + .setMessageConverter(getBeanFactory() + .getBean(IntegrationContextUtils.ARGUMENT_RESOLVER_MESSAGE_CONVERTER_BEAN_NAME, + MessageConverter.class)); + } + + this.messageHandlerMethodFactory.afterPropertiesSet(); + prepareEvaluationContext(); + this.initialized = true; + } + } + @SuppressWarnings("unchecked") private T invokeHandlerMethod(HandlerMethod handlerMethod, ParametersWrapper parameters) throws Exception { try { @@ -550,6 +609,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator InvocableHandlerMethod invocableHandlerMethod = this.messageHandlerMethodFactory.createInvocableHandlerMethod(targetObject, method1); handlerMethod1 = new HandlerMethod(invocableHandlerMethod, this.canProcessMessageList); + checkSpelInvokerRequired(targetClass, method1, handlerMethod1); } catch (IneligibleMethodException e) { if (logger.isDebugEnabled()) { @@ -657,6 +717,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator this.messageHandlerMethodFactory.createInvocableHandlerMethod(targetObject, method); HandlerMethod handlerMethod = new HandlerMethod(invocableHandlerMethod, this.canProcessMessageList); + checkSpelInvokerRequired(targetClass, method, handlerMethod); handlerMethods.put(CANDIDATE_METHODS, Collections.singletonMap(Object.class, handlerMethod)); handlerMethods.put(CANDIDATE_MESSAGE_METHODS, candidateMessageMethods); return handlerMethods; @@ -685,6 +746,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator Map, HandlerMethod> candidateMethods) { if (AopUtils.isAopProxy(targetObject)) { final AtomicReference targetMethod = new AtomicReference<>(); + final AtomicReference> targetClass = new AtomicReference<>(); Class[] interfaces = ((Advised) targetObject).getProxiedInterfaces(); for (Class clazz : interfaces) { ReflectionUtils.doWithMethods(clazz, method1 -> { @@ -693,6 +755,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator } else { targetMethod.set(method1); + targetClass.set(clazz); } }, method12 -> method12.getName().equals(methodName)); } @@ -702,6 +765,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator InvocableHandlerMethod invocableHandlerMethod = this.messageHandlerMethodFactory.createInvocableHandlerMethod(targetObject, method); HandlerMethod handlerMethod = new HandlerMethod(invocableHandlerMethod, this.canProcessMessageList); + checkSpelInvokerRequired(targetClass.get(), method, handlerMethod); Class targetParameterType = handlerMethod.getTargetParameterType(); if (handlerMethod.isMessageMethod()) { if (candidateMessageMethods.containsKey(targetParameterType)) { @@ -727,6 +791,37 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator } } + private void checkSpelInvokerRequired(final Class targetClass, Method methodArg, HandlerMethod handlerMethod) { + Method method = AopUtils.getMostSpecificMethod(methodArg, targetClass); + UseSpelInvoker useSpel = AnnotationUtils.findAnnotation(method, UseSpelInvoker.class); + if (useSpel == null) { + useSpel = AnnotationUtils.findAnnotation(targetClass, UseSpelInvoker.class); + } + if (useSpel != null) { + handlerMethod.spelOnly = true; + handlerMethod.useSpelInvoker = useSpel; + } + } + + private String resolveExpression(String value, String msg) { + String resolvedValue = resolve(value); + + if (!(resolvedValue.startsWith("#{") && value.endsWith("}"))) { + return resolvedValue; + } + + Object evaluated = this.resolver.evaluate(resolvedValue, this.expressionContext); + Assert.isInstanceOf(String.class, evaluated, msg); + return (String) evaluated; + } + + private String resolve(String value) { + if (getBeanFactory() != null && getBeanFactory() instanceof ConfigurableBeanFactory) { + return ((ConfigurableBeanFactory) getBeanFactory()).resolveEmbeddedValue(value); + } + return value; + } + private Class getTargetClass(Object targetObject) { Class targetClass = targetObject.getClass(); if (AopUtils.isAopProxy(targetObject)) { @@ -806,12 +901,14 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator */ private static class HandlerMethod { - private final Expression expression; + private final String expressionString; private final InvocableHandlerMethod invocableHandlerMethod; private final boolean canProcessMessageList; + private volatile Expression expression; + private volatile TypeDescriptor targetParameterTypeDescriptor; private volatile Class targetParameterType = Void.class; @@ -820,6 +917,10 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator private volatile boolean spelOnly; + private volatile UseSpelInvoker useSpelInvoker; + + private volatile boolean initialized; + // 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. @@ -828,7 +929,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator HandlerMethod(InvocableHandlerMethod invocableHandlerMethod, boolean canProcessMessageList) { this.invocableHandlerMethod = invocableHandlerMethod; this.canProcessMessageList = canProcessMessageList; - this.expression = generateExpression(this.invocableHandlerMethod.getMethod()); + this.expressionString = generateExpression(this.invocableHandlerMethod.getMethod()); } @@ -854,7 +955,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator return this.invocableHandlerMethod.toString(); } - private Expression generateExpression(Method method) { + private String generateExpression(Method method) { StringBuilder sb = new StringBuilder("#target." + method.getName() + "("); Class[] parameterTypes = method.getParameterTypes(); Annotation[][] parameterAnnotations = method.getParameterAnnotations(); @@ -977,7 +1078,7 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator if (this.targetParameterTypeDescriptor == null) { this.targetParameterTypeDescriptor = TypeDescriptor.valueOf(Void.class); } - return EXPRESSION_PARSER.parseExpression(sb.toString()); + return sb.toString(); } private String determineHeaderExpression(Annotation headerAnnotation, MethodParameter methodParameter) { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/ReleaseStrategyFactoryBeanTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/ReleaseStrategyFactoryBeanTests.java index 263787c9ed..a41a7ab351 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/ReleaseStrategyFactoryBeanTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/ReleaseStrategyFactoryBeanTests.java @@ -68,7 +68,7 @@ public class ReleaseStrategyFactoryBeanTests { ReleaseStrategy delegate = factory.getObject(); assertThat(delegate, instanceOf(MethodInvokingReleaseStrategy.class)); assertThat(TestUtils.getPropertyValue(delegate, "adapter.delegate.targetObject", Bar.class), is(bar)); - assertThat(TestUtils.getPropertyValue(delegate, "adapter.delegate.handlerMethod.expression.expression"), + assertThat(TestUtils.getPropertyValue(delegate, "adapter.delegate.handlerMethod.expressionString"), equalTo("#target.doRelease2(messages)")); } @@ -121,7 +121,7 @@ public class ReleaseStrategyFactoryBeanTests { ReleaseStrategy delegate = factory.getObject(); assertThat(delegate, instanceOf(MethodInvokingReleaseStrategy.class)); assertThat(TestUtils.getPropertyValue(delegate, "adapter.delegate.targetObject", Baz.class), is(baz)); - assertThat(TestUtils.getPropertyValue(delegate, "adapter.delegate.handlerMethod.expression.expression"), + assertThat(TestUtils.getPropertyValue(delegate, "adapter.delegate.handlerMethod.expressionString"), equalTo("#target.doRelease2(messages)")); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java index 3b2651b06d..3882da5921 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java @@ -18,6 +18,7 @@ package org.springframework.integration.configuration; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; @@ -38,6 +39,7 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -81,6 +83,7 @@ import org.springframework.integration.annotation.Publisher; import org.springframework.integration.annotation.Role; import org.springframework.integration.annotation.ServiceActivator; import org.springframework.integration.annotation.Transformer; +import org.springframework.integration.annotation.UseSpelInvoker; import org.springframework.integration.channel.AbstractMessageChannel; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.channel.NullChannel; @@ -435,8 +438,23 @@ public class EnableIntegrationTests { @Test public void testMessagingGateway() throws InterruptedException { String payload = "bar"; - assertEquals(payload.toUpperCase(), this.testGateway.echo(payload)); - assertEquals(payload.toUpperCase() + "2", this.testGateway2.echo2(payload)); + String result = this.testGateway.echo(payload); + assertEquals(payload.toUpperCase(), result.substring(0, payload.length())); + assertThat(result, containsString("InvocableHandlerMethod")); + assertThat(result, not(containsString("SpelExpression"))); + result = this.testGateway2.echo2(payload); + assertNotNull(result); + assertEquals(payload.toUpperCase() + "2", result.substring(0, payload.length() + 1)); + assertThat(result, not(containsString("InvocableHandlerMethod"))); + assertThat(result, containsString("SpelExpression")); + assertThat(result, containsString("CompoundExpression.getValueInternal")); + assertNotNull(this.testGateway2.echo2("baz")); + result = this.testGateway2.echo2("baz"); // third one should be compiled + assertNotNull(result); + assertEquals("BAZ2", result.substring(0, 4)); + assertThat(result, not(containsString("InvocableHandlerMethod"))); + assertThat(result, containsString("SpelExpression")); + assertThat(result, containsString("Ex2.getValue(")); this.testGateway.sendAsync("foo"); assertTrue(this.asyncAnnotationProcessLatch.await(1, TimeUnit.SECONDS)); assertNotSame(Thread.currentThread(), this.asyncAnnotationProcessThread.get()); @@ -1230,17 +1248,18 @@ public class EnableIntegrationTests { assertEquals("FOO", message.getHeaders().get("foo")); assertTrue(message.getHeaders().containsKey("calledMethod")); assertEquals("echo", message.getHeaders().get("calledMethod")); - return this.handle(message.getPayload()); + return this.handle(message.getPayload()) + Arrays.asList(new Throwable().getStackTrace()).toString(); } @Override @Transformer(inputChannel = "gatewayChannel2") + @UseSpelInvoker(compilerMode = "${xxxxxxxx:IMMEDIATE}") public String transform2(Message message) { assertTrue(message.getHeaders().containsKey("foo")); assertEquals("FOO", message.getHeaders().get("foo")); assertTrue(message.getHeaders().containsKey("calledMethod")); assertEquals("echo2", message.getHeaders().get("calledMethod")); - return this.handle(message.getPayload()) + "2"; + return this.handle(message.getPayload()) + "2" + Arrays.asList(new Throwable().getStackTrace()).toString(); } @Override diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java index c9058168dc..bcc0e62d84 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java @@ -25,6 +25,8 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.willAnswer; import static org.mockito.Mockito.mock; import java.lang.reflect.Method; @@ -49,10 +51,12 @@ import org.junit.rules.ExpectedException; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; 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.annotation.UseSpelInvoker; import org.springframework.integration.gateway.GatewayProxyFactoryBean; import org.springframework.integration.gateway.RequestReplyExchanger; import org.springframework.integration.support.MessageBuilder; @@ -798,6 +802,69 @@ public class MethodInvokingMessageProcessorTests { assertTrue(adviceCalled.get()); } + @Test + public void testUseSpelInvoker() throws Exception { + UseSpelInvokerBean bean = new UseSpelInvokerBean(); + MessagingMethodInvokerHelper helper = new MessagingMethodInvokerHelper<>(bean, + UseSpelInvokerBean.class.getDeclaredMethod("foo", String.class), false); + Message message = new GenericMessage<>("Test"); + helper.process(message); + assertEquals(SpelCompilerMode.OFF, + TestUtils.getPropertyValue(helper, "handlerMethod.expression.configuration.compilerMode")); + + helper = new MessagingMethodInvokerHelper<>(bean, + UseSpelInvokerBean.class.getDeclaredMethod("bar", String.class), false); + helper.process(message); + assertEquals(SpelCompilerMode.IMMEDIATE, + TestUtils.getPropertyValue(helper, "handlerMethod.expression.configuration.compilerMode")); + + helper = new MessagingMethodInvokerHelper<>(bean, + UseSpelInvokerBean.class.getDeclaredMethod("baz", String.class), false); + helper.process(message); + assertEquals(SpelCompilerMode.MIXED, + TestUtils.getPropertyValue(helper, "handlerMethod.expression.configuration.compilerMode")); + + helper = new MessagingMethodInvokerHelper<>(bean, + UseSpelInvokerBean.class.getDeclaredMethod("qux", String.class), false); + helper.process(message); + assertEquals(SpelCompilerMode.OFF, + TestUtils.getPropertyValue(helper, "handlerMethod.expression.configuration.compilerMode")); + + helper = new MessagingMethodInvokerHelper<>(bean, + UseSpelInvokerBean.class.getDeclaredMethod("fiz", String.class), false); + try { + helper.process(message); + } + catch (IllegalArgumentException e) { + assertThat(e.getMessage(), + equalTo("No enum constant org.springframework.expression.spel.SpelCompilerMode.JUNK")); + } + + helper = new MessagingMethodInvokerHelper<>(bean, + UseSpelInvokerBean.class.getDeclaredMethod("buz", String.class), false); + ConfigurableListableBeanFactory bf = mock(ConfigurableListableBeanFactory.class); + willAnswer(i -> i.getArgument(0)).given(bf).resolveEmbeddedValue(anyString()); + helper.setBeanFactory(bf); + try { + helper.process(message); + } + catch (IllegalArgumentException e) { + assertThat(e.getMessage(), equalTo( + "UseSpelInvoker.compilerMode: Object of class [java.lang.Object] " + + "must be an instance of class java.lang.String")); + } + + // Check other CTORs + helper = new MessagingMethodInvokerHelper<>(bean, "bar", false); + helper.process(message); + assertEquals(SpelCompilerMode.IMMEDIATE, + TestUtils.getPropertyValue(helper, "handlerMethod.expression.configuration.compilerMode")); + + helper = new MessagingMethodInvokerHelper<>(bean, ServiceActivator.class, false); + helper.process(message); + assertEquals(SpelCompilerMode.MIXED, + TestUtils.getPropertyValue(helper, "handlerMethod.expression.configuration.compilerMode")); + } private DirectFieldAccessor compileImmediate(MethodInvokingMessageProcessor processor) { // Update the parser configuration compiler mode @@ -863,9 +930,9 @@ public class MethodInvokingMessageProcessorTests { } @SuppressWarnings("serial") - public static final class CheckedException extends Exception { + private static final class CheckedException extends Exception { - public CheckedException(String string) { + CheckedException(String string) { super(string); } @@ -1040,6 +1107,48 @@ public class MethodInvokingMessageProcessorTests { } + private static class UseSpelInvokerBean { + + UseSpelInvokerBean() { + super(); + } + + @UseSpelInvoker + public void foo(String foo) { + // empty + } + + @UseSpelInvoker("IMMEDIATE") + public void bar(String bar) { + // empty + } + + @ServiceActivator + @UseSpelInvoker("mixed") + public void baz(String baz) { + // empty + } + + @UseSpelInvoker("OfF") + public void qux(String qux) { + // empty + } + + @UseSpelInvoker("JUNK") + public void fiz(String fiz) { + // empty + } + + @UseSpelInvoker("#{new Object()}") + public void buz(String buz) { + // empty + } + + } + + /* + * Public for SpEL access. + */ public static class DotBean { private final String foo = "bar"; diff --git a/src/reference/asciidoc/overview.adoc b/src/reference/asciidoc/overview.adoc index 010e9224b7..5340ddfd21 100644 --- a/src/reference/asciidoc/overview.adoc +++ b/src/reference/asciidoc/overview.adoc @@ -262,6 +262,7 @@ Also see <> for more information about Messaging Annotations. === Programming Considerations It is generally recommended that you use plain old java objects (POJOs) whenever possible and only expose the framework in your code when absolutely necessary. +See <> for more information. If you do expose the framework to your classes, there are some considerations that need to be taken into account, especially during application startup; some of these are listed here. @@ -387,3 +388,69 @@ The sending thread returns immediately; the reply is sent asynchronously; uses ' ---- + +[[pojo-invocation]] +=== POJO Method invocation + +As discussed in <>, it is generally recommended to use a POJO programming style. +For example, + +[source, java] +---- +@ServiceActivator +public String myService(String payload) { ... } +---- + +In this case, the framework will extract a String payload, invoke your method, and wrap the result in a message to send to the next component in the flow (the original headers will be copied to the new message). +In fact, if you are using XML configuration, you don't even need the `@ServiceActivator` annotation: + +[source, xml] +---- + +---- + +[source, java] +---- +public String myService(String payload) { ... } +---- + +You can omit the `method` attribute as long as there is no ambiguity in the public methods on the class. + +Some further observations: + +You can obtain header information in your POJO methods: + +[source, java] +---- +@ServiceActivator +public String myService(@Payload String payload, @Header("foo") String fooHeader) { ... } +---- + +You can dereference properties on the message: + +[source, java] +---- +@ServiceActivator +public String myService(@Payload("payload.foo") String foo, @Header("bar.baz") String barbaz) { ... } +---- + +Because many any varied POJO method invocations are available, versions prior to _5.0_ used SpEL to invoke the POJO methods. +SpEL (even interpreted) is usually "fast enough" for these operations, when compared to the actual work usually done in the methods. +However, starting with _version 5.0_, the `org.springframework.messaging.handler.invocation.InvocableHandlerMethod` is used by default, when possible. +This technique is usually faster to execute than interpreted SpEL and is consistent with other Spring messaging projects. +The `InvocableHandlerMethod` is similar to the technique used to invoke controller methods in Spring MVC. +There are certain methods that are still always invoked using SpEL; examples include annotated parameters with dereferenced properties as discussed above. +This is because SpEL has the capability to navigate a property path. + +There may be some other corner cases that we haven't considered that also won't work with `InvocableHandlerMethod` s. +For this reason, we automatically fall-back to using SpEL in those cases. + +If you wish, you can also set up your POJO method such that it always uses SpEL, with the `UseSpelInvoker` annotation: + +[source, java] +---- +@UseSpelInvoker(compilerMode = "IMMEDIATE") +public void bar(String bar) { ... } +---- + +If the `compilerMode` property is omitted, the `spring.expression.compiler.mode` system property will determine the compiler mode - see http://docs.spring.io/spring-framework/docs/current/spring-framework-reference/html/expressions.html#expressions-spel-compilation[SpEL compilation] for more information about compiled SpEL. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index a08edec72a..08bdbdc32a 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -42,7 +42,10 @@ See <> for more information. The `SmartLifecycleRoleController` now provides methods to obtain status of endpoints in roles. See <> for more information. -When targeting POJO objects as message handlers, one of the service methods can now be marked with the `@Default` annotation to provide a fallback mechanism for non-matched conditions. +POJO methods are now invoked using an `InvocableHandlerMethod` by default, but can be configured to use SpEL as before. +See <> for more information. + +When targeting POJO methods as message handlers, one of the service methods can now be marked with the `@Default` annotation to provide a fallback mechanism for non-matched conditions. See <> for more information. ==== JMS Changes