消息网关

网关隐藏了 Spring Integration 提供的消息 API。它使应用程序的业务逻辑无需了解 Spring Integration API。通过使用通用网关,您的代码仅与一个简单的接口交互。

进入 GatewayProxyFactoryBean

如前所述,不依赖 Spring Integration API(包括网关类)会很棒。因此,Spring Integration 提供了 GatewayProxyFactoryBean,它为任何接口生成一个代理,并在内部调用下面显示的网关方法。通过使用依赖注入,您可以将接口暴露给业务方法。

以下示例展示了一个可用于与 Spring Integration 交互的接口:

public interface Cafe {

    void placeOrder(Order order);

}

网关 XML 命名空间支持

还提供了命名空间支持。它允许您将接口配置为服务,如以下示例所示:

<int:gateway id="cafeService"
         service-interface="org.cafeteria.Cafe"
         default-request-channel="requestChannel"
         default-reply-timeout="10000"
         default-reply-channel="replyChannel"/>

定义了此配置后,cafeService 现在可以注入到其他 bean 中,并且调用 Cafe 接口的代理实例上的方法的代码对 Spring Integration API 一无所知。有关使用 gateway 元素(在 Cafe 演示中)的示例,请参阅 “Samples” 附录。

上述配置中的默认值应用于网关接口上的所有方法。如果未指定回复超时,则调用线程将等待回复 30 秒。请参阅 gateway-no-response

可以为单个方法覆盖默认值。请参阅 gateway-configuration-annotations

设置默认回复通道

通常,您无需指定 default-reply-channel,因为网关会自动创建一个临时的匿名回复通道,并在其中监听回复。但是,在某些情况下,您可能需要定义一个 default-reply-channel(或带有适配器网关(如 HTTP、JMS 等)的 reply-channel)。

为了提供一些背景信息,我们简要讨论一下网关的一些内部工作原理。网关创建一个临时点对点回复通道。它是匿名的,并以名称 replyChannel 添加到消息头中。当提供显式的 default-reply-channel(带有远程适配器网关的 reply-channel)时,您可以指向一个发布-订阅通道,之所以这样命名,是因为您可以向其添加多个订阅者。在内部,Spring Integration 在临时 replyChannel 和显式定义的 default-reply-channel 之间创建了一个桥梁。

假设您希望回复不仅发送到网关,还发送到其他一些消费者。在这种情况下,您需要两件事:

  • 一个您可以订阅的命名通道

  • 该通道是一个发布-订阅通道

网关使用的默认策略不满足这些需求,因为添加到头中的回复通道是匿名的点对点通道。这意味着没有其他订阅者可以获取它的句柄,即使可以,该通道也具有点对点行为,因此只有一个订阅者会收到消息。通过定义 default-reply-channel,您可以指向您选择的通道。在这种情况下,它是一个 publish-subscribe-channel。网关从它创建一个桥梁到存储在头中的临时匿名回复通道。

您可能还希望通过拦截器(例如,wiretap)显式提供一个回复通道用于监控或审计。要配置通道拦截器,您需要一个命名通道。

从 5.4 版本开始,当网关方法的返回类型为 void 时,如果未明确提供 replyChannel 头,框架会将 replyChannel 头填充为 nullChannel bean 引用。这允许丢弃来自下游流的任何可能回复,满足单向网关契约。

使用注解和 XML 进行网关配置

考虑以下示例,它通过添加 @Gateway 注解扩展了之前的 Cafe 接口示例:

public interface Cafe {

    @Gateway(requestChannel="orders")
    void placeOrder(Order order);

}

@Header 注解允许您添加被解释为消息头的值,如以下示例所示:

public interface FileWriter {

    @Gateway(requestChannel="filesOut")
    void write(byte[] content, @Header(FileHeaders.FILENAME) String filename);

}

如果您更喜欢 XML 方法来配置网关方法,您可以向网关配置中添加 method 元素,如以下示例所示:

<int:gateway id="myGateway" service-interface="org.foo.bar.TestGateway"
      default-request-channel="inputC">
  <int:default-header name="calledMethod" expression="#gatewayMethod.name"/>
  <int:method name="echo" request-channel="inputA" reply-timeout="2" request-timeout="200"/>
  <int:method name="echoUpperCase" request-channel="inputB"/>
  <int:method name="echoViaDefault"/>
</int:gateway>

您还可以使用 XML 为每个方法调用提供单独的头。如果需要设置的头是静态的,并且您不想通过使用 @Header 注解将它们嵌入到网关的方法签名中,这可能很有用。例如,在贷款经纪人示例中,我们希望根据发起的请求类型(单报价或所有报价)来影响贷款报价的聚合方式。通过评估调用了哪个网关方法来确定请求类型,虽然可能,但会违反关注点分离范式(方法是 Java 工件)。但是,在消息传递架构中,在消息头中表达您的意图(元信息)是很自然的。以下示例展示了如何为两个方法添加不同的消息头:

<int:gateway id="loanBrokerGateway"
         service-interface="org.springframework.integration.loanbroker.LoanBrokerGateway">
  <int:method name="getLoanQuote" request-channel="loanBrokerPreProcessingChannel">
    <int:header name="RESPONSE_TYPE" value="BEST"/>
  </int:method>
  <int:method name="getAllLoanQuotes" request-channel="loanBrokerPreProcessingChannel">
    <int:header name="RESPONSE_TYPE" value="ALL"/>
  </int:method>
</int:gateway>

在前面的示例中,根据网关的方法,为“RESPONSE_TYPE”头设置了不同的值。

如果您在 <int:method/> 中以及 @Gateway 注解中都指定了 requestChannel,则注解值优先。

如果在 XML 中指定了无参数网关,并且接口方法同时具有 @Payload@Gateway 注解(在 <int:method/> 元素中带有 payloadExpressionpayload-expression),则 @Payload 值将被忽略。

表达式和“全局”头

<header/> 元素支持 expression 作为 value 的替代。SpEL 表达式被评估以确定头的值。从 5.2 版本开始,评估上下文的 #root 对象是一个 MethodArgsHolder,带有 getMethod()getArgs() 访问器。例如,如果您希望根据简单方法名称进行路由,您可以添加一个带有以下表达式的头:method.name

java.reflect.Method 是不可序列化的。如果您稍后序列化消息,带有 method 表达式的头将丢失。因此,在这些情况下,您可能希望使用 method.namemethod.toString()toString() 方法提供方法的 String 表示,包括参数和返回类型。

从 3.0 版本开始,可以定义 <default-header/> 元素以向网关生成的所有消息添加头,无论调用哪个方法。为方法定义的特定头优先于默认头。此处为方法定义的特定头会覆盖服务接口中的任何 @Header 注解。但是,默认头不会覆盖服务接口中的任何 @Header 注解。

网关现在还支持 default-payload-expression,它适用于所有方法(除非被覆盖)。

将方法参数映射到消息

使用上一节中的配置技术可以控制方法参数如何映射到消息元素(有效负载和头)。当不使用显式配置时,会使用某些约定来执行映射。在某些情况下,这些约定无法确定哪个参数是有效负载,哪个应该映射到头。考虑以下示例:

public String send1(Object thing1, Map thing2);

public String send2(Map thing1, Map thing2);

在第一种情况下,约定是将第一个参数映射到有效负载(只要它不是 Map),并且第二个参数的内容成为头。

在第二种情况下(或第一种情况下,当参数 thing1 的参数是 Map 时),框架无法确定哪个参数应该作为有效负载。因此,映射失败。这通常可以通过使用 payload-expression@Payload 注解或 @Headers 注解来解决。

或者(以及当约定失效时),您可以完全负责将方法调用映射到消息。为此,请实现 MethodArgsMessageMapper 并通过使用 mapper 属性将其提供给 <gateway/>。映射器映射一个 MethodArgsHolder,它是一个简单的类,包装了 java.reflect.Method 实例和包含参数的 Object[]。当提供自定义映射器时,不允许在网关上使用 default-payload-expression 属性和 <default-header/> 元素。同样,不允许在任何 <method/> 元素上使用 payload-expression 属性和 <header/> 元素。

映射方法参数

以下示例展示了方法参数如何映射到消息,并展示了一些无效配置的示例:

public interface MyGateway {

    void payloadAndHeaderMapWithoutAnnotations(String s, Map<String, Object> map);

    void payloadAndHeaderMapWithAnnotations(@Payload String s, @Headers Map<String, Object> map);

    void headerValuesAndPayloadWithAnnotations(@Header("k1") String x, @Payload String s, @Header("k2") String y);

    void mapOnly(Map<String, Object> map); // the payload is the map and no custom headers are added

    void twoMapsAndOneAnnotatedWithPayload(@Payload Map<String, Object> payload, Map<String, Object> headers);

    @Payload("args[0] + args[1] + '!'")
    void payloadAnnotationAtMethodLevel(String a, String b);

    @Payload("@someBean.exclaim(args[0])")
    void payloadAnnotationAtMethodLevelUsingBeanResolver(String s);

    void payloadAnnotationWithExpression(@Payload("toUpperCase()") String s);

    void payloadAnnotationWithExpressionUsingBeanResolver(@Payload("@someBean.sum(#this)") String s); //  [id="CO1-1"]1

    // invalid
    void twoMapsWithoutAnnotations(Map<String, Object> m1, Map<String, Object> m2);

    // invalid
    void twoPayloads(@Payload String s1, @Payload String s2);

    // invalid
    void payloadAndHeaderAnnotationsOnSameParameter(@Payload @Header("x") String s);

    // invalid
    void payloadAndHeadersAnnotationsOnSameParameter(@Payload @Headers Map<String, Object> map);

}
<1>  请注意,在此示例中,SpEL 变量 `#this` 指的是参数(在本例中为 `s` 的值)。

XML 等效项看起来有点不同,因为方法参数没有 #this 上下文。但是,表达式可以使用 MethodArgsHolder 根对象的 args 属性来引用方法参数(有关更多信息,请参阅 gateway-expressions),如以下示例所示:

<int:gateway id="myGateway" service-interface="org.something.MyGateway">
  <int:method name="send1" payload-expression="args[0] + 'thing2'"/>
  <int:method name="send2" payload-expression="@someBean.sum(args[0])"/>
  <int:method name="send3" payload-expression="method"/>
  <int:method name="send4">
    <int:header name="thing1" expression="args[2].toUpperCase()"/>
  </int:method>
</int:gateway>

@MessagingGateway 注解

从 4.0 版本开始,网关服务接口可以使用 @MessagingGateway 注解进行标记,而无需为配置定义 <gateway /> xml 元素。以下两组示例比较了配置同一网关的两种方法:

<int:gateway id="myGateway" service-interface="org.something.TestGateway"
      default-request-channel="inputC">
  <int:default-header name="calledMethod" expression="#gatewayMethod.name"/>
  <int:method name="echo" request-channel="inputA" reply-timeout="2" request-timeout="200"/>
  <int:method name="echoUpperCase" request-channel="inputB">
    <int:header name="thing1" value="thing2"/>
  </int:method>
  <int:method name="echoViaDefault"/>
</int:gateway>
@MessagingGateway(name = "myGateway", defaultRequestChannel = "inputC",
		  defaultHeaders = @GatewayHeader(name = "calledMethod",
		                           expression="#gatewayMethod.name"))
public interface TestGateway {

   @Gateway(requestChannel = "inputA", replyTimeout = 2, requestTimeout = 200)
   String echo(String payload);

   @Gateway(requestChannel = "inputB", headers = @GatewayHeader(name = "thing1", value="thing2"))
   String echoUpperCase(String payload);

   String echoViaDefault(String payload);

}

与 XML 版本类似,当 Spring Integration 在组件扫描期间发现这些注解时,它会创建带有其消息传递基础设施的 proxy 实现。要执行此扫描并在应用程序上下文中注册 BeanDefinition,请将 @IntegrationComponentScan 注解添加到 @Configuration 类。标准的 @ComponentScan 基础设施不处理接口。因此,我们引入了自定义的 @IntegrationComponentScan 逻辑来查找接口上的 @MessagingGateway 注解并为其注册 GatewayProxyFactoryBean 实例。另请参阅 注解支持

除了 @MessagingGateway 注解,您还可以使用 @Profile 注解标记服务接口,以避免在未激活该配置文件时创建 bean。

从 6.0 版本开始,带有 @MessagingGateway 的接口也可以用 @Primary 注解标记,以实现相应的配置逻辑,就像任何 Spring @Component 定义一样。

从 6.0 版本开始,@MessagingGateway 接口可以在标准 Spring @Import 配置中使用。这可以作为 @IntegrationComponentScan 或手动 AnnotationGatewayProxyFactoryBean bean 定义的替代方案。

@MessagingGateway6.0 版本开始被 @MessageEndpoint 元注解,并且 name() 属性本质上是 @Compnent.value() 的别名。这样,网关代理的 bean 名称生成策略与扫描和导入组件的标准 Spring 注解配置保持一致。默认的 AnnotationBeanNameGenerator 可以通过 AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR 全局覆盖,或者作为 @IntegrationComponentScan.nameGenerator() 属性。

如果您没有 XML 配置,则至少一个 @Configuration 类需要 @EnableIntegration 注解。有关更多信息,请参阅 配置和 @EnableIntegration

调用无参数方法

当调用网关接口上没有任何参数的方法时,默认行为是从 PollableChannel 接收 Message

但是,有时您可能希望触发无参数方法,以便您可以与不需要用户提供参数的其他下游组件进行交互,例如触发无参数 SQL 调用或存储过程。

要实现发送和接收语义,您必须提供一个有效负载。要生成有效负载,接口上的方法参数不是必需的。您可以使用 @Payload 注解或 XML 中 method 元素上的 payload-expression 属性。以下列表包含一些有效负载的示例:

  • 字面字符串

  • #gatewayMethod.name

  • new java.util.Date()

  • @someBean.someMethod()'s 返回值

以下示例展示了如何使用 @Payload 注解:

public interface Cafe {

    @Payload("new java.util.Date()")
    List<Order> retrieveOpenOrders();

}

您也可以使用 @Gateway 注解。

public interface Cafe {

    @Gateway(payloadExpression = "new java.util.Date()")
    List<Order> retrieveOpenOrders();

}

如果两个注解都存在(并且提供了 payloadExpression),则 @Gateway 获胜。

如果方法没有参数也没有返回值,但包含有效负载表达式,则将其视为仅发送操作。

调用 default 方法

网关代理的接口也可以有 default 方法,从 5.3 版本开始,框架将 DefaultMethodInvokingMethodInterceptor 注入到代理中,用于使用 java.lang.invoke.MethodHandle 方法调用 default 方法,而不是代理。JDK 中的接口,例如 java.util.function.Function,仍然可以用于网关代理,但由于 MethodHandles.Lookup 实例化针对 JDK 类的内部 Java 安全原因,它们的 default 方法无法调用。这些方法也可以被代理(失去其实现逻辑,同时恢复以前的网关代理行为),通过在方法上使用显式的 @Gateway 注解,或者在 @MessagingGateway 注解或 <gateway> XML 组件上使用 proxyDefaultMethods

错误处理

网关调用可能导致错误。默认情况下,下游发生的任何错误都会在网关的方法调用时“原样”重新抛出。例如,考虑以下简单流:

gateway -> service-activator

如果服务激活器调用的服务抛出 MyException(例如),框架会将其包装在 MessagingException 中,并将传递给服务激活器的消息附加到 failedMessage 属性中。因此,框架执行的任何日志记录都具有完整的故障上下文。默认情况下,当网关捕获异常时,MyException 会被解包并抛给调用者。您可以在网关方法声明上配置 throws 子句以匹配原因链中的特定异常类型。例如,如果您想捕获带有下游错误原因的所有消息信息的完整 MessagingException,您应该有一个类似于以下内容的网关方法:

public interface MyGateway {

    void performProcess() throws MessagingException;

}

由于我们鼓励 POJO 编程,您可能不想将调用者暴露给消息传递基础设施。

如果您的网关方法没有 throws 子句,网关会遍历原因树,查找不是 MessagingExceptionRuntimeException。如果找不到,框架会抛出 MessagingException。如果前面讨论的 MyException 的原因是 SomeOtherException 并且您的方法 throws SomeOtherException,网关会进一步解包并将其抛给调用者。

当网关声明没有 service-interface 时,使用内部框架接口 RequestReplyExchanger

考虑以下示例:

public interface RequestReplyExchanger {

	Message<?> exchange(Message<?> request) throws MessagingException;

}

在 5.0 版本之前,此 exchange 方法没有 throws 子句,因此异常被解包。如果您使用此接口并希望恢复以前的解包行为,请改用自定义 service-interface 或自行访问 MessagingExceptioncause

但是,您可能希望记录错误而不是传播它,或者您可能希望将异常视为有效回复(通过将其映射到符合调用者理解的某些“错误消息”契约的消息)。为了实现这一点,网关通过支持 error-channel 属性来支持专用于错误的通道。在以下示例中,一个“转换器”从 Exception 创建一个回复 Message

<int:gateway id="sampleGateway"
    default-request-channel="gatewayChannel"
    service-interface="foo.bar.SimpleGateway"
    error-channel="exceptionTransformationChannel"/>

<int:transformer input-channel="exceptionTransformationChannel"
        ref="exceptionTransformer" method="createErrorResponse"/>

exceptionTransformer 可以是一个简单的 POJO,它知道如何创建预期的错误响应对象。这成为发送回调用者的有效负载。如果需要,您可以在这样的“错误流”中做更多复杂的事情。它可能涉及路由器(包括 Spring Integration 的 ErrorMessageExceptionTypeRouter)、过滤器等。但是,大多数情况下,一个简单的“转换器”应该足够了。

或者,您可能只想记录异常(或将其异步发送到某个地方)。如果您提供单向流,则不会向调用者发送任何内容。如果您想完全抑制异常,可以提供对全局 nullChannel 的引用(本质上是 /dev/null 方法)。最后,如上所述,如果未定义 error-channel,则异常照常传播。

当您使用 @MessagingGateway 注解时(请参阅 <<@MessagingGateway` Annotation,messaging-gateway-annotation>>`),您可以使用 errorChannel 属性。

从 5.0 版本开始,当您使用 void 返回类型(单向流)的网关方法时,error-channel 引用(如果提供)会填充到每个发送消息的标准 errorChannel 头中。此功能允许基于标准 ExecutorChannel 配置(或 QueueChannel)的下游异步流覆盖默认的全局 errorChannel 异常发送行为。以前,您必须使用 @GatewayHeader 注解或 <header> 元素手动指定 errorChannel 头。对于具有异步流的 void 方法,error-channel 属性被忽略。相反,错误消息被发送到默认的 errorChannel

通过简单的 POJI 网关暴露消息传递系统提供了好处,但“隐藏”底层消息传递系统的现实是有代价的,因此您应该考虑某些事情。我们希望我们的 Java 方法尽可能快地返回,而不是无限期地挂起,而调用者正在等待它返回(无论是 void、返回值还是抛出的异常)。当常规方法用作消息传递系统前面的代理时,我们必须考虑到底层消息传递的潜在异步性质。这意味着消息可能被过滤器丢弃,永远无法到达负责产生回复的组件,从而导致网关启动的消息可能永远不会到达。某些服务激活器方法可能导致异常,从而不提供回复(因为我们不生成空消息)。换句话说,多种情况可能导致回复消息永远不会到达。这在消息传递系统中是完全自然的。但是,请考虑对网关方法的影响。网关方法的输入参数被合并到消息中并发送到下游。回复消息将转换为网关方法的返回值。因此,您可能希望确保对于每个网关调用,始终存在一个回复消息。否则,如果 reply-timeout 设置为负值,您的网关方法可能永远不会返回并无限期挂起。处理这种情况的一种方法是使用异步网关(本节稍后解释)。另一种处理方法是依赖默认的 reply-timeout(30 秒)。这样,网关不会挂起超过 reply-timeout 指定的时间,并且如果超时过期则返回“null”。最后,您可能需要考虑设置下游标志,例如服务激活器上的“requires-reply”或过滤器上的“throw-exceptions-on-rejection”。这些选项将在本章的最后一节中更详细地讨论。

如果下游流返回 ErrorMessage,其 payload(一个 Throwable)被视为常规的下游错误。如果配置了 error-channel,它将被发送到错误流。否则,有效负载将抛给网关的调用者。同样,如果 error-channel 上的错误流返回 ErrorMessage,其有效负载将抛给调用者。这同样适用于任何带有 Throwable 有效负载的消息。这在异步情况下可能很有用,当您需要将 Exception 直接传播给调用者时。为此,您可以返回 Exception(作为某些服务的 reply)或抛出它。通常,即使是异步流,框架也会负责将下游流抛出的异常传播回网关。TCP 客户端-服务器多路复用 示例演示了将异常返回给调用者的两种技术。它通过使用带有 group-timeoutaggregator(请参阅 Aggregator 和 Group Timeout)和丢弃流上的 MessagingTimeoutException 回复来模拟等待线程的套接字 IO 错误。

网关超时

网关有两个超时属性:requestTimeoutreplyTimeout。请求超时仅适用于通道可能阻塞的情况(例如,已满的有界 QueueChannel)。replyTimeout 值是网关等待回复或返回 null 的时间。它默认为无限。

超时可以设置为网关上所有方法的默认值(defaultRequestTimeoutdefaultReplyTimeout),或者在 MessagingGateway 接口注解上设置。单个方法可以覆盖这些默认值(在 <method/> 子元素中)或在 @Gateway 注解上设置。

从 5.0 版本开始,超时可以定义为表达式,如以下示例所示:

@Gateway(payloadExpression = "args[0]", requestChannel = "someChannel",
        requestTimeoutExpression = "args[1]", replyTimeoutExpression = "args[2]")
String lateReply(String payload, long requestTimeout, long replyTimeout);

评估上下文具有 BeanResolver(使用 @someBean 引用其他 bean),并且 #root 对象的 args 数组属性可用。有关此根对象的更多信息,请参阅 gateway-expressions。当使用 XML 配置时,超时属性可以是 long 值或 SpEL 表达式,如以下示例所示:

<method name="someMethod" request-channel="someRequestChannel"
                      payload-expression="args[0]"
                      request-timeout="1000"
                      reply-timeout="args[1]">
</method>

异步网关

作为一种模式,消息网关提供了一种很好的方式来隐藏消息特定代码,同时仍然暴露消息传递系统的全部功能。如 gateway-proxyGatewayProxyFactoryBean 提供了一种方便的方式来通过服务接口暴露代理,为您提供基于 POJO 的消息传递系统访问(基于您自己域中的对象、原语/字符串或其他对象)。但是,当网关通过返回值的简单 POJO 方法暴露时,它意味着对于每个请求消息(在方法调用时生成),必须有一个回复消息(在方法返回时生成)。由于消息传递系统本质上是异步的,您可能无法始终保证“每个请求,总会有回复”的契约。Spring Integration 2.0 引入了对异步网关的支持,它提供了一种方便的方式来启动流,当您可能不知道是否需要回复或回复到达需要多长时间时。

为了处理这些类型的场景,Spring Integration 使用 java.util.concurrent.Future 实例来支持异步网关。

从 XML 配置来看,没有任何变化,您仍然像定义常规网关一样定义异步网关,如以下示例所示:

<int:gateway id="mathService"
     service-interface="org.springframework.integration.sample.gateway.futures.MathServiceGateway"
     default-request-channel="requestChannel"/>

但是,网关接口(服务接口)有点不同,如下所示:

public interface MathServiceGateway {

  Future<Integer> multiplyByTwo(int i);

}

如前面的示例所示,网关方法的返回类型是 Future。当 GatewayProxyFactoryBean 看到网关方法的返回类型是 Future 时,它会立即切换到异步模式,使用 AsyncTaskExecutor。这就是差异的程度。对此类方法的调用总是立即返回一个 Future 实例。然后您可以按照自己的节奏与 Future 交互以获取结果、取消等。此外,与任何其他 Future 实例的使用一样,调用 get() 可能会显示超时、执行异常等。以下示例展示了如何使用从异步网关返回的 Future

MathServiceGateway mathService = ac.getBean("mathService", MathServiceGateway.class);
Future<Integer> result = mathService.multiplyByTwo(number);
// do something else here since the reply might take a moment
int finalResult =  result.get(1000, TimeUnit.SECONDS);

有关更详细的示例,请参阅 Spring Integration 示例中的 async-gateway 示例。

此外,从 6.5 版本开始,Java DSL gateway() 运算符完全支持 async(true) 行为。在内部,为 GatewayProxyFactoryBean 提供了 AsyncRequestReplyExchanger 服务接口。由于 AsyncRequestReplyExchanger 契约是 CompletableFuture<Message<?>>,因此整个请求-回复都以异步方式执行。此行为很有用,例如,在分发器-聚合器场景中,当必须为每个项目调用另一个流时。但是,顺序并不重要 - 只有在所有处理之后它们在聚合器上的分组聚集。

AsyncTaskExecutor

默认情况下,GatewayProxyFactoryBean 在为任何返回类型为 Future 的网关方法提交内部 AsyncInvocationTask 实例时,使用 org.springframework.core.task.SimpleAsyncTaskExecutor。但是,<gateway/> 元素配置中的 async-executor 属性允许您提供对 Spring 应用程序上下文中可用的任何 java.util.concurrent.Executor 实现的引用。

(默认)SimpleAsyncTaskExecutor 支持 FutureCompletableFuture 返回类型。请参阅 <<`CompletableFuture`,gw-completable-future>>。尽管存在默认执行器,但提供外部执行器通常很有用,这样您就可以在日志中识别其线程(使用 XML 时,线程名称基于执行器的 bean 名称),如以下示例所示:

@Bean
public AsyncTaskExecutor exec() {
    SimpleAsyncTaskExecutor simpleAsyncTaskExecutor = new SimpleAsyncTaskExecutor();
    simpleAsyncTaskExecutor.setThreadNamePrefix("exec-");
    return simpleAsyncTaskExecutor;
}

@MessagingGateway(asyncExecutor = "exec")
public interface ExecGateway {

    @Gateway(requestChannel = "gatewayChannel")
    Future<?> doAsync(String foo);

}

如果您希望返回不同的 Future 实现,您可以提供一个自定义执行器,或者完全禁用执行器,并在下游流的回复消息有效负载中返回 Future。要禁用执行器,请在 GatewayProxyFactoryBean 中将其设置为 null(通过使用 setAsyncTaskExecutor(null))。当使用 XML 配置网关时,使用 async-executor=""。当使用 @MessagingGateway 注解进行配置时,使用类似于以下代码的代码:

@MessagingGateway(asyncExecutor = AnnotationConstants.NULL)
public interface NoExecGateway {

    @Gateway(requestChannel = "gatewayChannel")
    Future<?> doAsync(String foo);

}

如果返回类型是特定的具体 Future 实现或配置的执行器不支持的其他子接口,则流在调用者的线程上运行,并且流必须在回复消息有效负载中返回所需的类型。

CompletableFuture

从 4.2 版本开始,网关方法现在可以返回 CompletableFuture<?>。当返回此类型时,有两种操作模式:

  • 当提供了异步执行器并且返回类型恰好是 CompletableFuture(而不是子类)时,框架在执行器上运行任务并立即向调用者返回一个 CompletableFutureCompletableFuture.supplyAsync(Supplier<U> supplier, Executor executor) 用于创建 Future。

  • 当异步执行器显式设置为 null 并且返回类型是 CompletableFuture 或返回类型是 CompletableFuture 的子类时,流在调用者的线程上调用。在这种情况下,下游流预计会返回适当类型的 CompletableFuture

使用场景

在以下场景中,调用者线程立即返回一个 CompletableFuture<Invoice>,当下游流回复网关(带有一个 Invoice 对象)时,该 Future 完成。

CompletableFuture<Invoice> order(Order order);
<int:gateway service-interface="something.Service" default-request-channel="orders" />

在以下场景中,当下游流将其作为回复网关的有效负载提供时,调用者线程返回一个 CompletableFuture<Invoice>。当发票准备好时,其他一些进程必须完成该 Future。

CompletableFuture<Invoice> order(Order order);
<int:gateway service-interface="foo.Service" default-request-channel="orders"
    async-executor="" />

在以下场景中,当下游流将其作为回复网关的有效负载提供时,调用者线程返回一个 CompletableFuture<Invoice>。当发票准备好时,其他一些进程必须完成该 Future。如果启用了 DEBUG 日志记录,则会发出一条日志条目,指示异步执行器不能用于此场景。

MyCompletableFuture<Invoice> order(Order order);
<int:gateway service-interface="foo.Service" default-request-channel="orders" />

CompletableFuture 实例可用于对回复执行额外的操作,如以下示例所示:

CompletableFuture<String> process(String data);

...

CompletableFuture result = process("foo")
    .thenApply(t -> t.toUpperCase());

...

String out = result.get(10, TimeUnit.SECONDS);

Reactor Mono

从 5.0 版本开始,GatewayProxyFactoryBean 允许在网关接口方法中使用 Project Reactor,使用 Mono<T> 返回类型。内部的 AsyncInvocationTask 被包装在 Mono.fromCallable() 中。

Mono 可以用来稍后检索结果(类似于 Future<?>),或者您可以在结果返回到网关时通过调用您的 Consumer 来使用调度器进行消费。

Mono 不会被框架立即刷新。因此,在网关方法返回之前,底层消息流不会启动(与 Future<?> Executor 任务一样)。当 Mono 被订阅时,流开始。或者,Mono(作为一个“可组合的”)可能是 Reactor 流的一部分,当 subscribe() 与整个 Flux 相关时。以下示例展示了如何使用 Project Reactor 创建网关:

@MessagingGateway
public interface TestGateway {

    @Gateway(requestChannel = "multiplyChannel")
    Mono<Integer> multiply(Integer value);

}

@ServiceActivator(inputChannel = "multiplyChannel")
public Integer multiply(Integer value) {
    return value * 2;
}

其中这样的网关可以在处理 Flux 数据的服务中使用:

@Autowired
TestGateway testGateway;

public void hadnleFlux() {
    Flux.just("1", "2", "3", "4", "5")
            .map(Integer::parseInt)
            .flatMap(this.testGateway::multiply)
            .collectList()
            .subscribe(System.out::println);
}

另一个使用 Project Reactor 的示例是一个简单的回调场景,如以下示例所示:

Mono<Invoice> mono = service.process(myOrder);

mono.subscribe(invoice -> handleInvoice(invoice));

调用线程继续执行,当流完成时调用 handleInvoice()

另请参阅 Kotlin 协程 了解更多信息。

下游流返回异步类型

如上文 <<`AsyncTaskExecutor`,gateway-asynctaskexecutor>> 部分所述,如果您希望某些下游组件返回带有异步有效负载(FutureMono 等)的消息,则必须显式将异步执行器设置为 null(或使用 XML 配置时为 "")。然后流将在调用者线程上调用,并且结果可以稍后检索。

异步 void 返回类型

消息网关方法可以这样声明:

@MessagingGateway
public interface MyGateway {

    @Gateway(requestChannel = "sendAsyncChannel")
    @Async
    void sendAsync(String payload);

}

但是下游异常不会传播回调用者。为了确保下游流调用的异步行为和异常传播到调用者,从 6.0 版本开始,框架支持 Future<Void>Mono<Void> 返回类型。用例类似于前面描述的普通 void 返回类型的发送即忘行为,但不同之处在于流执行是异步的,并且返回的 Future(或 Mono)根据 send 操作结果完成为 null 或异常。

如果 Future<Void> 是精确的下游流回复,那么网关的 asyncExecutor 选项必须设置为 null(@MessagingGateway 配置为 AnnotationConstants.NULL),并且 send 部分在生产者线程上执行。回复取决于下游流配置。这样,由目标应用程序正确生成 Future<Void> 回复。Mono 用例已经超出了框架线程控制,因此将 asyncExecutor 设置为 null 没有意义。在那里,Mono<Void> 作为请求-回复网关操作的结果必须配置为网关方法的 Mono<?> 返回类型。

没有响应到达时的网关行为

gateway-proxy,网关提供了一种通过 POJO 方法调用与消息传递系统交互的便捷方式。然而,通常预期总是返回(即使带有异常)的典型方法调用可能并不总是与消息交换一一对应(例如,回复消息可能不会到达——这相当于方法没有返回)。

本节的其余部分涵盖了各种场景以及如何使网关表现得更可预测。可以配置某些属性以使同步网关行为更可预测,但其中一些可能并不总是像您期望的那样工作。其中之一是 reply-timeout(方法级别或网关级别的 default-reply-timeout)。我们检查 reply-timeout 属性,看看它在各种场景中如何以及不能如何影响同步网关的行为。我们检查单线程场景(所有下游组件都通过直接通道连接)和多线程场景(例如,在下游的某个地方,您可能有一个可轮询或执行器通道,它打破了单线程边界)。

下游长时间运行的进程

同步网关,单线程

如果下游组件仍在运行(可能是因为无限循环或慢速服务),设置 reply-timeout 没有效果,并且网关方法调用不会返回,直到下游服务退出(通过返回或抛出异常)。

同步网关,多线程

如果下游组件在多线程消息流中仍在运行(可能是因为无限循环或慢速服务),设置 reply-timeout 会产生效果,允许网关方法调用在达到超时后返回,因为 GatewayProxyFactoryBean 会轮询回复通道,等待消息直到超时过期。但是,如果在实际回复生成之前达到超时,可能会导致网关方法返回“null”。您应该明白,回复消息(如果生成)是在网关方法调用可能已经返回之后发送到回复通道的,因此您必须意识到这一点并在设计流时考虑它。

另请参阅 errorOnTimeout 属性,以便在超时发生时抛出 MessageTimeoutException 而不是返回 null

下游组件返回“null”

同步网关 - 单线程

如果下游组件返回“null”并且 reply-timeout 已配置为负值,则网关方法调用将无限期挂起,除非在可能返回“null”的下游组件(例如服务激活器)上设置了 requires-reply 属性。在这种情况下,将抛出异常并传播到网关。

同步网关 - 多线程

行为与前一种情况相同。

下游组件返回签名是 'void',而网关方法签名是非 'void'

同步网关 - 单线程

如果下游组件返回“void”并且 reply-timeout 已配置为负值,则网关方法调用将无限期挂起。

同步网关 - 多线程

行为与前一种情况相同。

下游组件导致运行时异常

同步网关 - 单线程

如果下游组件抛出运行时异常,则异常将通过错误消息传播回网关并重新抛出。

同步网关 - 多线程

行为与前一种情况相同。

您应该明白,默认情况下,reply-timeout 是无界的。因此,如果您将 reply-timeout 设置为负值,您的网关方法调用可能会无限期挂起。因此,为确保您分析您的流,并且即使远程存在其中一种情况的可能性,您也应该将 reply-timeout 属性设置为一个“安全”值。默认值为 30 秒。更好的是,您可以将下游组件的 requires-reply 属性设置为“true”,以确保及时响应,方法是在该下游组件内部返回 null 时立即抛出异常。但是,您还应该意识到,在某些情况下(请参阅 long-running-process-downstream),reply-timeout 没有帮助。这意味着分析您的消息流并决定何时使用同步网关而不是异步网关也很重要。如 前面所述,后一种情况是定义返回 Future 实例的网关方法。然后您就可以保证收到该返回值,并且可以更精细地控制调用的结果。此外,在处理路由器时,您应该记住,将 resolution-required 属性设置为“true”会导致路由器在无法解析特定通道时抛出异常。同样,在处理过滤器时,您可以设置 throw-exception-on-rejection 属性。在所有这些情况下,生成的流的行为就像它包含一个带有“requires-reply”属性的服务激活器。换句话说,它有助于确保网关方法调用的及时响应。

您应该明白,计时器在线程返回到网关时启动——也就是说,当流完成或消息传递给另一个线程时。此时,调用线程开始等待回复。如果流是完全同步的,则回复立即可用。对于异步流,线程最多等待这段时间。

从 6.2 版本开始,MethodInvocationGateway 内部 MessagingGatewaySupport 扩展的 errorOnTimeout 属性在 @MessagingGatewayGatewayEndpointSpec 上公开。此选项的含义与 端点摘要 章末解释的任何入站网关的含义完全相同。换句话说,将此选项设置为 true 会导致在接收超时耗尽时,从发送-接收网关操作中抛出 MessageTimeoutException,而不是返回 null

有关通过 IntegrationFlow 定义网关的选项,请参阅 Java DSL 章中的 IntegrationFlow 作为网关