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:
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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!");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
====
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user