diff --git a/spring-integration-core/src/main/java/org/springframework/integration/annotation/MessagingGateway.java b/spring-integration-core/src/main/java/org/springframework/integration/annotation/MessagingGateway.java index 64fb353dad..28f3b1c5a3 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/annotation/MessagingGateway.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/annotation/MessagingGateway.java @@ -132,4 +132,15 @@ public @interface MessagingGateway { */ String mapper() default ""; + /** + * Indicate if {@code default} methods on the interface should be proxied as well. + * If an explicit {@link Gateway} annotation is present on method it is proxied + * independently of this option. + * Note: default methods in JDK classes (such as {@code Function}) can be proxied, but cannot be invoked + * via {@code MethodHandle} by an internal Java security restriction for {@code MethodHandle.Lookup}. + * @return the boolean flag to proxy default methods or invoke via {@code MethodHandle}. + * @since 5.3 + */ + boolean proxyDefaultMethods() default false; + } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingGatewayRegistrar.java b/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingGatewayRegistrar.java index 93128ddc66..a92be496fc 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingGatewayRegistrar.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingGatewayRegistrar.java @@ -68,8 +68,8 @@ public class MessagingGatewayRegistrar implements ImportBeanDefinitionRegistrar importingClassMetadata.getAnnotationAttributes(MessagingGateway.class.getName()); replaceEmptyOverrides(valuesHierarchy, annotationAttributes); // NOSONAR never null annotationAttributes.put("serviceInterface", importingClassMetadata.getClassName()); - - BeanDefinitionReaderUtils.registerBeanDefinition(this.parse(annotationAttributes), registry); + annotationAttributes.put("proxyDefaultMethods", "" + annotationAttributes.remove("proxyDefaultMethods")); + BeanDefinitionReaderUtils.registerBeanDefinition(parse(annotationAttributes), registry); } } @@ -84,6 +84,7 @@ public class MessagingGatewayRegistrar implements ImportBeanDefinitionRegistrar String errorChannel = (String) gatewayAttributes.get("errorChannel"); String asyncExecutor = (String) gatewayAttributes.get("asyncExecutor"); String mapper = (String) gatewayAttributes.get("mapper"); + String proxyDefaultMethods = (String) gatewayAttributes.get("proxyDefaultMethods"); boolean hasMapper = StringUtils.hasText(mapper); boolean hasDefaultPayloadExpression = StringUtils.hasText(defaultPayloadExpression); @@ -152,6 +153,9 @@ public class MessagingGatewayRegistrar implements ImportBeanDefinitionRegistrar if (StringUtils.hasText(mapper)) { gatewayProxyBuilder.addPropertyReference("mapper", mapper); } + if (StringUtils.hasText(proxyDefaultMethods)) { + gatewayProxyBuilder.addPropertyValue("proxyDefaultMethods", proxyDefaultMethods); + } gatewayProxyBuilder.addPropertyValue("defaultRequestTimeoutExpressionString", gatewayAttributes.get("defaultRequestTimeout")); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/GatewayParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/GatewayParser.java index df88e342c1..cf468a0e04 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/GatewayParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/GatewayParser.java @@ -86,6 +86,8 @@ public class GatewayParser implements BeanDefinitionParser { gatewayAttributes.put("serviceInterface", element.getAttribute("service-interface")); + gatewayAttributes.put("proxyDefaultMethods", element.getAttribute("proxy-default-methods")); + BeanDefinitionHolder gatewayHolder = this.registrar.parse(gatewayAttributes); if (isNested) { return gatewayHolder.getBeanDefinition(); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/dsl/GatewayProxySpec.java b/spring-integration-core/src/main/java/org/springframework/integration/dsl/GatewayProxySpec.java index d1f5e37396..2987b201e1 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/dsl/GatewayProxySpec.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/dsl/GatewayProxySpec.java @@ -263,6 +263,18 @@ public class GatewayProxySpec { return this; } + /** + * Indicate if {@code default} methods on the interface should be proxied as well. + * @param proxyDefaultMethods the boolean flag to proxy default methods or invoke via {@code MethodHandle}. + * @return current {@link GatewayProxySpec}. + * @see GatewayProxyFactoryBean#setProxyDefaultMethods(boolean) + * @since 5.3 + */ + public GatewayProxySpec proxyDefaultMethods(boolean proxyDefaultMethods) { + this.gatewayProxyFactoryBean.setProxyDefaultMethods(proxyDefaultMethods); + return this; + } + MessageChannel getGatewayRequestChannel() { return this.gatewayRequestChannel; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/gateway/AnnotationGatewayProxyFactoryBean.java b/spring-integration-core/src/main/java/org/springframework/integration/gateway/AnnotationGatewayProxyFactoryBean.java index 2b5d97adb6..83bcc8e622 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/gateway/AnnotationGatewayProxyFactoryBean.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/gateway/AnnotationGatewayProxyFactoryBean.java @@ -90,7 +90,10 @@ public class AnnotationGatewayProxyFactoryBean extends GatewayProxyFactoryBean { else if (StringUtils.hasText(asyncExecutor)) { setAsyncExecutor(beanFactory.getBean(asyncExecutor, Executor.class)); } - + boolean proxyDefaultMethods = this.gatewayAttributes.getBoolean("proxyDefaultMethods"); + if (proxyDefaultMethods) { + setProxyDefaultMethods(proxyDefaultMethods); + } super.onInit(); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/gateway/DefaultMethodInvokingMethodInterceptor.java b/spring-integration-core/src/main/java/org/springframework/integration/gateway/DefaultMethodInvokingMethodInterceptor.java new file mode 100644 index 0000000000..15fe76794b --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/gateway/DefaultMethodInvokingMethodInterceptor.java @@ -0,0 +1,202 @@ +/* + * Copyright 2015-2019 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 + * + * https://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.gateway; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.MethodType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Map; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.ProxyMethodInvocation; +import org.springframework.lang.Nullable; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; +import org.springframework.util.ReflectionUtils; + +/** + * Method interceptor to invoke default methods on the repository proxy. + * + * The copy of {@code DefaultMethodInvokingMethodInterceptor} from Spring Data Commons. + * + * @author Oliver Gierke + * @author Jens Schauder + * @author Mark Paluch + * @author Artem Bilan + * + * @since 5.3 + */ +class DefaultMethodInvokingMethodInterceptor implements MethodInterceptor { + + private final MethodHandleLookup methodHandleLookup = MethodHandleLookup.getMethodHandleLookup(); + + private final Map methodHandleCache = + new ConcurrentReferenceHashMap<>(10, ReferenceType.WEAK); + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { // NOSONAR + Method method = invocation.getMethod(); + if (!method.isDefault()) { + return invocation.proceed(); + } + Object[] arguments = invocation.getArguments(); + Object proxy = ((ProxyMethodInvocation) invocation).getProxy(); + return getMethodHandle(method) + .bindTo(proxy) + .invokeWithArguments(arguments); + } + + private MethodHandle getMethodHandle(Method method) { + return this.methodHandleCache.computeIfAbsent(method, + (key) -> { + try { + return this.methodHandleLookup.lookup(key); + } + catch (ReflectiveOperationException ex) { + throw new IllegalStateException(ex); + } + }); + } + + enum MethodHandleLookup { + + /** + * Encapsulated {@link MethodHandle} lookup working on Java 9. + */ + ENCAPSULATED { + + @Nullable + private final Method privateLookupIn = + ReflectionUtils.findMethod(MethodHandles.class, "privateLookupIn", Class.class, Lookup.class); + + @Override + MethodHandle lookup(Method method) throws ReflectiveOperationException { + if (this.privateLookupIn == null) { + throw new IllegalStateException("Could not obtain MethodHandles.privateLookupIn!"); + } + return doLookup(method, getLookup(method.getDeclaringClass(), this.privateLookupIn)); + } + + @Override + boolean isAvailable() { + return this.privateLookupIn != null; + } + + private Lookup getLookup(Class declaringClass, Method privateLookupIn) { + Lookup lookup = MethodHandles.lookup(); + try { + return (Lookup) privateLookupIn.invoke(MethodHandles.class, declaringClass, lookup); + } + catch (ReflectiveOperationException e) { + return lookup; + } + } + + }, + + /** + * Open (via reflection construction of {@link Lookup}) method handle lookup. Works with Java 8 and + * with Java 9 permitting illegal access. + */ + OPEN { + + @Nullable + private final Constructor constructor; + + { + Constructor ctor = null; + try { + ctor = Lookup.class.getDeclaredConstructor(Class.class); + ReflectionUtils.makeAccessible(ctor); + } + catch (Exception ex) { + // this is the signal that we are on Java 9 (encapsulated) and can't use the accessible constructor + // approach. + if (!ex.getClass().getName().equals("java.lang.reflect.InaccessibleObjectException")) { + throw new IllegalStateException(ex); + } + } + this.constructor = ctor; + } + + @Override + MethodHandle lookup(Method method) throws ReflectiveOperationException { + if (!isAvailable()) { + throw new IllegalStateException("Could not obtain MethodHandles.lookup constructor!"); + } + return this.constructor.newInstance(method.getDeclaringClass()) + .unreflectSpecial(method, method.getDeclaringClass()); + } + + @Override + boolean isAvailable() { + return this.constructor != null; + } + + }, + + /** + * Fallback {@link MethodHandle} lookup using {@link MethodHandles#lookup() public lookup}. + */ + FALLBACK { + @Override + MethodHandle lookup(Method method) throws ReflectiveOperationException { + return doLookup(method, MethodHandles.lookup()); + } + + @Override + boolean isAvailable() { + return true; + } + + }; + + private static MethodHandle doLookup(Method method, Lookup lookup) throws ReflectiveOperationException { + MethodType methodType = MethodType.methodType(method.getReturnType(), method.getParameterTypes()); + return lookup.findSpecial(method.getDeclaringClass(), method.getName(), + methodType, method.getDeclaringClass()); + } + + abstract MethodHandle lookup(Method method) throws ReflectiveOperationException; + + /** + * @return {@literal true} if the lookup is available. + */ + abstract boolean isAvailable(); + + /** + * Obtain the first available {@link MethodHandleLookup}. + * @return the {@link MethodHandleLookup} + * @throws IllegalStateException if no {@link MethodHandleLookup} is available. + */ + public static MethodHandleLookup getMethodHandleLookup() { + for (MethodHandleLookup it : MethodHandleLookup.values()) { + if (it.isAvailable()) { + return it; + } + } + throw new IllegalStateException("No MethodHandleLookup available!"); + } + + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/gateway/GatewayProxyFactoryBean.java b/spring-integration-core/src/main/java/org/springframework/integration/gateway/GatewayProxyFactoryBean.java index 02ef08585d..fe242fb82c 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/gateway/GatewayProxyFactoryBean.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/gateway/GatewayProxyFactoryBean.java @@ -17,6 +17,7 @@ package org.springframework.integration.gateway; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.UndeclaredThrowableException; import java.util.Collections; import java.util.HashMap; @@ -150,6 +151,8 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint private MethodArgsMessageMapper argsMapper; + private boolean proxyDefaultMethods; + private EvaluationContext evaluationContext = new StandardEvaluationContext(); /** @@ -362,6 +365,19 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint this.argsMapper = mapper; } + /** + * Indicate if {@code default} methods on the interface should be proxied as well. + * If an explicit {@link Gateway} annotation is present on method it is proxied + * independently of this option. + * Note: default methods in JDK classes (such as {@code Function}) can be proxied, but cannot be invoked + * via {@code MethodHandle} by an internal Java security restriction for {@code MethodHandle.Lookup}. + * @param proxyDefaultMethods the boolean flag to proxy default methods or invoke via {@code MethodHandle}. + * @since 5.3 + */ + public void setProxyDefaultMethods(boolean proxyDefaultMethods) { + this.proxyDefaultMethods = proxyDefaultMethods; + } + protected AsyncTaskExecutor getAsyncExecutor() { return this.asyncExecutor; } @@ -386,14 +402,13 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint if (this.channelResolver == null && beanFactory != null) { this.channelResolver = ChannelResolverUtils.getChannelResolver(beanFactory); } - Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(this.serviceInterface); - for (Method method : methods) { - MethodInvocationGateway gateway = createGatewayForMethod(method); - this.gatewayMap.put(method, gateway); - } - this.serviceProxy = - new ProxyFactory(this.serviceInterface, this) - .getProxy(this.beanClassLoader); + + populateMethodInvocationGateways(); + + ProxyFactory gatewayProxyFactory = + new ProxyFactory(this.serviceInterface, this); + gatewayProxyFactory.addAdvice(new DefaultMethodInvokingMethodInterceptor()); + this.serviceProxy = gatewayProxyFactory.getProxy(this.beanClassLoader); if (this.asyncExecutor != null) { Callable task = () -> null; Future submitType = this.asyncExecutor.submit(task); @@ -408,6 +423,19 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint } } + private void populateMethodInvocationGateways() { + Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(this.serviceInterface); + for (Method method : methods) { + if (Modifier.isAbstract(method.getModifiers()) + || method.getAnnotation(Gateway.class) != null + || (method.isDefault() && this.proxyDefaultMethods)) { + + MethodInvocationGateway gateway = createGatewayForMethod(method); + this.gatewayMap.put(method, gateway); + } + } + } + @Override public Class getObjectType() { return this.serviceInterface; @@ -477,6 +505,14 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint } Method method = invocation.getMethod(); MethodInvocationGateway gateway = this.gatewayMap.get(method); + if (gateway == null) { + try { + return invocation.proceed(); + } + catch (Throwable throwable) { + throw new IllegalStateException(throwable); + } + } boolean shouldReturnMessage = Message.class.isAssignableFrom(gateway.returnType) || (!runningOnCallerThread && gateway.expectMessage); boolean shouldReply = gateway.returnType != void.class; diff --git a/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd b/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd index 4a92cad9c9..3a188499c2 100644 --- a/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd +++ b/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd @@ -832,6 +832,19 @@ + + + + Indicate if default methods on the interface should be proxied as well. + If an explicit Gateway annotation is present on method it is proxied independently of this option. + Note: default methods in JDK classes (such as 'Function') can be proxied, but cannot be invoked + via 'MethodHandle' by an internal Java security restriction for 'MethodHandle.Lookup'. + + + + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/GatewayParserTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/GatewayParserTests-context.xml index 9d8ef50dec..946938031e 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/GatewayParserTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/GatewayParserTests-context.xml @@ -21,7 +21,8 @@ + default-request-channel="requestChannel" + proxy-default-methods="true"/> + request-timeout="456" + reply-timeout="123" + payload-expression="'fiz'" + reply-channel="foo">
+ request-timeout="args[1]" + reply-timeout="args[2]"> diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/GatewayParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/GatewayParserTests.java index 911deb2e64..0b48adb590 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/GatewayParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/GatewayParserTests.java @@ -31,8 +31,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.logging.Log; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.BeanNameAware; @@ -55,28 +54,32 @@ import org.springframework.integration.test.util.TestUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; +import org.springframework.messaging.SubscribableChannel; import org.springframework.messaging.support.ChannelInterceptor; import org.springframework.messaging.support.GenericMessage; import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; +import reactor.test.StepVerifier; /** * @author Mark Fisher * @author Artem Bilan * @author Gary Russell */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) public class GatewayParserTests { @Autowired private ApplicationContext context; + @Autowired + private SubscribableChannel errorChannel; + @Test public void testOneWay() { TestService service = (TestService) context.getBean("oneWay"); @@ -84,6 +87,17 @@ public class GatewayParserTests { PollableChannel channel = (PollableChannel) context.getBean("requestChannel"); Message result = channel.receive(10000); assertThat(result.getPayload()).isEqualTo("foo"); + + MonoProcessor defaultMethodHandler = MonoProcessor.create(); + + this.errorChannel.subscribe(message -> defaultMethodHandler.onNext(message.getPayload())); + + String defaultMethodPayload = "defaultMethodPayload"; + service.defaultMethodGateway(defaultMethodPayload); + + StepVerifier.create(defaultMethodHandler) + .expectNext(defaultMethodPayload) + .verifyComplete(); } @Test @@ -283,8 +297,8 @@ public class GatewayParserTests { assertThat(thread.get()).isEqualTo(Thread.currentThread()); assertThat(TestUtils.getPropertyValue(gateway, "asyncExecutor")).isNotNull(); verify(logger).debug("AsyncTaskExecutor submit*() return types are incompatible with the method return type; " - + "running on calling thread; the downstream flow must return the required Future: " - + "MyCompletableFuture"); + + "running on calling thread; the downstream flow must return the required Future: " + + "MyCompletableFuture"); } @Test @@ -409,7 +423,7 @@ public class GatewayParserTests { } @Override - @SuppressWarnings({"rawtypes", "unchecked"}) + @SuppressWarnings({ "rawtypes", "unchecked" }) public Future submit(Callable task) { try { Future result = super.submit(task); @@ -421,7 +435,7 @@ public class GatewayParserTests { } else { modifiedMessage = MessageBuilder.fromMessage(message) - .setHeader("executor", this.beanName).build(); + .setHeader("executor", this.beanName).build(); } return new AsyncResult(modifiedMessage); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/dsl/gateway/GatewayDslTests.java b/spring-integration-core/src/test/java/org/springframework/integration/dsl/gateway/GatewayDslTests.java index e63721a340..e55ad8c742 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/dsl/gateway/GatewayDslTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/dsl/gateway/GatewayDslTests.java @@ -20,7 +20,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.function.Function; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -30,11 +34,14 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.TaskExecutor; import org.springframework.integration.MessageRejectedException; +import org.springframework.integration.annotation.Gateway; import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.dsl.IntegrationFlows; import org.springframework.integration.dsl.MessageChannels; +import org.springframework.integration.gateway.GatewayProxyFactoryBean; +import org.springframework.integration.gateway.MessagingGatewaySupport; import org.springframework.integration.gateway.MethodArgsHolder; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; @@ -102,13 +109,36 @@ public class GatewayDslTests { } @Autowired - private Function> functionGateay; + private MessageFunction functionGateway; + + @Autowired + @Qualifier("&functionGateway.gateway") + private GatewayProxyFactoryBean functionGatewayFactoryBean; @Test void testHeadersFromFunctionGateway() { - Message message = this.functionGateay.apply("testPayload"); - assertThat(message.getPayload()).isEqualTo("testPayload"); - assertThat(message.getHeaders()).containsKeys("gatewayMethod", "gatewayArgs"); + Object payload = this.functionGateway + .andThen(message -> { + assertThat(message.getHeaders()).containsKeys("gatewayMethod", "gatewayArgs"); + return message.getPayload(); + }) + .apply("testPayload"); + + assertThat(payload).isEqualTo("testPayload"); + + Map gateways = this.functionGatewayFactoryBean.getGateways(); + assertThat(gateways).hasSize(2); + + List methodNames = gateways.keySet().stream().map(Method::getName).collect(Collectors.toList()); + assertThat(methodNames).containsExactlyInAnyOrder("apply", "defaultMethodGateway"); + + String defaultMethodPayload = "defaultMethodPayload"; + this.functionGateway.defaultMethodGateway(defaultMethodPayload); + + Message receive = this.gatewayError.receive(10_000); + assertThat(receive).isNotNull() + .extracting(Message::getPayload) + .isEqualTo(defaultMethodPayload); } @Autowired @@ -183,7 +213,23 @@ public class GatewayDslTests { } - interface MessageFunction extends Function> { + interface MessageFunction { + + Message apply(Object t); + + @Gateway(requestChannel = "gatewayError") + default void defaultMethodGateway(Object payload) { + throw new UnsupportedOperationException(); + } + + default Function andThen(Function, ? extends V> after) { + Objects.requireNonNull(after); + return (t) -> after.apply(apply(t)); + } + + static Function identity() { + return t -> t; + } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/gateway/TestService.java b/spring-integration-core/src/test/java/org/springframework/integration/gateway/TestService.java index 692a49274d..880f31bbb8 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/gateway/TestService.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/gateway/TestService.java @@ -19,6 +19,7 @@ package org.springframework.integration.gateway; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; +import org.springframework.integration.annotation.Gateway; import org.springframework.messaging.Message; import org.springframework.messaging.handler.annotation.Payload; @@ -65,6 +66,11 @@ public interface TestService { MyCompletableMessageFuture customCompletableReturnsMessage(String s); + @Gateway(requestChannel = "errorChannel") + default void defaultMethodGateway(Object payload) { + throw new UnsupportedOperationException(); + } + class MyCompletableFuture extends CompletableFuture { } diff --git a/spring-integration-core/src/test/kotlin/org/springframework/integration/function/FunctionsTests.kt b/spring-integration-core/src/test/kotlin/org/springframework/integration/function/FunctionsTests.kt index 9329bbfbdc..bc495dd630 100644 --- a/spring-integration-core/src/test/kotlin/org/springframework/integration/function/FunctionsTests.kt +++ b/spring-integration-core/src/test/kotlin/org/springframework/integration/function/FunctionsTests.kt @@ -17,6 +17,7 @@ package org.springframework.integration.function import assertk.assertThat +import assertk.assertions.containsAll import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import assertk.assertions.isTrue @@ -36,6 +37,7 @@ import org.springframework.integration.channel.QueueChannel import org.springframework.integration.config.EnableIntegration import org.springframework.integration.dsl.IntegrationFlows import org.springframework.integration.endpoint.SourcePollingChannelAdapter +import org.springframework.integration.gateway.GatewayProxyFactoryBean import org.springframework.messaging.Message import org.springframework.messaging.MessageChannel import org.springframework.messaging.PollableChannel @@ -50,6 +52,7 @@ import java.util.* import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.function.Function +import java.util.stream.Collectors /** * @author Artem Bilan @@ -126,6 +129,10 @@ class FunctionsTests { @Autowired private lateinit var monoFunction: Function>> + @Autowired + @Qualifier("&monoFunctionGateway.gateway") + private lateinit var monoFunctionGateway: GatewayProxyFactoryBean + @Test fun `verify Mono gateway`() { val mono = this.monoFunction.apply("test") @@ -133,6 +140,11 @@ class FunctionsTests { StepVerifier.create(mono.map(Message<*>::getPayload).cast(String::class.java)) .expectNext("TEST") .verifyComplete() + + val gateways = this.monoFunctionGateway.gateways + assertThat(gateways).size().isEqualTo(3) + val methodNames = gateways.keys.stream().map { it.name }.collect(Collectors.toList()) + assertThat(methodNames).containsAll("apply", "andThen", "compose") } @Configuration @@ -174,7 +186,7 @@ class FunctionsTests { @Bean fun monoFunctionGateway() = - IntegrationFlows.from(MonoFunction::class.java) + IntegrationFlows.from(MonoFunction::class.java) { gateway -> gateway.proxyDefaultMethods(true) } .handle({ p, _ -> Mono.just(p).map(String::toUpperCase) }) { e -> e.async(true) } .get() } diff --git a/src/reference/asciidoc/gateway.adoc b/src/reference/asciidoc/gateway.adoc index 1ae67d116a..ac65faffbb 100644 --- a/src/reference/asciidoc/gateway.adoc +++ b/src/reference/asciidoc/gateway.adoc @@ -351,6 +351,13 @@ public interface Cafe { If a method has no argument and no return value but does contain a payload expression, it is treated as a send-only operation. +[[gateway-calling-default-methods]] +==== Invoking `default` Methods + +An interface for gateway proxy may have `default` methods as well and starting with version 5.3, the framework injects a `DefaultMethodInvokingMethodInterceptor` into a proxy for calling `default` methods using a `java.lang.invoke.MethodHandle` approach instead of proxying. +The interfaces from JDK, such as `java.util.function.Function`, still can be used for gateway proxy, but their `default` methods cannot be called because of internal Java security reasons for a `MethodHandles.Lookup` instantiation against JDK classes. +These methods also can be proxied (losing their implementation logic and, at the same time, restoring previous gateway proxy behavior) using an explicit `@Gateway` annotation on the method, or `proxyDefaultMethods` on the `@MessagingGateway` annotation or `` XML component. + [[gateway-error-handling]] ==== Error Handling @@ -719,7 +726,7 @@ A `Mono` can be used to retrieve the result later (similar to a `Future`), or IMPORTANT: The `Mono` is not immediately flushed by the framework. Consequently, the underlying message flow is not started before the gateway method returns (as it is with a `Future` `Executor` task). The flow starts when the `Mono` is subscribed to. -Alternatively, the `Mono` (being a `Composable`) might be a part of Reactor stream, when the `subscribe()` is related to the entire `Flux`. +Alternatively, the `Mono` (being a "`Composable`") might be a part of Reactor stream, when the `subscribe()` is related to the entire `Flux`. The following example shows how to create a gateway with Project Reactor: ==== diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 1a781333b0..e960db2f0e 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -24,4 +24,5 @@ See its JavaDocs and <<./graph.adoc#integration-graph,Integration Graph>> for mo [[x5.3-general]] === General Changes - +The gateway proxy now doesn't proxy `default` methods by default. +see <<./gateway.adoc/gateway-calling-default-methods,Invoking `default` Methods>> for more information.