配置消息通道
要创建消息通道实例,您可以使用<channel/>
元素或DirectChannel
instance 进行 Java 配置,如下所示:
-
Java
-
XML
@Bean
public MessageChannel exampleChannel() {
return new DirectChannel();
}
<int:channel id="exampleChannel"/>
当您使用<channel/>
元素中,它会创建一个DirectChannel
实例(一个SubscribableChannel
).
要创建 publish-subscribe 通道,请使用<publish-subscribe-channel/>
元素(PublishSubscribeChannel
在 Java 中),如下所示:
-
Java
-
XML
@Bean
public MessageChannel exampleChannel() {
return new PublishSubscribeChannel();
}
<int:publish-subscribe-channel id="exampleChannel"/>
您也可以提供各种<queue/>
子元素来创建任何可轮询的通道类型(如 消息通道实现中所述)。
以下部分显示了每种通道类型的示例。
DirectChannel
配置
如前所述,DirectChannel
是默认类型。
下面的清单显示了定义谁:
-
Java
-
XML
@Bean
public MessageChannel directChannel() {
return new DirectChannel();
}
<int:channel id="directChannel"/>
默认通道具有循环负载均衡器,并且还启用了故障转移(请参阅DirectChannel
了解更多详情)。
要禁用其中一项或两项,请添加<dispatcher/>
子元素 (一个LoadBalancingStrategy
构造函数的DirectChannel
) 并配置属性,如下所示:
-
Java
-
XML
@Bean
public MessageChannel failFastChannel() {
DirectChannel channel = new DirectChannel();
channel.setFailover(false);
return channel;
}
@Bean
public MessageChannel failFastChannel() {
return new DirectChannel(null);
}
<int:channel id="failFastChannel">
<int:dispatcher failover="false"/>
</channel>
<int:channel id="channelWithFixedOrderSequenceFailover">
<int:dispatcher load-balancer="none"/>
</int:channel>
从版本 6.3 开始,所有MessageChannel
基于UnicastingDispatcher
可以配置Predicate<Exception> failoverStrategy
而不是普通failover
选择。
此谓词决定是否故障转移到下一个MessageHandler
基于从当前 ID 引发的异常。
更复杂的错误分析应使用ErrorMessageExceptionTypeRouter
.
数据类型 Channel 配置
有时,使用者只能处理特定类型的有效负载,这迫使您确保输入消息的有效负载类型。 首先想到的可能是使用消息过滤器。 但是,消息筛选器所能做的只是筛选出不符合使用者要求的消息。 另一种方法是使用基于内容的路由器,并将具有不合规数据类型的消息路由到特定的转换器,以强制转换和转换为所需的数据类型。 这将有效,但完成相同任务的更简单方法是应用 Datatype Channel 模式。 您可以为每个特定的负载数据类型使用单独的数据类型通道。
要创建仅接受包含特定有效负载类型的消息的数据类型通道,请在通道元素的datatype
属性,如下例所示:
-
Java
-
XML
@Bean
public MessageChannel numberChannel() {
DirectChannel channel = new DirectChannel();
channel.setDatatypes(Number.class);
return channel;
}
<int:channel id="numberChannel" datatype="java.lang.Number"/>
请注意,对于可分配给通道数据类型的任何类型的类型,类型检查都会通过。
换句话说,numberChannel
在前面的示例中,将接受 payload 为java.lang.Integer
或java.lang.Double
.
可以将多个类型作为逗号分隔的列表提供,如下例所示:
-
Java
-
XML
@Bean
public MessageChannel numberChannel() {
DirectChannel channel = new DirectChannel();
channel.setDatatypes(String.class, Number.class);
return channel;
}
<int:channel id="stringOrNumberChannel" datatype="java.lang.String,java.lang.Number"/>
因此,前面示例中的 'numberChannel' 只接受数据类型为java.lang.Number
.
但是,如果消息的有效负载不是 required 类型,会发生什么情况呢?
这取决于您是否定义了一个名为integrationConversionService
那是 Spring 的 Conversion Service 的一个实例。
如果不是,则Exception
将立即被抛出。
但是,如果您定义了integrationConversionService
bean,则使用它来尝试将消息的有效负载转换为可接受的类型。
您甚至可以注册自定义转换器。
例如,假设您发送一条消息,其中包含String
payload 添加到我们上面配置的 'numberChannel' 中。
您可以按如下方式处理该消息:
MessageChannel inChannel = context.getBean("numberChannel", MessageChannel.class);
inChannel.send(new GenericMessage<String>("5"));
通常,这将是一个完全合法的作。 但是,由于我们使用 Datatype Channel,因此此类作的结果将生成类似于以下内容的异常:
Exception in thread "main" org.springframework.integration.MessageDeliveryException:
Channel 'numberChannel'
expected one of the following datataypes [class java.lang.Number],
but received [class java.lang.String]
…
发生异常是因为我们要求 payload 类型为Number
,但我们发送了String
.
所以我们需要一些东西来转换String
更改为Number
.
为此,我们可以实现类似于以下示例的转换器:
public static class StringToIntegerConverter implements Converter<String, Integer> {
public Integer convert(String source) {
return Integer.parseInt(source);
}
}
然后,我们可以将其注册为 Integration Conversion Service 的转换器,如下例所示:
-
Java
-
XML
@Bean
@IntegrationConverter
public StringToIntegerConverter strToInt {
return new StringToIntegerConverter();
}
<int:converter ref="strToInt"/>
<bean id="strToInt" class="org.springframework.integration.util.Demo.StringToIntegerConverter"/>
或者在StringToIntegerConverter
类(当它标有@Component
annotation 进行自动扫描。
当 'converter' 元素被解析时,它会创建integrationConversionService
bean(如果尚未定义)。
有了该转换器,send
作现在将成功,因为 DataType 通道使用该转换器将String
payload 添加到Integer
.
有关负载类型转换的更多信息,请参阅负载类型转换。
从版本 4.0 开始,integrationConversionService
由DefaultDatatypeChannelMessageConverter
,它在应用程序上下文中查找 conversion 服务。
要使用其他转换技术,您可以指定message-converter
属性。
这必须是对MessageConverter
实现。
只有fromMessage
方法。
它为转换器提供了对消息 Headers 的访问(如果转换可能需要来自 Headers 的信息,例如content-type
).
该方法只能返回转换后的有效负载或完整的Message
对象。
如果是后者,则转换器必须小心地从入站消息中复制所有 Headers。
或者,您可以声明<bean/>
的类型MessageConverter
的 ID 为datatypeChannelMessageConverter
,并且该转换器被所有通道使用datatype
.
QueueChannel
配置
要创建QueueChannel
,请使用<queue/>
sub-元素。
您可以按如下方式指定通道的容量:
-
Java
-
XML
@Bean
public PollableChannel queueChannel() {
return new QueueChannel(25);
}
<int:channel id="queueChannel">
<queue capacity="25"/>
</int:channel>
如果您没有为此的 'capacity' 属性提供值<queue/> sub-元素,则生成的队列是无界的。
为避免内存不足等问题,我们强烈建议您为有界队列设置显式值。 |
持续QueueChannel
配置
由于QueueChannel
提供缓冲消息的功能,但默认情况下仅在内存中缓冲,它还引入了在系统故障时消息可能会丢失的可能性。
为了降低这种风险,一个QueueChannel
可能由MessageGroupStore
strategy 界面。
有关的更多详细信息MessageGroupStore
和MessageStore
,请参阅 Message Store。
这capacity 属性时不允许使用message-store 属性。 |
当QueueChannel
接收一个Message
,它会将消息添加到邮件存储中。
当Message
从QueueChannel
,则会从邮件存储中删除该 ID。
默认情况下,QueueChannel
将其消息存储在内存中队列中,这可能会导致前面提到的消息丢失情况。
但是, Spring 集成提供了持久存储,例如JdbcChannelMessageStore
.
您可以为任何QueueChannel
通过添加message-store
属性,如下例所示:
<int:channel id="dbBackedChannel">
<int:queue message-store="channelStore"/>
</int:channel>
<bean id="channelStore" class="o.s.i.jdbc.store.JdbcChannelMessageStore">
<property name="dataSource" ref="dataSource"/>
<property name="channelMessageStoreQueryProvider" ref="queryProvider"/>
</bean>
(有关Java/Kotlin配置选项,请参阅以下示例。
Spring 集成 JDBC 模块还为许多流行的数据库提供了模式数据定义语言(DDL)。
这些模式位于该模块的 org.springframework.integration.jdbc.store.channel 包(spring-integration-jdbc
).
一个重要的特性是,对于任何事务性持久存储(例如JdbcChannelMessageStore ),只要 Poller 配置了事务,则只有在事务成功完成时,才能永久删除从 store 中删除的消息。
否则,事务将回滚,并且Message 没有丢失。 |
随着越来越多的与 “NoSQL” 数据存储相关的 Spring 项目开始为这些存储提供底层支持,可以使用消息存储的许多其他实现。
您还可以提供自己的MessageGroupStore
界面。
从 4.0 版本开始,我们建议QueueChannel
实例配置为使用ChannelMessageStore
,如果可能。
与一般邮件存储相比,这些存储通常针对此用途进行了优化。
如果ChannelMessageStore
是一个ChannelPriorityMessageStore
,则消息将按优先级顺序在 FIFO 中接收。
优先级的概念由 message store 实现确定。
例如,以下示例显示了 MongoDB 通道消息存储的 Java 配置:
-
Java
-
Java DSL
-
Kotlin DSL
@Bean
public BasicMessageGroupStore mongoDbChannelMessageStore(MongoDbFactory mongoDbFactory) {
MongoDbChannelMessageStore store = new MongoDbChannelMessageStore(mongoDbFactory);
store.setPriorityEnabled(true);
return store;
}
@Bean
public PollableChannel priorityQueue(BasicMessageGroupStore mongoDbChannelMessageStore) {
return new PriorityChannel(new MessageGroupQueue(mongoDbChannelMessageStore, "priorityQueue"));
}
@Bean
public IntegrationFlow priorityFlow(PriorityCapableChannelMessageStore mongoDbChannelMessageStore) {
return IntegrationFlow.from((Channels c) ->
c.priority("priorityChannel", mongoDbChannelMessageStore, "priorityGroup"))
....
.get();
}
@Bean
fun priorityFlow(mongoDbChannelMessageStore: PriorityCapableChannelMessageStore) =
integrationFlow {
channel { priority("priorityChannel", mongoDbChannelMessageStore, "priorityGroup") }
}
注意MessageGroupQueue 类。
那是一个BlockingQueue implementation 以使用MessageGroupStore 操作。 |
自定义QueueChannel
environment 由ref
属性的<int:queue>
sub-元素或其特定构造函数。
此属性提供对任何java.util.Queue
实现。
例如,分布式 HazelcastIQueue
可以按如下方式进行配置:
@Bean
public HazelcastInstance hazelcastInstance() {
return Hazelcast.newHazelcastInstance(new Config()
.setProperty("hazelcast.logging.type", "log4j"));
}
@Bean
public PollableChannel distributedQueue() {
return new QueueChannel(hazelcastInstance()
.getQueue("springIntegrationQueue"));
}
PublishSubscribeChannel
配置
要创建PublishSubscribeChannel
,请使用 <publish-subscribe-channel/> 元素。
使用此元素时,您还可以指定task-executor
用于发布消息(如果未指定,则在 sender 的线程中发布),如下所示:
-
Java
-
XML
@Bean
public MessageChannel pubsubChannel() {
return new PublishSubscribeChannel(someExecutor());
}
<int:publish-subscribe-channel id="pubsubChannel" task-executor="someExecutor"/>
如果您在PublishSubscribeChannel
中,您可以将渠道上的“apply-sequence”属性设置为true
.
这样做表明通道应将sequence-size
和sequence-number
消息标头以及相关 ID。
例如,如果有 5 个订阅者,则sequence-size
将设置为5
,并且消息将具有sequence-number
标头值范围从1
自5
.
与Executor
,您还可以配置ErrorHandler
.
默认情况下,PublishSubscribeChannel
使用MessagePublishingErrorHandler
实现将错误发送到MessageChannel
从errorChannel
标头或全局errorChannel
实例。
如果Executor
未配置,则ErrorHandler
被忽略,异常将直接抛出到调用方的线程中。
如果您提供Resequencer
或Aggregator
下游PublishSubscribeChannel
中,您可以将渠道上的“apply-sequence”属性设置为true
.
这样做表示通道应在传递消息之前设置 sequence-size 和 sequence-number 消息头以及相关 ID。
例如,如果有 5 个订阅者,则 sequence-size 将设置为5
,并且消息将具有序列号报头值,范围从1
自5
.
以下示例演示如何设置apply-sequence
header 设置为true
:
-
Java
-
XML
@Bean
public MessageChannel pubsubChannel() {
PublishSubscribeChannel channel = new PublishSubscribeChannel();
channel.setApplySequence(true);
return channel;
}
<int:publish-subscribe-channel id="pubsubChannel" apply-sequence="true"/>
这apply-sequence value 为false 默认情况下,以便 Publish-Subscribe 通道可以将完全相同的消息实例发送到多个出站通道。
由于 Spring Integration 强制执行有效负载和标头引用的不可变性,因此当该标志设置为true ,通道会创建新的Message 具有相同有效负载引用但标头值不同的实例。 |
从版本 5.4.3 开始,PublishSubscribeChannel
也可以使用requireSubscribers
选项的BroadcastingDispatcher
表示该频道在没有订阅者时不会静默忽略消息。
一个MessageDispatchingException
替换为Dispatcher has no subscribers
消息 当没有订阅者时抛出,并且此选项设置为true
.
ExecutorChannel
要创建ExecutorChannel
,请添加<dispatcher>
子元素替换为task-executor
属性。
该属性的值可以引用任何TaskExecutor
在上下文中。
例如,这样做可以启用线程池的配置,以便将消息分派给订阅的处理程序。
如前所述,这样做会破坏发送方和接收方之间的单线程执行上下文,以便任何活动的事务上下文都不会被处理程序的调用共享(即,处理程序可能会抛出一个Exception
,但send
invocation has already returned successfully) 的调用。
以下示例演示如何使用dispatcher
元素并在task-executor
属性:
-
Java
-
XML
@Bean
public MessageChannel executorChannel() {
return new ExecutorChannel(someExecutor());
}
<int:channel id="executorChannel">
<int:dispatcher task-executor="someExecutor"/>
</int:channel>
这
|
PriorityChannel
配置
要创建PriorityChannel
,请使用<priority-queue/>
sub-元素,如下例所示:
-
Java
-
XML
@Bean
public PollableChannel priorityChannel() {
return new PriorityChannel(20);
}
<int:channel id="priorityChannel">
<int:priority-queue capacity="20"/>
</int:channel>
默认情况下,通道会查询priority
标头。
但是,您可以改为提供自定义Comparator
参考。
另请注意,PriorityChannel
(与其他类型一样)支持datatype
属性。
与QueueChannel
,它还支持capacity
属性。
以下示例演示了所有这些:
-
Java
-
XML
@Bean
public PollableChannel priorityChannel() {
PriorityChannel channel = new PriorityChannel(20, widgetComparator());
channel.setDatatypes(example.Widget.class);
return channel;
}
<int:channel id="priorityChannel" datatype="example.Widget">
<int:priority-queue comparator="widgetComparator"
capacity="10"/>
</int:channel>
从 4.0 版本开始,priority-channel
子元素支持message-store
选项 (comparator
和capacity
在这种情况下是不允许的)。
邮件存储必须是PriorityCapableChannelMessageStore
.
的PriorityCapableChannelMessageStore
目前为Redis
,JDBC
和MongoDB
.
看QueueChannel
配置和 Message Store 了解更多信息。
您可以在 Backing Message Channels 中找到示例配置。
RendezvousChannel
配置
一个RendezvousChannel
在队列子元素为<rendezvous-queue>
.
它不提供前面描述的配置选项的任何其他配置选项,并且其队列不接受任何容量值,因为它是零容量直接切换队列。
以下示例说明如何声明RendezvousChannel
:
-
Java
-
XML
@Bean
public PollableChannel rendezvousChannel() {
return new RendezvousChannel();
}
<int:channel id="rendezvousChannel"/>
<int:rendezvous-queue/>
</int:channel>
通道拦截器配置
消息通道也可能具有拦截器,如 通道拦截器中所述。
这<interceptors/>
sub-元素可以添加到<channel/>
(或更具体的元素类型)。
您可以提供ref
属性来引用任何 Spring 管理的对象,该对象实现了ChannelInterceptor
接口,如下例所示:
<int:channel id="exampleChannel">
<int:interceptors>
<ref bean="trafficMonitoringInterceptor"/>
</int:interceptors>
</int:channel>
通常,我们建议在单独的位置定义拦截器实现,因为它们通常提供可在多个通道之间重用的常见行为。
全局通道拦截器配置
Channel interceptors 提供了一种简洁明了的方式来为每个单独的 Channel 应用横切行为。 如果应该在多个 channel 上应用相同的行为,则为每个 channel 配置相同的拦截器集将不是最有效的方法。 为了避免重复配置,同时使拦截器能够应用于多个通道, Spring 集成提供了全局拦截器。 请考虑以下一对示例:
<int:channel-interceptor pattern="input*, thing2*, thing1, !cat*" order="3">
<bean class="thing1.thing2SampleInterceptor"/>
</int:channel-interceptor>
<int:channel-interceptor ref="myInterceptor" pattern="input*, thing2*, thing1, !cat*" order="3"/>
<bean id="myInterceptor" class="thing1.thing2SampleInterceptor"/>
每<channel-interceptor/>
元素允许您定义一个全局拦截器,该拦截器将应用于与pattern
属性。
在前面的情况下,全局拦截器应用于 'thing1' 通道和所有其他以 'thing2' 或 'input' 开头的通道,但不应用于以 'thing3' 开头的通道(从 5.0 版本开始)。
将此语法添加到模式中会导致一个可能的(尽管可能不太可能)问题。
如果你有一个名为!thing1 ,并且您包含一个!thing1 在你的通道拦截器的pattern patterns,则它不再匹配。
该模式现在匹配所有未命名的 beanthing1 .
在这种情况下,您可以转义! 在模式中。
模式\ \!thing1 匹配名为!thing1 . |
order 属性允许您管理当给定通道上有多个拦截器时,此拦截器的注入位置。 例如,通道 'inputChannel' 可以在本地配置单独的拦截器(见下文),如下例所示:
<int:channel id="inputChannel">
<int:interceptors>
<int:wire-tap channel="logger"/>
</int:interceptors>
</int:channel>
一个合理的问题是“相对于本地配置的其他拦截器或通过其他全局拦截器定义,全局拦截器是如何注入的?
当前的实现提供了一种简单的机制来定义拦截器执行的顺序。
中order
属性确保在任何现有拦截器之后注入拦截器,而负数确保拦截器在现有拦截器之前注入。
这意味着,在前面的示例中,全局拦截器是在order
大于0
) 本地配置的 'wire-tap' 侦听器。
如果存在另一个全局拦截器,并且pattern
,则其顺序将通过比较两个拦截器的值来确定order
属性。
要在现有拦截器之前注入全局拦截器,请对order
属性。
请注意,order 和pattern 属性是可选的。
的默认值order 将为 0,对于pattern ,默认值为 '*' (以匹配所有通道)。 |
丝锥
如前所述, Spring 集成提供了一个简单的 wire tap 拦截器。
您可以在<interceptors/>
元素。
这样做对于调试特别有用,并且可以与 Spring 集成的日志记录通道适配器结合使用,如下所示:
<int:channel id="in">
<int:interceptors>
<int:wire-tap channel="logger"/>
</int:interceptors>
</int:channel>
<int:logging-channel-adapter id="logger" level="DEBUG"/>
'logging-channel-adapter'还接受'expression'属性,以便您可以根据'payload'和'headers'变量评估 SPEL 表达式。
或者,要记录完整消息toString() result,请提供true 对于 'log-full-message' 属性。
默认情况下,它是false ,以便仅记录有效负载。
将其设置为true 启用除有效负载之外的所有标头的日志记录。
'expression' 选项提供了最大的灵活性(例如expression="payload.user.name" ). |
关于 wire tap 和其他类似组件(消息发布配置)的一个常见误解是,它们在本质上是自动异步的。 默认情况下,作为组件的 wire tap 不会异步调用。 相反, Spring 集成专注于配置异步行为的单一统一方法:消息通道。 使消息流的某些部分同步或异步的是在该流中配置的 Message Channel 的类型。 这是消息通道抽象的主要好处之一。 从框架成立之初,我们就一直强调消息通道作为框架的一等公民的需求和价值。 它不仅仅是 EIP 模式的内部隐式实现。 它作为可配置组件完全公开给最终用户。 因此,Wire Tap 组件仅负责执行以下任务:
-
通过点击通道(例如
channelA
) -
抓取每条消息
-
将消息发送到另一个通道(例如
channelB
)
它本质上是桥接模式的变体,但它封装在通道定义中(因此更容易在不中断流的情况下启用和禁用)。 此外,与桥接不同,它基本上是分叉另一个消息流。 该流是同步的还是异步的?答案取决于 'channelB' 的消息通道类型。 我们有以下选项:direct channel、pollable channel 和 executor channel。 最后两个打破了线程边界,使通过此类通道的通信异步,因为将消息从该通道分派到其订阅的处理程序发生在与用于将消息发送到该通道的线程不同的线程上。 这就是使您的 wire-tap 流同步或异步的原因。 它与框架中的其他组件(比如消息发布者)一致,并且通过让您无需提前担心(除了编写线程安全代码)特定代码段应该作为同步还是异步实现,从而增加了一定程度的一致性和简单性。 两个代码段(比如组件 A 和组件 B)在消息通道上的实际连接使它们的协作同步或异步。 你甚至可能希望将来从 synchronous 更改为 asynchronous ,而 message channel 让你无需接触代码即可快速完成。
关于窃听的最后一点是,尽管上面提供了默认情况下不异步的基本原理,但您应该记住,通常希望尽快传递消息。 因此,使用 asynchronous channel 选项作为 wire tap 的出站通道是很常见的。 但是,默认情况下不强制实施异步行为。 如果我们这样做,有许多用例会中断,包括您可能不想打破事务边界。 也许您使用 wire tap 模式进行审计,并且您确实希望在原始事务中发送审计消息。 例如,您可以将 wire tap 连接到 JMS 出站通道适配器。 这样,您可以获得两全其美的效果:1) JMS 消息的发送可以在事务中进行,而 2) 它仍然是一个 “即发即弃”作,从而防止主消息流中出现任何明显的延迟。
从版本 4.0 开始,当拦截器(例如WireTap 类) 引用频道。
您需要将此类 channel 从当前拦截器拦截的 channels 中排除。
这可以通过适当的模式或编程方式完成。
如果您有一个自定义的ChannelInterceptor 引用channel ,请考虑实施VetoCapableInterceptor .
这样,框架会询问拦截器是否可以根据提供的模式拦截每个候选通道。
您还可以在拦截器方法中添加运行时保护,以确保通道不是拦截器引用的通道。
这WireTap 使用这两种技术。 |
从版本 4.3 开始,WireTap
具有其他构造函数,这些构造函数采用channelName
而不是MessageChannel
实例。
这对于 Java 配置以及使用通道自动创建逻辑时非常方便。
目标MessageChannel
bean 从提供的channelName
稍后,在第一次与
拦截 器。
通道解析需要BeanFactory ,因此 wire tap 实例必须是 Spring 管理的 bean。 |
这种后期绑定方法还允许使用 Java DSL 配置简化典型的窃听模式,如下例所示:
@Bean
public PollableChannel myChannel() {
return MessageChannels.queue()
.wireTap("loggingFlow.input")
.get();
}
@Bean
public IntegrationFlow loggingFlow() {
return f -> f.log();
}
条件接线器
Wire Tap 可以通过使用selector
或selector-expression
属性。
这selector
参考资料 aMessageSelector
bean,它可以在运行时确定消息是否应转到 tap 通道。
同样,selector-expression
是执行相同目的的布尔 SpEL 表达式:如果表达式的计算结果为true
,消息将发送到 Tap 渠道。
全局 Wire Tap 配置
可以将全局 wire tap 配置为 Global Channel Interceptor Configuration 的特殊情况。
为此,请配置顶级wire-tap
元素。
现在,除了正常的wire-tap
命名空间支持、pattern
和order
attributes 受支持,其工作方式与它们对channel-interceptor
.
以下示例说明如何配置全局 Wire Tap:
-
Java
-
XML
@Bean
@GlobalChannelInterceptor(patterns = "input*,thing2*,thing1", order = 3)
public WireTap wireTap(MessageChannel wiretapChannel) {
return new WireTap(wiretapChannel);
}
<int:wire-tap pattern="input*, thing2*, thing1" order="3" channel="wiretapChannel"/>
全局 Wire Tap 提供了一种在外部配置单通道 Wire Tap 而无需修改现有通道配置的便捷方法。
为此,请将pattern 属性添加到目标频道名称中。
例如,您可以使用此技术配置测试用例以验证通道上的消息。 |