GH-3118: MessagingGW: Don't proxy default methods (#3119)

* GH-3118: MessagingGW: Don't proxy default methods

Fixes https://github.com/spring-projects/spring-integration/issues/3118

The `GatewayProxyFactoryBean` proxies all the methods in the provided interface,
including `default` and `static`.
This is not what is expected from end-users.

* Proxy only `abstract` methods from the provided interface
* Introduce a `DefaultMethodInvokingMethodInterceptor` to call `default`
method on the interface using a `MethodHandle` approach for those methods
calling

* * Introduce a `proxyDefaultMethods` option to let to
restore a previous behavior
* Handle a new property from XML, annotations & DSL configurations
* Ensure that the property works in various tests
* Document the feature

* * Fix typo in the `build.gradle`
This commit is contained in:
Artem Bilan
2019-12-17 14:37:06 -05:00
parent 13c72f653e
commit c08f4ec5c6
15 changed files with 406 additions and 36 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Method, MethodHandle> 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<Lookup> constructor;
{
Constructor<Lookup> 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!");
}
}
}

View File

@@ -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<String> task = () -> null;
Future<String> 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;

View File

@@ -832,6 +832,19 @@
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="proxy-default-methods" default="false">
<xsd:annotation>
<xsd:documentation>
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'.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="xsd:boolean xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
</xsd:complexType>
</xsd:element>

View File

@@ -21,7 +21,8 @@
<gateway id="oneWay"
service-interface="org.springframework.integration.gateway.TestService"
default-request-channel="requestChannel"/>
default-request-channel="requestChannel"
proxy-default-methods="true"/>
<gateway id="solicitResponse"
service-interface="org.springframework.integration.gateway.TestService"
@@ -84,15 +85,15 @@
async-executor="testExecutor">
<default-header name="baz" value="qux"/>
<method name="oneWay" request-channel="otherRequestChannel"
request-timeout="456"
reply-timeout="123"
payload-expression="'fiz'"
reply-channel="foo">
request-timeout="456"
reply-timeout="123"
payload-expression="'fiz'"
reply-channel="foo">
<header name="foo" value="bar"/>
</method>
<method name="oneWayWithTimeouts" request-channel="otherRequestChannel"
request-timeout="args[1]"
reply-timeout="args[2]">
request-timeout="args[1]"
reply-timeout="args[2]">
</method>
</gateway>

View File

@@ -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<Object> 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 <T> Future<T> submit(Callable<T> 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);
}

View File

@@ -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<Object, Message<?>> 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<Method, MessagingGatewaySupport> gateways = this.functionGatewayFactoryBean.getGateways();
assertThat(gateways).hasSize(2);
List<String> 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<Object, Message<?>> {
interface MessageFunction {
Message<?> apply(Object t);
@Gateway(requestChannel = "gatewayError")
default void defaultMethodGateway(Object payload) {
throw new UnsupportedOperationException();
}
default <V> Function<Object, V> andThen(Function<? super Message<?>, ? extends V> after) {
Objects.requireNonNull(after);
return (t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}

View File

@@ -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<String> {
}

View File

@@ -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<String, Mono<Message<*>>>
@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<String>({ p, _ -> Mono.just(p).map(String::toUpperCase) }) { e -> e.async(true) }
.get()
}

View File

@@ -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 `<gateway>` 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:
====

View File

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