New abstraction for StreamListener method setup invoker.

StreamListenerMethodSetupOrchestrator is an API hook that allows
downstram binder implementations or applications to inject custom
strategies to alter the default StreamListener adapter method invocations.

This PR primarily focuses on allowing customizations on the oubound side.
For the inbound, StreamListenerMethodSetupOrchestrator interface provides
a default implementation.

Corresponding refactoring in StreamListenerAnnotationBeanPostProcessor.
By default StreamListenerAnnotationBeanPostProcessor will use the current
strategies used for invoking the StreamListener adapters. If beans are provided
for orchestration, they take precedence however.

Test changes.

Further polishing in StreamListenerAnnotationBeanPostProcessor.

Resolves #1177
Resolves #1179
This commit is contained in:
Soby Chacko
2018-01-10 13:51:59 -05:00
committed by Oleg Zhurakousky
parent 9ef1181f7a
commit c1749ca866
3 changed files with 445 additions and 173 deletions

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2018 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.cloud.stream.config;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binding.StreamListenerAnnotationBeanPostProcessor;
import org.springframework.cloud.stream.binding.StreamListenerSetupMethodOrchestrator;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* @author Soby Chacko
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class StreamListenerMethodSetupOrchestratorTests {
@SpyBean
CustomOrchestrator customOrchestrator;
@SpyBean
MultipleStreamListenerProcessor multipleStreamListenerProcessor;
@SpyBean
StreamListenerAnnotationBeanPostProcessor streamListenerAnnotationBeanPostProcessor;
@Test
@SuppressWarnings("unchecked")
public void testCustomStreamListenerOrchestratorAndDefaultTogetherInSameContext() throws Exception {
//Two StreamListener methods, so 2 invocations
verify(customOrchestrator, times(2)).supports(any());
Method method = multipleStreamListenerProcessor.getClass().getMethod("handleMessage");
StreamListener streamListener = AnnotatedElementUtils.findMergedAnnotation(method, StreamListener.class);
//verify that the invocation happened on the custom Orchestrator
verify(customOrchestrator).orchestrateStreamListenerSetupMethod(streamListener, method, multipleStreamListenerProcessor);
Method method1 = multipleStreamListenerProcessor.getClass().getMethod("produceString");
StreamListener streamListener1 = AnnotatedElementUtils.findMergedAnnotation(method, StreamListener.class);
//Verify that the invocation did not happen on the custom orchestrator
verify(customOrchestrator, never()).orchestrateStreamListenerSetupMethod(streamListener1, method1, multipleStreamListenerProcessor);
Field field = ReflectionUtils.findField(streamListenerAnnotationBeanPostProcessor.getClass(), "streamListenerSetupMethodOrchestrators");
ReflectionUtils.makeAccessible(field);
Set<StreamListenerSetupMethodOrchestrator> field1 =
(LinkedHashSet<StreamListenerSetupMethodOrchestrator>)ReflectionUtils.getField(field, streamListenerAnnotationBeanPostProcessor);
List<StreamListenerSetupMethodOrchestrator> list = new ArrayList<>(field1);
//Ensure that the custom orchestrator did not support this request
assertThat(list.get(0).supports(method1)).isEqualTo(false);
//Ensure that we are using the default Orchestrator in StreamListenerAnnoatationBeanPostProcessor
assertThat(list.get(1).supports(method1)).isEqualTo(true);
}
public interface SomeProcessor {
@Input(Sink.INPUT)
SubscribableChannel channel1();
@Input("foobar")
SubscribableChannel channel2();
@Output(Source.OUTPUT)
MessageChannel channel3();
}
@EnableBinding(SomeProcessor.class)
@EnableAutoConfiguration
public static class MultipleStreamListenerProcessor {
@StreamListener(Sink.INPUT)
public void handleMessage() {
}
@StreamListener("foobar")
@SendTo("output")
public String produceString(){
return "foobar";
}
@Bean
public CustomOrchestrator myOrchestrator() {
return new CustomOrchestrator();
}
}
static class CustomOrchestrator implements StreamListenerSetupMethodOrchestrator {
@Override
public boolean supports(Method method) {
return method.getReturnType() != String.class;
}
@Override
public void orchestrateStreamListenerSetupMethod(StreamListener streamListener, Method method, Object bean) {
//stub method
}
}
}

View File

@@ -20,12 +20,12 @@ import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import reactor.core.publisher.Flux;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
@@ -76,12 +76,6 @@ public class StreamListenerAnnotationBeanPostProcessor implements BeanPostProces
private final MultiValueMap<String, StreamListenerHandlerMethodMapping> mappedListenerMethods = new LinkedMultiValueMap<>();
// == dependencies that are injected in 'afterSingletonsInstantiated' to avoid early initialization
@SuppressWarnings("rawtypes")
private Collection<StreamListenerParameterAdapter> streamListenerParameterAdapters;
@SuppressWarnings("rawtypes")
private Collection<StreamListenerResultAdapter> streamListenerResultAdapters;
private DestinationResolver<MessageChannel> binderAwareChannelResolver;
private MessageHandlerMethodFactory messageHandlerMethodFactory;
@@ -93,12 +87,12 @@ public class StreamListenerAnnotationBeanPostProcessor implements BeanPostProces
private ConfigurableApplicationContext applicationContext;
private EvaluationContext evaluationContext;
private BeanExpressionResolver resolver;
private BeanExpressionContext expressionContext;
private Set<StreamListenerSetupMethodOrchestrator> streamListenerSetupMethodOrchestrators = new LinkedHashSet<>();
@Override
public final void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = (ConfigurableApplicationContext) applicationContext;
@@ -109,7 +103,7 @@ public class StreamListenerAnnotationBeanPostProcessor implements BeanPostProces
@Override
public final void afterSingletonsInstantiated() {
this.injectAndPostProcessDependencies();
this.evaluationContext = IntegrationContextUtils.getEvaluationContext(this.applicationContext.getBeanFactory());
EvaluationContext evaluationContext = IntegrationContextUtils.getEvaluationContext(this.applicationContext.getBeanFactory());
for (Map.Entry<String, List<StreamListenerHandlerMethodMapping>> mappedBindingEntry : mappedListenerMethods
.entrySet()) {
ArrayList<DispatchingStreamListenerMessageHandler.ConditionalStreamListenerMessageHandlerWrapper> handlers = new ArrayList<>();
@@ -147,7 +141,7 @@ public class StreamListenerAnnotationBeanPostProcessor implements BeanPostProces
AbstractReplyProducingMessageHandler handler;
if (handlers.size() > 1 || handlers.get(0).getCondition() != null) {
handler = new DispatchingStreamListenerMessageHandler(handlers, this.evaluationContext);
handler = new DispatchingStreamListenerMessageHandler(handlers, evaluationContext);
}
else {
handler = handlers.get(0).getStreamListenerMessageHandler();
@@ -177,26 +171,6 @@ public class StreamListenerAnnotationBeanPostProcessor implements BeanPostProces
return bean;
}
protected final void registerHandlerMethodOnListenedChannel(Method method, StreamListener streamListener, Object bean) {
Assert.hasText(streamListener.value(), "The binding name cannot be null");
if (!StringUtils.hasText(streamListener.value())) {
throw new BeanInitializationException("A bound component name must be specified");
}
final String defaultOutputChannel = StreamListenerMethodUtils.getOutboundBindingTargetName(method);
if (Void.TYPE.equals(method.getReturnType())) {
Assert.isTrue(StringUtils.isEmpty(defaultOutputChannel),
"An output channel cannot be specified for a method that does not return a value");
}
else {
Assert.isTrue(!StringUtils.isEmpty(defaultOutputChannel),
"An output channel must be specified for a method that can return a value");
}
StreamListenerMethodUtils.validateStreamListenerMessageHandler(method);
mappedListenerMethods.add(streamListener.value(),
new StreamListenerHandlerMethodMapping(bean, method, streamListener.condition(), defaultOutputChannel,
streamListener.copyHeaders()));
}
/**
* Extension point, allowing subclasses to customize the {@link StreamListener}
* annotation detected by the postprocessor.
@@ -209,145 +183,16 @@ public class StreamListenerAnnotationBeanPostProcessor implements BeanPostProces
return originalAnnotation;
}
private boolean checkDeclarativeMethod(Method method, String methodAnnotatedInboundName, String methodAnnotatedOutboundName) {
int methodArgumentsLength = method.getParameterTypes().length;
for (int parameterIndex = 0; parameterIndex < methodArgumentsLength; parameterIndex++) {
MethodParameter methodParameter = MethodParameter.forExecutable(method, parameterIndex);
if (methodParameter.hasParameterAnnotation(Input.class)) {
String inboundName = (String) AnnotationUtils
.getValue(methodParameter.getParameterAnnotation(Input.class));
Assert.isTrue(StringUtils.hasText(inboundName), StreamListenerErrorMessages.INVALID_INBOUND_NAME);
Assert.isTrue(isDeclarativeMethodParameter(inboundName, methodParameter),
StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS);
return true;
}
else if (methodParameter.hasParameterAnnotation(Output.class)) {
String outboundName = (String) AnnotationUtils
.getValue(methodParameter.getParameterAnnotation(Output.class));
Assert.isTrue(StringUtils.hasText(outboundName), StreamListenerErrorMessages.INVALID_OUTBOUND_NAME);
Assert.isTrue(isDeclarativeMethodParameter(outboundName, methodParameter),
StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS);
return true;
}
else if (StringUtils.hasText(methodAnnotatedOutboundName)) {
return isDeclarativeMethodParameter(methodAnnotatedOutboundName, methodParameter);
}
else if (StringUtils.hasText(methodAnnotatedInboundName)) {
return isDeclarativeMethodParameter(methodAnnotatedInboundName, methodParameter);
}
}
return false;
}
/**
* Determines if method parameters signify an imperative or declarative listener definition.
* <br>
* Imperative - where handler method is invoked on each message by the handler infrastructure provided
* by the framework
* <br>
* Declarative - where handler is provided by the method itself.
* <br>
* Declarative method parameter could either be {@link MessageChannel} or any other Object for which
* there is a {@link StreamListenerParameterAdapter} (i.e., {@link Flux}). Declarative method is invoked only
* once during initialization phase.
*/
@SuppressWarnings("unchecked")
private boolean isDeclarativeMethodParameter(String targetBeanName, MethodParameter methodParameter) {
boolean declarative = false;
if (!methodParameter.getParameterType().isAssignableFrom(Object.class) && this.applicationContext.containsBean(targetBeanName)) {
declarative = MessageChannel.class.isAssignableFrom(methodParameter.getParameterType());
if (!declarative) {
Class<?> targetBeanClass = this.applicationContext.getType(targetBeanName);
declarative = this.streamListenerParameterAdapters.stream()
.filter(slpa -> slpa.supports(targetBeanClass, methodParameter)).findFirst().isPresent();
}
}
return declarative;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void invokeSetupMethodOnListenedChannel(Method method, Object bean, String inboundName, String outboundName) {
Object[] arguments = new Object[method.getParameterTypes().length];
for (int parameterIndex = 0; parameterIndex < arguments.length; parameterIndex++) {
MethodParameter methodParameter = MethodParameter.forExecutable(method, parameterIndex);
Class<?> parameterType = methodParameter.getParameterType();
Object targetReferenceValue = null;
if (methodParameter.hasParameterAnnotation(Input.class)) {
targetReferenceValue = AnnotationUtils.getValue(methodParameter.getParameterAnnotation(Input.class));
}
else if (methodParameter.hasParameterAnnotation(Output.class)) {
targetReferenceValue = AnnotationUtils.getValue(methodParameter.getParameterAnnotation(Output.class));
}
else if (arguments.length == 1 && StringUtils.hasText(inboundName)) {
targetReferenceValue = inboundName;
}
if (targetReferenceValue != null) {
Assert.isInstanceOf(String.class, targetReferenceValue, "Annotation value must be a String");
Object targetBean = this.applicationContext.getBean((String) targetReferenceValue);
// Iterate existing parameter adapters first
for (StreamListenerParameterAdapter streamListenerParameterAdapter : this.streamListenerParameterAdapters) {
if (streamListenerParameterAdapter.supports(targetBean.getClass(), methodParameter)) {
arguments[parameterIndex] = streamListenerParameterAdapter.adapt(targetBean, methodParameter);
break;
}
}
if (arguments[parameterIndex] == null && parameterType.isAssignableFrom(targetBean.getClass())) {
arguments[parameterIndex] = targetBean;
}
Assert.notNull(arguments[parameterIndex], "Cannot convert argument " + parameterIndex + " of " + method
+ "from " + targetBean.getClass() + " to " + parameterType);
}
else {
throw new IllegalStateException(StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS);
}
}
try {
if (Void.TYPE.equals(method.getReturnType())) {
method.invoke(bean, arguments);
}
else {
Object result = method.invoke(bean, arguments);
if (!StringUtils.hasText(outboundName)) {
for (int parameterIndex = 0; parameterIndex < method.getParameterTypes().length; parameterIndex++) {
MethodParameter methodParameter = MethodParameter.forExecutable(method, parameterIndex);
if (methodParameter.hasParameterAnnotation(Output.class)) {
outboundName = methodParameter.getParameterAnnotation(Output.class).value();
}
}
}
Object targetBean = this.applicationContext.getBean(outboundName);
for (StreamListenerResultAdapter streamListenerResultAdapter : this.streamListenerResultAdapters) {
if (streamListenerResultAdapter.supports(result.getClass(), targetBean.getClass())) {
streamListenerResultAdapter.adapt(result, targetBean);
break;
}
}
}
}
catch (Exception e) {
throw new BeanInitializationException("Cannot setup StreamListener for " + method, e);
}
}
private void doPostProcess(StreamListener streamListener, Method method, Object bean) {
streamListener = postProcessAnnotation(streamListener, method);
String methodAnnotatedInboundName = streamListener.value();
String methodAnnotatedOutboundName = StreamListenerMethodUtils.getOutboundBindingTargetName(method);
int inputAnnotationCount = StreamListenerMethodUtils.inputAnnotationCount(method);
int outputAnnotationCount = StreamListenerMethodUtils.outputAnnotationCount(method);
boolean isDeclarative = checkDeclarativeMethod(method, methodAnnotatedInboundName, methodAnnotatedOutboundName);
StreamListenerMethodUtils.validateStreamListenerMethod(method,
inputAnnotationCount, outputAnnotationCount,
methodAnnotatedInboundName, methodAnnotatedOutboundName,
isDeclarative, streamListener.condition());
if (isDeclarative) {
invokeSetupMethodOnListenedChannel(method, bean, methodAnnotatedInboundName, methodAnnotatedOutboundName);
}
else {
registerHandlerMethodOnListenedChannel(method, streamListener, bean);
}
Optional<StreamListenerSetupMethodOrchestrator> streamListenerSetupMethodOrchestratorAvailable =
streamListenerSetupMethodOrchestrators.stream()
.filter(t -> t.supports(method))
.findFirst();
Assert.isTrue(streamListenerSetupMethodOrchestratorAvailable.isPresent(),
"A matching StreamListenerSetupMethodOrchestrator must be present");
StreamListenerSetupMethodOrchestrator streamListenerSetupMethodOrchestrator = streamListenerSetupMethodOrchestratorAvailable.get();
streamListenerSetupMethodOrchestrator.orchestrateStreamListenerSetupMethod(streamListener, method, bean);
}
private Method checkProxy(Method methodArg, Object bean) {
@@ -420,15 +265,24 @@ public class StreamListenerAnnotationBeanPostProcessor implements BeanPostProces
}
/**
* This operations ensures that required dependencies are not accidently injected early given that this bean is BPP.
* This operations ensures that required dependencies are not accidentally injected early given that this bean is BPP.
*/
@SuppressWarnings("unchecked")
private void injectAndPostProcessDependencies() {
this.streamListenerParameterAdapters = this.applicationContext.getBeansOfType(StreamListenerParameterAdapter.class).values();
this.streamListenerResultAdapters = this.applicationContext.getBeansOfType(StreamListenerResultAdapter.class).values();
Collection<StreamListenerParameterAdapter> streamListenerParameterAdapters = this.applicationContext.getBeansOfType(StreamListenerParameterAdapter.class).values();
Collection<StreamListenerResultAdapter> streamListenerResultAdapters = this.applicationContext.getBeansOfType(StreamListenerResultAdapter.class).values();
this.binderAwareChannelResolver = this.applicationContext.getBean(DestinationResolver.class);
this.messageHandlerMethodFactory = this.applicationContext.getBean(MessageHandlerMethodFactory.class);
this.springIntegrationProperties = this.applicationContext.getBean(SpringIntegrationProperties.class);
this.streamListenerCallbacks.forEach(r -> r.run());
this.streamListenerSetupMethodOrchestrators.addAll(
this.applicationContext.getBeansOfType(StreamListenerSetupMethodOrchestrator.class).values());
//Default orchestrator for StreamListener method invocation is added last into the LinkedHashSet.
this.streamListenerSetupMethodOrchestrators.add(new DefaultStreamListenerSetupMethodOrchestrator(this.applicationContext,
streamListenerParameterAdapters, streamListenerResultAdapters));
this.streamListenerCallbacks.forEach(Runnable::run);
}
private class StreamListenerHandlerMethodMapping {
@@ -473,4 +327,152 @@ public class StreamListenerAnnotationBeanPostProcessor implements BeanPostProces
}
}
private class DefaultStreamListenerSetupMethodOrchestrator implements StreamListenerSetupMethodOrchestrator {
private final ConfigurableApplicationContext applicationContext;
private final Collection<StreamListenerParameterAdapter> streamListenerParameterAdapters;
private final Collection<StreamListenerResultAdapter> streamListenerResultAdapters;
private DefaultStreamListenerSetupMethodOrchestrator(ConfigurableApplicationContext applicationContext, Collection<StreamListenerParameterAdapter> streamListenerParameterAdapters, Collection<StreamListenerResultAdapter> streamListenerResultAdapters) {
this.applicationContext = applicationContext;
this.streamListenerParameterAdapters = streamListenerParameterAdapters;
this.streamListenerResultAdapters = streamListenerResultAdapters;
}
@Override
public void orchestrateStreamListenerSetupMethod(StreamListener streamListener, Method method, Object bean) {
String methodAnnotatedInboundName = streamListener.value();
String methodAnnotatedOutboundName = StreamListenerMethodUtils.getOutboundBindingTargetName(method);
int inputAnnotationCount = StreamListenerMethodUtils.inputAnnotationCount(method);
int outputAnnotationCount = StreamListenerMethodUtils.outputAnnotationCount(method);
boolean isDeclarative = checkDeclarativeMethod(method, methodAnnotatedInboundName, methodAnnotatedOutboundName);
StreamListenerMethodUtils.validateStreamListenerMethod(method,
inputAnnotationCount, outputAnnotationCount,
methodAnnotatedInboundName, methodAnnotatedOutboundName,
isDeclarative, streamListener.condition());
if (isDeclarative) {
StreamListenerParameterAdapter[] toSlpaArray = new StreamListenerParameterAdapter[this.streamListenerParameterAdapters.size()];
Object[] adaptedInboundArguments = adaptAndRetrieveInboundArguments(method, methodAnnotatedInboundName,
this.applicationContext,
this.streamListenerParameterAdapters.toArray(toSlpaArray));
invokeStreamListenerResultAdapter(method, bean, methodAnnotatedOutboundName, adaptedInboundArguments);
} else {
registerHandlerMethodOnListenedChannel(method, streamListener, bean);
}
}
@Override
public boolean supports(Method method) {
//default catch all orchestrator
return true;
}
@SuppressWarnings({"rawtypes", "unchecked"})
private void invokeStreamListenerResultAdapter(Method method, Object bean, String outboundName, Object... arguments) {
try {
if (Void.TYPE.equals(method.getReturnType())) {
method.invoke(bean, arguments);
} else {
Object result = method.invoke(bean, arguments);
if (!StringUtils.hasText(outboundName)) {
for (int parameterIndex = 0; parameterIndex < method.getParameterTypes().length; parameterIndex++) {
MethodParameter methodParameter = MethodParameter.forExecutable(method, parameterIndex);
if (methodParameter.hasParameterAnnotation(Output.class)) {
outboundName = methodParameter.getParameterAnnotation(Output.class).value();
}
}
}
Object targetBean = this.applicationContext.getBean(outboundName);
for (StreamListenerResultAdapter streamListenerResultAdapter : this.streamListenerResultAdapters) {
if (streamListenerResultAdapter.supports(result.getClass(), targetBean.getClass())) {
streamListenerResultAdapter.adapt(result, targetBean);
break;
}
}
}
}
catch (Exception e) {
throw new BeanInitializationException("Cannot setup StreamListener for " + method, e);
}
}
private void registerHandlerMethodOnListenedChannel(Method method, StreamListener streamListener, Object bean) {
Assert.hasText(streamListener.value(), "The binding name cannot be null");
if (!StringUtils.hasText(streamListener.value())) {
throw new BeanInitializationException("A bound component name must be specified");
}
final String defaultOutputChannel = StreamListenerMethodUtils.getOutboundBindingTargetName(method);
if (Void.TYPE.equals(method.getReturnType())) {
Assert.isTrue(StringUtils.isEmpty(defaultOutputChannel),
"An output channel cannot be specified for a method that does not return a value");
}
else {
Assert.isTrue(!StringUtils.isEmpty(defaultOutputChannel),
"An output channel must be specified for a method that can return a value");
}
StreamListenerMethodUtils.validateStreamListenerMessageHandler(method);
mappedListenerMethods.add(streamListener.value(),
new StreamListenerHandlerMethodMapping(bean, method, streamListener.condition(), defaultOutputChannel,
streamListener.copyHeaders()));
}
private boolean checkDeclarativeMethod(Method method, String methodAnnotatedInboundName, String methodAnnotatedOutboundName) {
int methodArgumentsLength = method.getParameterTypes().length;
for (int parameterIndex = 0; parameterIndex < methodArgumentsLength; parameterIndex++) {
MethodParameter methodParameter = MethodParameter.forExecutable(method, parameterIndex);
if (methodParameter.hasParameterAnnotation(Input.class)) {
String inboundName = (String) AnnotationUtils
.getValue(methodParameter.getParameterAnnotation(Input.class));
Assert.isTrue(StringUtils.hasText(inboundName), StreamListenerErrorMessages.INVALID_INBOUND_NAME);
Assert.isTrue(isDeclarativeMethodParameter(inboundName, methodParameter),
StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS);
return true;
}
else if (methodParameter.hasParameterAnnotation(Output.class)) {
String outboundName = (String) AnnotationUtils
.getValue(methodParameter.getParameterAnnotation(Output.class));
Assert.isTrue(StringUtils.hasText(outboundName), StreamListenerErrorMessages.INVALID_OUTBOUND_NAME);
Assert.isTrue(isDeclarativeMethodParameter(outboundName, methodParameter),
StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS);
return true;
}
else if (StringUtils.hasText(methodAnnotatedOutboundName)) {
return isDeclarativeMethodParameter(methodAnnotatedOutboundName, methodParameter);
}
else if (StringUtils.hasText(methodAnnotatedInboundName)) {
return isDeclarativeMethodParameter(methodAnnotatedInboundName, methodParameter);
}
}
return false;
}
/**
* Determines if method parameters signify an imperative or declarative listener definition.
* <br>
* Imperative - where handler method is invoked on each message by the handler infrastructure provided
* by the framework
* <br>
* Declarative - where handler is provided by the method itself.
* <br>
* Declarative method parameter could either be {@link MessageChannel} or any other Object for which
* there is a {@link StreamListenerParameterAdapter} (i.e., {@link reactor.core.publisher.Flux}). Declarative method is invoked only
* once during initialization phase.
*/
@SuppressWarnings("unchecked")
private boolean isDeclarativeMethodParameter(String targetBeanName, MethodParameter methodParameter) {
boolean declarative = false;
if (!methodParameter.getParameterType().isAssignableFrom(Object.class) && this.applicationContext.containsBean(targetBeanName)) {
declarative = MessageChannel.class.isAssignableFrom(methodParameter.getParameterType());
if (!declarative) {
Class<?> targetBeanClass = this.applicationContext.getType(targetBeanName);
declarative = this.streamListenerParameterAdapters.stream()
.anyMatch(slpa -> slpa.supports(targetBeanClass, methodParameter));
}
}
return declarative;
}
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2018 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.cloud.stream.binding;
import java.lang.reflect.Method;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.context.ApplicationContext;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Orchestrator used for invoking the {@link StreamListener} setup method.
*
* By default {@link StreamListenerAnnotationBeanPostProcessor} will use an internal implementation
* of this interface to invoke {@link StreamListenerParameterAdapter}s and {@link StreamListenerResultAdapter}s
* or handler mappings on the method annotated with {@link StreamListener}.
*
* By providing a different implementation of this interface and registering it as a Spring Bean in the
* context, one can override the default invocation strategies used by the {@link StreamListenerAnnotationBeanPostProcessor}.
* A typical usecase for such overriding can happen when a downstream {@link org.springframework.cloud.stream.binder.Binder}
* implementation wants to change the way in which any of the default StreamListener handling needs to be changed in a
* custom manner.
*
* When beans of this interface are present in the context, they get priority in the {@link StreamListenerAnnotationBeanPostProcessor}
* before falling back to the default implementation.
*
* {@see StreamListener}
* {@see StreamListenerAnnotationBeanPostProcessor}
*
* @author Soby Chacko
*/
public interface StreamListenerSetupMethodOrchestrator {
/**
* Checks the method annotated with {@link StreamListener} to see if this implementation
* can successfully orchestrate this method.
*
* @param method annotated with {@link StreamListener}
* @return true if this implementation can orchestrate this method, false otherwise
*/
boolean supports(Method method);
/**
* Method that allows custom orchestration on the {@link StreamListener} setup method.
*
* @param streamListener reference to the {@link StreamListener} annotation on the method
* @param method annotated with {@link StreamListener}
* @param bean that contains the StreamListener method
*
*/
void orchestrateStreamListenerSetupMethod(StreamListener streamListener, Method method, Object bean);
/**
* Default implementation for adapting each of the incoming method arguments using an available
* {@link StreamListenerParameterAdapter} and provide the adapted collection of arguments back to the caller.
*
* @param method annotated with {@link StreamListener}
* @param inboundName inbound binding
* @param applicationContext spring application context
* @param streamListenerParameterAdapters used for adapting the method arguments
* @return adapted incoming arguments
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
default Object[] adaptAndRetrieveInboundArguments(Method method, String inboundName,
ApplicationContext applicationContext,
StreamListenerParameterAdapter... streamListenerParameterAdapters) {
Object[] arguments = new Object[method.getParameterTypes().length];
for (int parameterIndex = 0; parameterIndex < arguments.length; parameterIndex++) {
MethodParameter methodParameter = MethodParameter.forExecutable(method, parameterIndex);
Class<?> parameterType = methodParameter.getParameterType();
Object targetReferenceValue = null;
if (methodParameter.hasParameterAnnotation(Input.class)) {
targetReferenceValue = AnnotationUtils.getValue(methodParameter.getParameterAnnotation(Input.class));
}
else if (methodParameter.hasParameterAnnotation(Output.class)) {
targetReferenceValue = AnnotationUtils.getValue(methodParameter.getParameterAnnotation(Output.class));
}
else if (arguments.length == 1 && StringUtils.hasText(inboundName)) {
targetReferenceValue = inboundName;
}
if (targetReferenceValue != null) {
Assert.isInstanceOf(String.class, targetReferenceValue, "Annotation value must be a String");
Object targetBean = applicationContext.getBean((String) targetReferenceValue);
// Iterate existing parameter adapters first
for (StreamListenerParameterAdapter streamListenerParameterAdapter : streamListenerParameterAdapters) {
if (streamListenerParameterAdapter.supports(targetBean.getClass(), methodParameter)) {
arguments[parameterIndex] = streamListenerParameterAdapter.adapt(targetBean, methodParameter);
break;
}
}
if (arguments[parameterIndex] == null && parameterType.isAssignableFrom(targetBean.getClass())) {
arguments[parameterIndex] = targetBean;
}
Assert.notNull(arguments[parameterIndex], "Cannot convert argument " + parameterIndex + " of " + method
+ "from " + targetBean.getClass() + " to " + parameterType);
}
else {
throw new IllegalStateException(StreamListenerErrorMessages.INVALID_DECLARATIVE_METHOD_PARAMETERS);
}
}
return arguments;
}
}