org.springframework.integration.gateway.GatewayProxyFactoryBean.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.integration.gateway.GatewayProxyFactoryBean.java

Source

/*
 * Copyright 2002-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.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.function.Consumer;
import java.util.function.Supplier;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.SimpleTypeConverter;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.core.MethodParameter;
import org.springframework.core.task.AsyncListenableTaskExecutor;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.support.TaskExecutorAdapter;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.annotation.Gateway;
import org.springframework.integration.annotation.GatewayHeader;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.expression.ValueExpression;
import org.springframework.integration.support.DefaultMessageBuilderFactory;
import org.springframework.integration.support.channel.ChannelResolverUtils;
import org.springframework.integration.support.management.TrackableComponent;
import org.springframework.integration.util.JavaUtils;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.core.DestinationResolver;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import reactor.core.publisher.Mono;

/**
 * Generates a proxy for the provided service interface to enable interaction
 * with messaging components without application code being aware of them allowing
 * for POJO-style interaction.
 * This component is also aware of the
 * {@link org.springframework.core.convert.ConversionService} set on the enclosing {@link BeanFactory}
 * under the name
 * {@link org.springframework.integration.support.utils.IntegrationUtils#INTEGRATION_CONVERSION_SERVICE_BEAN_NAME}
 * to
 * perform type conversions when necessary (thanks to Jon Schneider's contribution and suggestion in INT-1230).
 *
 * @author Mark Fisher
 * @author Oleg Zhurakousky
 * @author Gary Russell
 * @author Artem Bilan
 */
public class GatewayProxyFactoryBean extends AbstractEndpoint
        implements TrackableComponent, FactoryBean<Object>, MethodInterceptor, BeanClassLoaderAware {

    private volatile Class<?> serviceInterface;

    private volatile MessageChannel defaultRequestChannel;

    private volatile String defaultRequestChannelName;

    private volatile MessageChannel defaultReplyChannel;

    private volatile String defaultReplyChannelName;

    private volatile MessageChannel errorChannel;

    private volatile String errorChannelName;

    private volatile Expression defaultRequestTimeout;

    private volatile Expression defaultReplyTimeout;

    private volatile DestinationResolver<MessageChannel> channelResolver;

    private volatile boolean shouldTrack = false;

    private volatile TypeConverter typeConverter = new SimpleTypeConverter();

    private volatile ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();

    private volatile Object serviceProxy;

    private final Map<Method, MethodInvocationGateway> gatewayMap = new HashMap<>();

    private volatile AsyncTaskExecutor asyncExecutor = new SimpleAsyncTaskExecutor();

    private volatile Class<?> asyncSubmitType;

    private volatile Class<?> asyncSubmitListenableType;

    private volatile boolean initialized;

    private final Object initializationMonitor = new Object();

    private volatile Map<String, GatewayMethodMetadata> methodMetadataMap;

    private volatile GatewayMethodMetadata globalMethodMetadata;

    private volatile MethodArgsMessageMapper argsMapper;

    private EvaluationContext evaluationContext = new StandardEvaluationContext();

    /**
     * Create a Factory whose service interface type can be configured by setter injection.
     * If none is set, it will fall back to the default service interface type,
     * {@link RequestReplyExchanger}, upon initialization.
     */
    public GatewayProxyFactoryBean() {
        // serviceInterface will be determined on demand later
    }

    public GatewayProxyFactoryBean(Class<?> serviceInterface) {
        Assert.notNull(serviceInterface, "'serviceInterface' must not be null");
        Assert.isTrue(serviceInterface.isInterface(), "'serviceInterface' must be an interface");
        this.serviceInterface = serviceInterface;
    }

    /**
     * Set the interface class that the generated proxy should implement.
     * If none is provided explicitly, the default is {@link RequestReplyExchanger}.
     *
     * @param serviceInterface The service interface.
     */
    public void setServiceInterface(Class<?> serviceInterface) {
        Assert.notNull(serviceInterface, "'serviceInterface' must not be null");
        Assert.isTrue(serviceInterface.isInterface(), "'serviceInterface' must be an interface");
        this.serviceInterface = serviceInterface;
    }

    /**
     * Set the default request channel.
     * @param defaultRequestChannel the channel to which request messages will
     * be sent if no request channel has been configured with an annotation.
     */
    public void setDefaultRequestChannel(MessageChannel defaultRequestChannel) {
        this.defaultRequestChannel = defaultRequestChannel;
    }

    /**
     * Set the default request channel bean name.
     * @param defaultRequestChannelName the channel name to which request messages will
     * be sent if no request channel has been configured with an annotation.
     * @since 4.2.9
     */
    public void setDefaultRequestChannelName(String defaultRequestChannelName) {
        this.defaultRequestChannelName = defaultRequestChannelName;
    }

    /**
     * Set the default reply channel. If no default reply channel is provided,
     * and no reply channel is configured with annotations, an anonymous,
     * temporary channel will be used for handling replies.
     * @param defaultReplyChannel the channel from which reply messages will be
     * received if no reply channel has been configured with an annotation
     */
    public void setDefaultReplyChannel(MessageChannel defaultReplyChannel) {
        this.defaultReplyChannel = defaultReplyChannel;
    }

    /**
     * Set the default reply channel bean name. If no default reply channel is provided,
     * and no reply channel is configured with annotations, an anonymous,
     * temporary channel will be used for handling replies.
     * @param defaultReplyChannelName the channel name from which reply messages will be
     * received if no reply channel has been configured with an annotation
     * @since 4.2.9
     */
    public void setDefaultReplyChannelName(String defaultReplyChannelName) {
        this.defaultReplyChannelName = defaultReplyChannelName;
    }

    /**
     * Set the error channel. If no error channel is provided, this gateway will
     * propagate Exceptions to the caller. To completely suppress Exceptions, provide
     * a reference to the "nullChannel" here.
     * @param errorChannel The error channel.
     */
    public void setErrorChannel(MessageChannel errorChannel) {
        this.errorChannel = errorChannel;
    }

    /**
     * Set the error channel name. If no error channel is provided, this gateway will
     * propagate Exceptions to the caller. To completely suppress Exceptions, provide
     * a reference to the "nullChannel" here.
     * @param errorChannelName The error channel bean name.
     * @since 4.2.9
     */
    public void setErrorChannelName(String errorChannelName) {
        this.errorChannelName = errorChannelName;
    }

    /**
     * Set the default timeout value for sending request messages. If not explicitly
     * configured with an annotation, or on a method element, this value will be used.
     *
     * @param defaultRequestTimeout the timeout value in milliseconds
     */
    public void setDefaultRequestTimeout(Long defaultRequestTimeout) {
        this.defaultRequestTimeout = new ValueExpression<>(defaultRequestTimeout);
    }

    /**
     * Set an expression to be evaluated to determine the default timeout value for
     * sending request messages. If not explicitly configured with an annotation, or on a
     * method element, this value will be used.
     *
     * @param defaultRequestTimeout the timeout value in milliseconds
     * @since 5.0
     */
    public void setDefaultRequestTimeoutExpression(Expression defaultRequestTimeout) {
        this.defaultRequestTimeout = defaultRequestTimeout;
    }

    /**
     * Set an expression to be evaluated to determine the default timeout value for
     * sending request messages. If not explicitly configured with an annotation, or on a
     * method element, this value will be used.
     *
     * @param defaultRequestTimeout the timeout value in milliseconds
     * @since 5.0
     */
    public void setDefaultRequestTimeoutExpressionString(String defaultRequestTimeout) {
        if (StringUtils.hasText(defaultRequestTimeout)) {
            this.defaultRequestTimeout = ExpressionUtils.longExpression(defaultRequestTimeout);
        }
    }

    /**
     * Set the default timeout value for receiving reply messages. If not explicitly
     * configured with an annotation, or on a method element, this value will be used.
     *
     * @param defaultReplyTimeout the timeout value in milliseconds
     */
    public void setDefaultReplyTimeout(Long defaultReplyTimeout) {
        this.defaultReplyTimeout = new ValueExpression<>(defaultReplyTimeout);
    }

    /**
     * Set an expression to be evaluated to determine the default timeout value for
     * receiving reply messages. If not explicitly configured with an annotation, or on a
     * method element, this value will be used.
     *
     * @param defaultReplyTimeout the timeout value in milliseconds
     * @since 5.0
     */
    public void setDefaultReplyTimeoutExpression(Expression defaultReplyTimeout) {
        this.defaultReplyTimeout = defaultReplyTimeout;
    }

    /**
     * Set an expression to be evaluated to determine the default timeout value for
     * receiving reply messages. If not explicitly configured with an annotation, or on a
     * method element, this value will be used.
     *
     * @param defaultReplyTimeout the timeout value in milliseconds
     * @since 5.0
     */
    public void setDefaultReplyTimeoutExpressionString(String defaultReplyTimeout) {
        if (StringUtils.hasText(defaultReplyTimeout)) {
            this.defaultReplyTimeout = ExpressionUtils.longExpression(defaultReplyTimeout);
        }
    }

    @Override
    public void setShouldTrack(boolean shouldTrack) {
        this.shouldTrack = shouldTrack;
        if (!CollectionUtils.isEmpty(this.gatewayMap)) {
            for (MethodInvocationGateway gateway : this.gatewayMap.values()) {
                gateway.setShouldTrack(shouldTrack);
            }
        }
    }

    /**
     * Set the executor for use when the gateway method returns
     * {@link java.util.concurrent.Future} or {@link org.springframework.util.concurrent.ListenableFuture}.
     * Set it to null to disable the async processing, and any
     * {@link java.util.concurrent.Future} return types must be returned by the downstream flow.
     * @param executor The executor.
     */
    public void setAsyncExecutor(@Nullable Executor executor) {
        if (executor == null && logger.isInfoEnabled()) {
            logger.info("A null executor disables the async gateway; "
                    + "methods returning Future<?> will run on the calling thread");
        }
        this.asyncExecutor = (executor instanceof AsyncTaskExecutor || executor == null)
                ? (AsyncTaskExecutor) executor
                : new TaskExecutorAdapter(executor);
    }

    public void setTypeConverter(TypeConverter typeConverter) {
        Assert.notNull(typeConverter, "typeConverter must not be null");
        this.typeConverter = typeConverter;
    }

    public void setMethodMetadataMap(Map<String, GatewayMethodMetadata> methodMetadataMap) {
        this.methodMetadataMap = methodMetadataMap;
    }

    public void setGlobalMethodMetadata(GatewayMethodMetadata globalMethodMetadata) {
        this.globalMethodMetadata = globalMethodMetadata;
    }

    @Override
    public void setBeanClassLoader(ClassLoader beanClassLoader) {
        this.beanClassLoader = beanClassLoader;
    }

    /**
     * Provide a custom {@link MethodArgsMessageMapper} to map from a {@link MethodArgsHolder}
     * to a {@link Message}.
     * @param mapper the mapper.
     */
    public final void setMapper(MethodArgsMessageMapper mapper) {
        this.argsMapper = mapper;
    }

    protected AsyncTaskExecutor getAsyncExecutor() {
        return this.asyncExecutor;
    }

    /**
     * Return the Map of {@link Method} to {@link MessagingGatewaySupport}
     * generated by this factory bean.
     * @return the map.
     * @since 4.3
     */
    public Map<Method, MessagingGatewaySupport> getGateways() {
        return Collections.unmodifiableMap(this.gatewayMap);
    }

    @Override
    protected void onInit() {
        synchronized (this.initializationMonitor) {
            if (this.initialized) {
                return;
            }
            BeanFactory beanFactory = this.getBeanFactory();
            if (this.channelResolver == null && beanFactory != null) {
                this.channelResolver = ChannelResolverUtils.getChannelResolver(beanFactory);
            }
            Class<?> proxyInterface = determineServiceInterface();
            Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(proxyInterface);
            for (Method method : methods) {
                MethodInvocationGateway gateway = createGatewayForMethod(method);
                this.gatewayMap.put(method, gateway);
            }
            this.serviceProxy = new ProxyFactory(proxyInterface, this).getProxy(this.beanClassLoader);
            if (this.asyncExecutor != null) {
                Callable<String> task = () -> null;
                Future<String> submitType = this.asyncExecutor.submit(task);
                this.asyncSubmitType = submitType.getClass();
                if (this.asyncExecutor instanceof AsyncListenableTaskExecutor) {
                    submitType = ((AsyncListenableTaskExecutor) this.asyncExecutor).submitListenable(task);
                    this.asyncSubmitListenableType = submitType.getClass();
                }
            }
            this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
            this.initialized = true;
        }
    }

    private Class<?> determineServiceInterface() {
        if (this.serviceInterface == null) {
            this.serviceInterface = RequestReplyExchanger.class;
        }
        return this.serviceInterface;
    }

    @Override
    public Class<?> getObjectType() {
        return (this.serviceInterface != null ? this.serviceInterface : null);
    }

    @Override
    public Object getObject() {
        if (this.serviceProxy == null) {
            this.onInit();
            Assert.notNull(this.serviceProxy, "failed to initialize proxy");
        }
        return this.serviceProxy;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

    @Override
    @Nullable
    public Object invoke(final MethodInvocation invocation) throws Throwable { // NOSONAR
        final Class<?> returnType = invocation.getMethod().getReturnType();
        if (this.asyncExecutor != null && !Object.class.equals(returnType)) {
            Invoker invoker = new Invoker(invocation);
            if (returnType.isAssignableFrom(this.asyncSubmitType)) {
                return this.asyncExecutor.submit(invoker::get);
            } else if (returnType.isAssignableFrom(this.asyncSubmitListenableType)) {
                return ((AsyncListenableTaskExecutor) this.asyncExecutor).submitListenable(invoker::get);
            } else if (CompletableFuture.class.equals(returnType)) { // exact
                return CompletableFuture.supplyAsync(invoker, this.asyncExecutor);
            } else if (Future.class.isAssignableFrom(returnType)) {
                if (logger.isDebugEnabled()) {
                    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: "
                            + returnType.getSimpleName());
                }
            }
        }
        if (Mono.class.isAssignableFrom(returnType)) {
            return Mono.fromSupplier(new Invoker(invocation));
        }
        return doInvoke(invocation, true);
    }

    @Nullable
    protected Object doInvoke(MethodInvocation invocation, boolean runningOnCallerThread) throws Throwable { // NOSONAR
        Method method = invocation.getMethod();
        if (AopUtils.isToStringMethod(method)) {
            return "gateway proxy for service interface [" + this.serviceInterface + "]";
        }
        try {
            return invokeGatewayMethod(invocation, runningOnCallerThread);
        } catch (Throwable e) { //NOSONAR - ok to catch, rethrown below
            rethrowExceptionCauseIfPossible(e, invocation.getMethod());
            return null; // preceding call should always throw something
        }
    }

    @Nullable
    private Object invokeGatewayMethod(MethodInvocation invocation, boolean runningOnCallerThread) {
        if (!this.initialized) {
            this.afterPropertiesSet();
        }
        Method method = invocation.getMethod();
        MethodInvocationGateway gateway = this.gatewayMap.get(method);
        Class<?> returnType = method.getReturnType();
        boolean shouldReturnMessage = Message.class.isAssignableFrom(returnType)
                || hasReturnParameterizedWithMessage(method, runningOnCallerThread);
        boolean shouldReply = returnType != void.class;
        int paramCount = method.getParameterTypes().length;
        Object response = null;
        boolean hasPayloadExpression = findPayloadExpression(method);
        if (paramCount == 0 && !hasPayloadExpression) {
            Long receiveTimeout = null;
            if (gateway.getReceiveTimeoutExpression() != null) {
                receiveTimeout = gateway.getReceiveTimeoutExpression().getValue(this.evaluationContext, Long.class);
            }
            if (shouldReply) {
                if (shouldReturnMessage) {
                    if (receiveTimeout != null) {
                        return gateway.receiveMessage(receiveTimeout);
                    } else {
                        return gateway.receiveMessage();
                    }
                }
                if (receiveTimeout != null) {
                    response = gateway.receive(receiveTimeout);
                } else {
                    response = gateway.receive();
                }
            }
        } else {
            response = sendOrSendAndReceive(invocation, gateway, shouldReturnMessage, shouldReply);
        }
        return response(returnType, shouldReturnMessage, response);
    }

    @Nullable
    private Object response(Class<?> returnType, boolean shouldReturnMessage, @Nullable Object response) {
        if (shouldReturnMessage) {
            return response;
        } else {
            return response != null ? convert(response, returnType) : null;
        }
    }

    private boolean findPayloadExpression(Method method) {
        boolean hasPayloadExpression = method.isAnnotationPresent(Payload.class);
        if (!hasPayloadExpression) {
            // check for the method metadata next
            if (this.methodMetadataMap != null) {
                GatewayMethodMetadata metadata = this.methodMetadataMap.get(method.getName());
                hasPayloadExpression = (metadata != null) && StringUtils.hasText(metadata.getPayloadExpression());
            } else if (this.globalMethodMetadata != null) {
                hasPayloadExpression = StringUtils.hasText(this.globalMethodMetadata.getPayloadExpression());
            }
        }
        return hasPayloadExpression;
    }

    @Nullable
    private Object sendOrSendAndReceive(MethodInvocation invocation, MethodInvocationGateway gateway,
            boolean shouldReturnMessage, boolean shouldReply) {
        Object response;
        Object[] args = invocation.getArguments();
        if (shouldReply) {
            response = shouldReturnMessage ? gateway.sendAndReceiveMessage(args) : gateway.sendAndReceive(args);
        } else {
            gateway.send(args);
            response = null;
        }
        return response;
    }

    private void rethrowExceptionCauseIfPossible(Throwable originalException, Method method) throws Throwable { // NOSONAR
        Class<?>[] exceptionTypes = method.getExceptionTypes();
        Throwable t = originalException;
        while (t != null) {
            for (Class<?> exceptionType : exceptionTypes) {
                if (exceptionType.isAssignableFrom(t.getClass())) {
                    throw t;
                }
            }
            if (t instanceof RuntimeException // NOSONAR boolean complexity
                    && !(t instanceof MessagingException) && !(t instanceof UndeclaredThrowableException)
                    && !(t instanceof IllegalStateException
                            && "Unexpected exception thrown".equals(t.getMessage()))) {
                throw t;
            }
            t = t.getCause();
        }
        throw originalException;
    }

    private MethodInvocationGateway createGatewayForMethod(Method method) {
        Gateway gatewayAnnotation = method.getAnnotation(Gateway.class);
        String requestChannelName = null;
        String replyChannelName = null;
        Expression requestTimeout = this.defaultRequestTimeout;
        Expression replyTimeout = this.defaultReplyTimeout;
        String payloadExpression = this.globalMethodMetadata != null
                ? this.globalMethodMetadata.getPayloadExpression()
                : null;
        Map<String, Expression> headerExpressions = new HashMap<String, Expression>();
        if (gatewayAnnotation != null) {
            requestChannelName = gatewayAnnotation.requestChannel();
            replyChannelName = gatewayAnnotation.replyChannel();
            /*
             * INT-2636 Unspecified annotation attributes should not
             * override the default values supplied by explicit configuration.
             * There is a small risk that someone has used Long.MIN_VALUE explicitly
             * to indicate an indefinite timeout on a gateway method and that will
             * no longer work as expected; they will need to use, say, -1 instead.
             */
            if (requestTimeout == null || gatewayAnnotation.requestTimeout() != Long.MIN_VALUE) {
                requestTimeout = new ValueExpression<>(gatewayAnnotation.requestTimeout());
            }
            if (StringUtils.hasText(gatewayAnnotation.requestTimeoutExpression())) {
                requestTimeout = ExpressionUtils.longExpression(gatewayAnnotation.requestTimeoutExpression());
            }
            if (replyTimeout == null || gatewayAnnotation.replyTimeout() != Long.MIN_VALUE) {
                replyTimeout = new ValueExpression<>(gatewayAnnotation.replyTimeout());
            }
            if (StringUtils.hasText(gatewayAnnotation.replyTimeoutExpression())) {
                replyTimeout = ExpressionUtils.longExpression(gatewayAnnotation.replyTimeoutExpression());
            }
            if (payloadExpression == null || StringUtils.hasText(gatewayAnnotation.payloadExpression())) {
                payloadExpression = gatewayAnnotation.payloadExpression();
            }

            annotationHeaders(gatewayAnnotation, headerExpressions);
        } else if (this.methodMetadataMap != null && this.methodMetadataMap.size() > 0) {
            GatewayMethodMetadata methodMetadata = this.methodMetadataMap.get(method.getName());
            if (methodMetadata != null) {
                if (StringUtils.hasText(methodMetadata.getPayloadExpression())) {
                    payloadExpression = methodMetadata.getPayloadExpression();
                }
                if (!CollectionUtils.isEmpty(methodMetadata.getHeaderExpressions())) {
                    headerExpressions.putAll(methodMetadata.getHeaderExpressions());
                }
                requestChannelName = methodMetadata.getRequestChannelName();
                replyChannelName = methodMetadata.getReplyChannelName();
                String reqTimeout = methodMetadata.getRequestTimeout();
                if (StringUtils.hasText(reqTimeout)) {
                    requestTimeout = ExpressionUtils.longExpression(reqTimeout);
                }
                String repTimeout = methodMetadata.getReplyTimeout();
                if (StringUtils.hasText(repTimeout)) {
                    replyTimeout = ExpressionUtils.longExpression(repTimeout);
                }
            }
        }
        Map<String, Object> headers = headers(method, headerExpressions);

        GatewayMethodInboundMessageMapper messageMapper = new GatewayMethodInboundMessageMapper(method,
                headerExpressions,
                this.globalMethodMetadata != null ? this.globalMethodMetadata.getHeaderExpressions() : null,
                headers, this.argsMapper, this.getMessageBuilderFactory());
        MethodInvocationGateway gateway = new MethodInvocationGateway(messageMapper);

        JavaUtils.INSTANCE.acceptIfHasText(payloadExpression, messageMapper::setPayloadExpression)
                .acceptIfNotNull(getTaskScheduler(), gateway::setTaskScheduler);
        gateway.setBeanName(this.getComponentName());

        setChannel(this.errorChannel, gateway::setErrorChannel, this.errorChannelName,
                gateway::setErrorChannelName);
        setChannel(requestChannelName, this.defaultRequestChannelName, gateway::setRequestChannelName,
                this.defaultRequestChannel, gateway::setRequestChannel);
        setChannel(replyChannelName, this.defaultReplyChannelName, gateway::setReplyChannelName,
                this.defaultReplyChannel, gateway::setReplyChannel);

        timeouts(requestTimeout, replyTimeout, messageMapper, gateway);
        BeanFactory beanFactory = this.getBeanFactory();
        if (beanFactory != null) {
            gateway.setBeanFactory(beanFactory);
            messageMapper.setBeanFactory(beanFactory);
        }
        gateway.setShouldTrack(this.shouldTrack);
        gateway.afterPropertiesSet();
        return gateway;
    }

    private void annotationHeaders(Gateway gatewayAnnotation, Map<String, Expression> headerExpressions) {
        if (!ObjectUtils.isEmpty(gatewayAnnotation.headers())) {
            for (GatewayHeader gatewayHeader : gatewayAnnotation.headers()) {
                String value = gatewayHeader.value();
                String expression = gatewayHeader.expression();
                String name = gatewayHeader.name();
                boolean hasValue = StringUtils.hasText(value);

                if (hasValue == StringUtils.hasText(expression)) {
                    throw new BeanDefinitionStoreException(
                            "exactly one of 'value' or 'expression' " + "is required on a gateway's header.");
                }
                headerExpressions.put(name,
                        hasValue ? new LiteralExpression(value) : EXPRESSION_PARSER.parseExpression(expression));
            }
        }
    }

    @Nullable
    private Map<String, Object> headers(Method method, Map<String, Expression> headerExpressions) {
        Map<String, Object> headers = null;
        // We don't want to eagerly resolve the error channel here
        Object errorChannelForVoidReturn = this.errorChannel == null ? this.errorChannelName : this.errorChannel;
        if (errorChannelForVoidReturn != null && method.getReturnType().equals(void.class)) {
            headers = new HashMap<>();
            headers.put(MessageHeaders.ERROR_CHANNEL, errorChannelForVoidReturn);
        }

        if (getMessageBuilderFactory() instanceof DefaultMessageBuilderFactory) {
            Set<String> headerNames = new HashSet<>(headerExpressions.keySet());

            if (this.globalMethodMetadata != null) {
                headerNames.addAll(this.globalMethodMetadata.getHeaderExpressions().keySet());
            }

            List<MethodParameter> methodParameters = GatewayMethodInboundMessageMapper
                    .getMethodParameterList(method);

            for (MethodParameter methodParameter : methodParameters) {
                Header header = methodParameter.getParameterAnnotation(Header.class);
                if (header != null) {
                    String headerName = GatewayMethodInboundMessageMapper.determineHeaderName(header,
                            methodParameter);
                    headerNames.add(headerName);
                }
            }

            validateHeaders(headerNames);
        }
        return headers;
    }

    private void validateHeaders(Set<String> headerNames) {
        for (String header : headerNames) {
            if ((MessageHeaders.ID.equals(header) || MessageHeaders.TIMESTAMP.equals(header))) {
                throw new BeanInitializationException(
                        "Messaging Gateway cannot override 'id' and 'timestamp' read-only headers.\n"
                                + "Wrong headers configuration for " + getComponentName());
            }
        }
    }

    private void timeouts(Expression requestTimeout, Expression replyTimeout,
            GatewayMethodInboundMessageMapper messageMapper, MethodInvocationGateway gateway) {
        if (requestTimeout == null) {
            gateway.setRequestTimeout(-1);
        } else if (requestTimeout instanceof ValueExpression) {
            Long timeout = requestTimeout.getValue(Long.class);
            if (timeout != null) {
                gateway.setRequestTimeout(timeout);
            }
        } else {
            messageMapper.setSendTimeoutExpression(requestTimeout);
        }
        if (replyTimeout == null) {
            gateway.setReplyTimeout(-1);
        } else if (replyTimeout instanceof ValueExpression) {
            Long timeout = replyTimeout.getValue(Long.class);
            if (timeout != null) {
                gateway.setReplyTimeout(timeout);
            }
        } else {
            messageMapper.setReplyTimeoutExpression(replyTimeout);
        }
        if (replyTimeout != null) {
            gateway.setReceiveTimeoutExpression(replyTimeout);
        }
    }

    private void setChannel(MessageChannel channel, Consumer<MessageChannel> channelMethod, String channelName,
            Consumer<String> channelNameMethod) {
        if (channel != null) {
            channelMethod.accept(channel);
        } else if (StringUtils.hasText(channelName)) {
            channelNameMethod.accept(channelName);
        }
    }

    private void setChannel(String channelName1, String channelName2, Consumer<String> channelNameMethod,
            MessageChannel channel, Consumer<MessageChannel> channelMethod) {

        if (StringUtils.hasText(channelName1)) {
            channelNameMethod.accept(channelName1);
        } else if (StringUtils.hasText(channelName2)) {
            channelNameMethod.accept(channelName2);
        } else {
            channelMethod.accept(channel);
        }
    }

    // Lifecycle implementation

    @Override // guarded by super#lifecycleLock
    protected void doStart() {
        for (MethodInvocationGateway gateway : this.gatewayMap.values()) {
            gateway.start();
        }
    }

    @Override // guarded by super#lifecycleLock
    protected void doStop() {
        for (MethodInvocationGateway gateway : this.gatewayMap.values()) {
            gateway.stop();
        }
    }

    @SuppressWarnings("unchecked")
    @Nullable
    private <T> T convert(Object source, Class<T> expectedReturnType) {
        if (Future.class.isAssignableFrom(expectedReturnType)) {
            return (T) source;
        }
        if (Mono.class.isAssignableFrom(expectedReturnType)) {
            return (T) source;
        }
        if (getConversionService() != null) {
            return getConversionService().convert(source, expectedReturnType);
        } else {
            return this.typeConverter.convertIfNecessary(source, expectedReturnType);
        }
    }

    private static boolean hasReturnParameterizedWithMessage(Method method, boolean runningOnCallerThread) {
        if (!runningOnCallerThread && (Future.class.isAssignableFrom(method.getReturnType())
                || Mono.class.isAssignableFrom(method.getReturnType()))) {
            Type returnType = method.getGenericReturnType();
            if (returnType instanceof ParameterizedType) {
                Type[] typeArgs = ((ParameterizedType) returnType).getActualTypeArguments();
                if (typeArgs != null && typeArgs.length == 1) {
                    Type parameterizedType = typeArgs[0];
                    if (parameterizedType instanceof ParameterizedType) {
                        Type rawType = ((ParameterizedType) parameterizedType).getRawType();
                        if (rawType instanceof Class) {
                            return Message.class.isAssignableFrom((Class<?>) rawType);
                        }
                    }
                }
            }
        }
        return false;
    }

    private static final class MethodInvocationGateway extends MessagingGatewaySupport {

        private Expression receiveTimeoutExpression;

        MethodInvocationGateway(GatewayMethodInboundMessageMapper messageMapper) {
            setRequestMapper(messageMapper);
        }

        Expression getReceiveTimeoutExpression() {
            return this.receiveTimeoutExpression;
        }

        void setReceiveTimeoutExpression(Expression receiveTimeoutExpression) {
            this.receiveTimeoutExpression = receiveTimeoutExpression;
        }

    }

    private final class Invoker implements Supplier<Object> {

        private final MethodInvocation invocation;

        Invoker(MethodInvocation methodInvocation) {
            this.invocation = methodInvocation;
        }

        @Override
        public Object get() {
            try {
                return doInvoke(this.invocation, false);
            } catch (Error e) { //NOSONAR
                throw e;
            } catch (Throwable t) { //NOSONAR
                if (t instanceof RuntimeException) {
                    throw (RuntimeException) t;
                }
                throw new MessagingException("Asynchronous gateway invocation failed", t);
            }
        }

    }

}