spring-cloud-stream学习笔记
本文研究SpringCloudStream配置流程及消息中间件的加入,以@OutPut
为例
Stream配置
org.springframework.cloud.stream.config.BindingServiceProperties
stream的管道配置一般写为spring.cloud.stream.bindings.<channelName>.*
,这里接受的主要参数为:
private String destination;//消息topic
private String group;//组,输出管道时可以不填,这时组名会与topic相同
private String contentType = DEFAULT_CONTENT_TYPE.toString();//消息类型,默认json
private String binder;//存在多个消息中间件时需要指定中间件名称,该名称在对应的spring-cloud-stream-binder-*的META-INF文件夹中,spring.binders
private ConsumerProperties consumer;
private ProducerProperties producer;
这里给出一个配置例子:
spring:
cloud:
stream:
bindings:
test-input:
destination: binderTest
group: test-consumer
binder: rocketmq
test-out-put:
destination: binderTest
group: test-producer
binder: rocketmq
管道绑定
在系统启动时,spring会通过org.springframework.cloud.stream.config.BindingServiceConfiguration
加载org.springframework.cloud.stream.binding.OutputBindingLifecycle
,根据@EnableBinding
配置的类循环调用doStartWithBindable(Bindable bindable)
后执行管道配置。
abstract class AbstractBindingLifecycle implements SmartLifecycle {
.......
@Override
public void start() {
if (!this.running) {
if (this.context != null) {
this.bindables.putAll(context.getBeansOfType(Bindable.class));
}
this.bindables.values().forEach(this::doStartWithBindable);//这里的bandables即为@EnableBinding配置的信息map
this.running = true;
}
.......
}
管道实现类配置
调用output的绑定时,通过Bindable携带管道类信息,比如:
public interface RocketMQMessageChannel {
@Input("test-input")
SubscribableChannel orderInput();
@Output("test-output")
MessageChannel orderOutput();
}
//那这里组成的bandable实现为org.springframework.cloud.stream.binding.BindableProxyFactory
//type=com.springcloudstudy.RocketMQstream.config.RocketMQMessageChannel
通过bindable.createAndBindOutputs
方法组装管道名与对应messageHandle的对应关系,以Binding
接口形式返回,这里会返回所有的Output管道对应的messageHandle。这里的bindable对象包含了所有的input和output节点,分别保存在outputHolders
和inputHolders
两个map里面。
public class OutputBindingLifecycle extends AbstractBindingLifecycle {
......
@Override
void doStartWithBindable(Bindable bindable) {
Collection<Binding<Object>> bindableBindings = bindable
.createAndBindOutputs(this.bindingService);
if (!CollectionUtils.isEmpty(bindableBindings)) {
this.outputBindings.addAll(bindableBindings);
}
}
......
}
进入bindable.createAndBindOutputs
,调用bindingService.bindProducer
解析每个holder,outputTargetName
即为当前管道名称
public Collection<Binding<Object>> createAndBindOutputs(
BindingService bindingService) {
......
for (Map.Entry<String, BoundTargetHolder> boundTargetHolderEntry : this.outputHolders
.entrySet()) {
BoundTargetHolder boundTargetHolder = boundTargetHolderEntry.getValue();
String outputTargetName = boundTargetHolderEntry.getKey();
if (boundTargetHolderEntry.getValue().isBindable()) {
if (log.isDebugEnabled()) {
log.debug(String.format("Binding %s:%s:%s", this.namespace, this.type,
outputTargetName));
}
bindings.add(bindingService.bindProducer(
boundTargetHolder.getBoundTarget(), outputTargetName));//BindingService解析
}
}
return bindings;
}
org.springframework.cloud.stream.binding.BindingService
BindingService是将MQBinder和参数属性绑定的服务类,这里会根据管道名找出对应配置的binder,如果未指定且存在多个消息中间件binder时会抛出异常。
public <T> Binding<T> bindProducer(T output, String outputName) {
String bindingTarget = this.bindingServiceProperties
.getBindingDestination(outputName);//topic
//获取binder对应的MessageChannelBinder
Binder<T, ?, ProducerProperties> binder = (Binder<T, ?, ProducerProperties>) getBinder(
outputName, output.getClass());
//这里是spring.cloud.stream.bindings.<channelName>.producer.*配置内容,outputName = channelName
ProducerProperties producerProperties = this.bindingServiceProperties
.getProducerProperties(outputName);
if (binder instanceof ExtendedPropertiesBinder) {
//这里取的是spring.cloud.stream.<MQName>.bindings.<channelname>.producer.*的内容,即为mq的特有配置
Object extension = ((ExtendedPropertiesBinder) binder)
.getExtendedProducerProperties(outputName);
ExtendedProducerProperties extendedProducerProperties = new ExtendedProducerProperties<>(
extension);
BeanUtils.copyProperties(producerProperties, extendedProducerProperties);
producerProperties = extendedProducerProperties;
}
validate(producerProperties);
Binding<T> binding = doBindProducer(output, bindingTarget, binder,
producerProperties);
this.producerBindings.put(outputName, binding);
return binding;
}
binder获取
调用getBander
方法时会根据outputName获取配置中的bander,没配置的情况接下来展示。之后会调用org.springframework.cloud.stream.binde.DefaultBinderFactory.getBinder(String name, Class<? extends T> bindingTargetType)
方法,根据SpringCloudStream实现的要求,需要在META-INF目录下新增spring.binders存放中间件名称与自动配置类对应关系,保存在DefaultBinderFactory
的binderTypeRegistry
属性中。
public synchronized <T> Binder<T, ?, ?> getBinder(String name,
Class<? extends T> bindingTargetType) {
......
Map<String, Binder> binders = this.context == null ? Collections.emptyMap()
: this.context.getBeansOfType(Binder.class);
Binder<T, ConsumerProperties, ProducerProperties> binder;
......
binder = this.doGetBinder(binderName, bindingTargetType);
......
}
注意,在执行这个流程时,可能因为优先级等原因对应的MQAutoConfiguration还没有进行自动配置,这时会调用doGetBinder(String name,Class<? extends T> bindingTargetType)
方法临时加塞,触发对应MQAutoConfiguration自动配置。
private <T> Binder<T, ConsumerProperties, ProducerProperties> doGetBinder(String name,
Class<? extends T> bindingTargetType) {
String configurationName;//mq名称
if (StringUtils.isEmpty(name)) {
......
//查找所有符合条件的mq,当未指定binderName且存在多个binder时,会抛出错误: no default binder has been set
for (Map.Entry<String, BinderConfiguration> binderConfigurationEntry : this.binderConfigurations
.entrySet()) {
if (binderConfigurationEntry.getValue().isDefaultCandidate()) {
defaultCandidateConfigurations
.add(binderConfigurationEntry.getKey());
}
}
......
}
else {
configurationName = name;
}
Binder<T, ConsumerProperties, ProducerProperties> binderInstance = getBinderInstance(
configurationName);
........
}
调用getBinderInstance(String configurationName)
方法时,需要拿到binder实现类,所以需要SpringApplicationBuilder来触发对应的MQAutoConfigration,这里的SpringApplicationBuilder会绑定原有的springContext使用,所以是相当于把需要在之后执行的MQAutoConfigration放到这里执行,不会影响原来的启动流程。至此,主要的binder实现类拿到了。
private <T> Binder<T, ConsumerProperties, ProducerProperties> getBinderInstance(
String configurationName) {
if (!this.binderInstanceCache.containsKey(configurationName)) {
BinderConfiguration binderConfiguration = this.binderConfigurations
.get(configurationName);
//binderTypeRegistry保存的是mq名称与自动配置类关系
BinderType binderType = this.binderTypeRegistry
.get(binderConfiguration.getBinderType());
......
ArrayList<String> args = new ArrayList<>();
......
SpringApplicationBuilder springApplicationBuilder = new SpringApplicationBuilder(
binderType.getConfigurationClasses())
.bannerMode(Mode.OFF).logStartupInfo(false)
.web(WebApplicationType.NONE);
........
springApplicationBuilder.parent(this.context);
........
ConfigurableApplicationContext binderProducingContext = springApplicationBuilder
.run(args.toArray(new String[0]));
Binder<T, ?, ?> binder = binderProducingContext.getBean(Binder.class);
......
//缓存已生成的mqBinder
this.binderInstanceCache.put(configurationName,
new SimpleImmutableEntry<>(binder, binderProducingContext));
}
return (Binder<T, ConsumerProperties, ProducerProperties>) this.binderInstanceCache
.get(configurationName).getKey();
}
@OutPut
与@Input
同名情况
当我们把@OutPut
和@Input
填入相同的值之后,有一定几率会报MessageDeliveryException: Dispatcher has no subscribers for channel 'test-output'
,为什么是有几率呢,是因为@OutPut
和@Input
会以value为名称生成MessageChannel
的bean到beanFactory中,绑定到inputHolders
和outputHolders
里面,但是在设置该MessageChannel
的handle
属性时又是以名称去spring上下文中找bean,也就是beanFactory
中的值,这时候就看运气了,如果inputHolders
中的bean在outputHolders
之后生成,这时spring上下文中保存的bean就是对的,反之则会报错。
跟踪源码获知,inputHolders
和outputHolders
赋值是在org.springframework.cloud.stream.binding.BindableProxyFactory#afterPropertiesSet
方法中执行的:
public void afterPropertiesSet() {
/**
* 根据标记@Input和@Output注解的方法执行
* type @EnableBinding的值
* method 标记过的方法
*/
ReflectionUtils.doWithMethods(this.type, method -> {
Input input = AnnotationUtils.findAnnotation(method, Input.class);
if (input != null) {
String name = BindingBeanDefinitionRegistryUtils
.getBindingTargetName(input, method);
Class<?> returnType = method.getReturnType();
BindableProxyFactory.this.inputHolders.put(name,
new BoundTargetHolder(getBindingTargetFactory(returnType)
.createInput(name), true));
}
});
ReflectionUtils.doWithMethods(this.type, method -> {
Output output = AnnotationUtils.findAnnotation(method, Output.class);
if (output != null) {
String name = BindingBeanDefinitionRegistryUtils
.getBindingTargetName(output, method);
Class<?> returnType = method.getReturnType();
BindableProxyFactory.this.outputHolders.put(name,
new BoundTargetHolder(getBindingTargetFactory(returnType)
.createOutput(name), true));
}
});
}
这里最后生成的键值对类型为map<Stirng,DirectWithAttributesChannel>
。
在执行生成DirectWithAttributesChannel
实例的BindingTargetFactory(returnType).createInput(name)
方法中是这样注册bean的:
public SubscribableChannel createInput(String name) {
DirectWithAttributesChannel subscribableChannel = new DirectWithAttributesChannel();
subscribableChannel.setComponentName(name);
subscribableChannel.setAttribute("type", Sink.INPUT);
this.messageChannelConfigurer.configureInputChannel(subscribableChannel, name);
if (context != null && !context.containsBean(name)) {
context.registerBean(name, DirectWithAttributesChannel.class, () -> subscribableChannel);
}
return subscribableChannel;
}
这里的context.registerBean
会以名称为key注册实例,这里问题已经出现了,下面解释问题出现点:
org.springframework.cloud.stream.binding.StreamListenerAnnotationBeanPostProcessor#afterSingletonsInstantiated
方法执行,这里会设置消息消费者的回调方法到管道绑定的DirectWithAttributesChannel
对象中:
public final void afterSingletonsInstantiated() {
......
for (Map.Entry<String, List<StreamListenerHandlerMethodMapping>> mappedBindingEntry : this.mappedListenerMethods
.entrySet()) {//mappedListenerMethods记录了@StreamListener绑定的名称与方法
......
this.applicationContext.getBeanFactory().registerSingleton(
handler.getClass().getSimpleName() + handler.hashCode(), handler);//注册当前消息回调方法到spring上下文中
this.applicationContext
.getBean(mappedBindingEntry.getKey(), SubscribableChannel.class)//根据当前@StreamListener的value获取channel
.subscribe(handler);//绑定回调方法
}
this.mappedListenerMethods.clear();
}
这里获取DirectWithAttributesChannel
时是通过@StreamListener
绑定的管道名获取的,结合之前的代码,当@Input
和@Output
值相同时,如果同名的@Input
绑定的DirectWithAttributesChannel
实例在@Output
之后设置到spring上下文时,不会报错,这里需要注意一下,否则问题不会每次都出现,比较闹心。
总结
SpringCloudStream相当于MQ与系统的接口,MQ的发送接收消息逻辑在MessageHandle里面,跟管道绑定,而stream只需要关注消息应该发送至哪个管道即可。
SpringCloudStream的实现规则
- 实现Binder接口
- 新增spring.binders文件,并写入名称与配置类对应关系::
binder加载
- 扫描
@EnableBinding
的类中的方法,获取注解信息 - 将标记了
@Input
和@Output
的方法存入inputHolders
和outputHolders
- 获取标记了
@StreamListener
的方法,并关联到inputChannel中 - 根据channelName的配置的binder实现各自消息发送方法
未完待续。。。。。。。