/* * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.integration.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.Map; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Future; 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.FactoryBean; import org.springframework.core.convert.ConversionService; 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.Expression; import org.springframework.expression.common.LiteralExpression; import org.springframework.integration.annotation.Gateway; import org.springframework.integration.annotation.GatewayHeader; import org.springframework.integration.endpoint.AbstractEndpoint; import org.springframework.integration.support.channel.BeanFactoryChannelResolver; import org.springframework.integration.support.management.TrackableComponent; import org.springframework.integration.support.utils.IntegrationUtils; 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.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 ConversionService} set on the enclosing {@link BeanFactory} * under the name {@link 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 Long defaultRequestTimeout; private volatile Long 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; /** * 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, this value will be used. * * @param defaultRequestTimeout the timeout value in milliseconds */ public void setDefaultRequestTimeout(Long defaultRequestTimeout) { this.defaultRequestTimeout = defaultRequestTimeout; } /** * Set the default timeout value for receiving reply messages. If not * explicitly configured with an annotation, this value will be used. * * @param defaultReplyTimeout the timeout value in milliseconds */ public void setDefaultReplyTimeout(Long defaultReplyTimeout) { this.defaultReplyTimeout = 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(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 = new BeanFactoryChannelResolver(beanFactory); } Class<?> proxyInterface = this.determineServiceInterface(); Method[] methods = ReflectionUtils.getAllDeclaredMethods(proxyInterface); for (Method method : methods) { MethodInvocationGateway gateway = this.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.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() throws Exception { if (this.serviceProxy == null) { this.onInit(); Assert.notNull(this.serviceProxy, "failed to initialize proxy"); } return this.serviceProxy; } @Override public boolean isSingleton() { return true; } @Override public Object invoke(final MethodInvocation invocation) throws Throwable { 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 this.doInvoke(invocation, true); } protected Object doInvoke(MethodInvocation invocation, boolean runningOnCallerThread) throws Throwable { Method method = invocation.getMethod(); if (AopUtils.isToStringMethod(method)) { return "gateway proxy for service interface [" + this.serviceInterface + "]"; } try { return this.invokeGatewayMethod(invocation, runningOnCallerThread); } catch (Throwable e) { //NOSONAR - ok to catch, rethrown below this.rethrowExceptionCauseIfPossible(e, invocation.getMethod()); return null; // preceding call should always throw something } } private Object invokeGatewayMethod(MethodInvocation invocation, boolean runningOnCallerThread) throws Exception { 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 = method.isAnnotationPresent(Payload.class); if (!hasPayloadExpression && this.methodMetadataMap != null) { // check for the method metadata next GatewayMethodMetadata metadata = this.methodMetadataMap.get(method.getName()); hasPayloadExpression = (metadata != null) && StringUtils.hasText(metadata.getPayloadExpression()); } if (paramCount == 0 && !hasPayloadExpression) { if (shouldReply) { if (shouldReturnMessage) { return gateway.receiveMessage(); } response = gateway.receive(); } } else { Object[] args = invocation.getArguments(); if (shouldReply) { response = shouldReturnMessage ? gateway.sendAndReceiveMessage(args) : gateway.sendAndReceive(args); } else { gateway.send(args); response = null; } } return (response != null) ? this.convert(response, returnType) : null; } private void rethrowExceptionCauseIfPossible(Throwable originalException, Method method) throws Throwable { 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 && !(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; Long requestTimeout = this.defaultRequestTimeout; Long 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 = gatewayAnnotation.requestTimeout(); } if (replyTimeout == null || gatewayAnnotation.replyTimeout() != Long.MIN_VALUE) { replyTimeout = gatewayAnnotation.replyTimeout(); } if (payloadExpression == null || StringUtils.hasText(gatewayAnnotation.payloadExpression())) { payloadExpression = gatewayAnnotation.payloadExpression(); } 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)); } } } 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 = this.convert(reqTimeout, Long.class); } String repTimeout = methodMetadata.getReplyTimeout(); if (StringUtils.hasText(repTimeout)) { replyTimeout = this.convert(repTimeout, Long.class); } } } Map<String, Object> headers = null; // We don't want to eagerly resolve the error channel here Object errorChannel = this.errorChannel == null ? this.errorChannelName : this.errorChannel; if (errorChannel != null && method.getReturnType().equals(void.class)) { headers = new HashMap<>(); headers.put(MessageHeaders.ERROR_CHANNEL, errorChannel); } GatewayMethodInboundMessageMapper messageMapper = new GatewayMethodInboundMessageMapper(method, headerExpressions, this.globalMethodMetadata != null ? this.globalMethodMetadata.getHeaderExpressions() : null, headers, this.argsMapper, this.getMessageBuilderFactory()); if (StringUtils.hasText(payloadExpression)) { messageMapper.setPayloadExpression(payloadExpression); } messageMapper.setBeanFactory(this.getBeanFactory()); MethodInvocationGateway gateway = new MethodInvocationGateway(messageMapper); if (this.errorChannel != null) { gateway.setErrorChannel(this.errorChannel); } else if (StringUtils.hasText(this.errorChannelName)) { gateway.setErrorChannelName(this.errorChannelName); } if (this.getTaskScheduler() != null) { gateway.setTaskScheduler(this.getTaskScheduler()); } gateway.setBeanName(this.getComponentName()); if (StringUtils.hasText(requestChannelName)) { gateway.setRequestChannelName(requestChannelName); } else if (StringUtils.hasText(this.defaultRequestChannelName)) { gateway.setRequestChannelName(this.defaultRequestChannelName); } else { gateway.setRequestChannel(this.defaultRequestChannel); } if (StringUtils.hasText(replyChannelName)) { gateway.setReplyChannelName(replyChannelName); } else if (StringUtils.hasText(this.defaultReplyChannelName)) { gateway.setReplyChannelName(this.defaultReplyChannelName); } else { gateway.setReplyChannel(this.defaultReplyChannel); } if (requestTimeout == null) { gateway.setRequestTimeout(-1); } else { gateway.setRequestTimeout(requestTimeout); } if (replyTimeout == null) { gateway.setReplyTimeout(-1); } else { gateway.setReplyTimeout(replyTimeout); } if (this.getBeanFactory() != null) { gateway.setBeanFactory(this.getBeanFactory()); } gateway.setShouldTrack(this.shouldTrack); gateway.afterPropertiesSet(); return gateway; } // 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") 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 { MethodInvocationGateway(GatewayMethodInboundMessageMapper messageMapper) { this.setRequestMapper(messageMapper); } } 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); } } } }