9.3 创建Email集成流

我们决定Taco Cloud应该允许客户通过Email提交taco设计和创建订单。我们发放传单并在报纸上刊登外卖广告,邀请每个人通过Email发送taco订单。这非常成功!但是,令人遗憾的是,它过于成功了。有太多的Email涌了进来,我们不得不申请临时帮助,让别人阅读所有的Email并将订单提交到订单系统中。

在本节中,我们将会实现一个集成流,轮询Taco Cloud的taco订单Email的收件箱、解析Email中的订单细节并将订单提交给Taco Cloud来进行处理。简而言之,在我们所创建的集成流中,入站通道适配器将会使用Email端点模块摄取TacoCloud收件箱中的Email到集成流中。

集成流的下一步会将Email解析为订单对象,这些订单对象会被传递给另一个处理器,从而将订单提交至Taco Cloud的REST API中,在这里我们会像其他订单那样处理它们。首先,我们定义一个简单的配置属性类,它会捕获处理Taco CloudEmail的特定信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@ConfigurationProperties(prefix="tacocloud.email")
@Component
public class EmailProperties {
private String username;
private String password;
private String host;
private String mailbox;
private long pollRate = 30000;
public String getImapUrl() {
return String.format("imaps://%s:%s@%s/%s",
this.username, this.password, this.host, this.mailbox);
}
}

我们可以看到,EmailProperties会捕获生成IMAP URL的属性。这个流会使用这个URL连接Taco Cloud Email服务器并轮询Email。在捕获的属性中包括Email用户的用户名和密码以及IMAP服务器的主机、要轮询的邮箱以及邮箱轮询的频率(默认为30秒)。

EmailProperties在类级别使用了@ConfigurationProperties注解,并将prefix属性设置为tacocloud.email。这意味着,我们可以在application.yml文件中按照下述方式配置消费Email的详细信息:

1
2
3
4
5
6
7
tacocloud:
email:
host: imap.tacocloud.com
mailbox: INBOX
username: taco-in-flow
password: 1L0v3T4c0s
poll-rate: 10000

现在,我们使用EmailProperties来配置集成流。我们想要创建的流大致如图9.10所示。

image-20211016100608960

图9.10 通过Email接受taco订单的集成流

我们有两种方案来定义这个流。

  • 在Taco Cloud应用中进行定义:在流的结束点,服务激活器要调用我们之前定义的创建订单的repository。
  • 在单独的应用中进行定义:在流的结束点,服务激活器要发送POST请求到Taco Cloud API以提交taco订单。

不管选择哪种方式,除了服务激活器的实现方式之外,对流的本身影响并不大。但是,因为我们需要一些表示taco、订单和配料的类型,它们与Taco Cloud主应用可能会略微有所差异,所以我们会在一个单独的应用中定义集成流,避免与已有的领域类型相混淆。

我们还可以选择使用XML配置、Java配置或者Java DSL来定义流。我更喜欢DSL的优雅,所以在这里将会使用这种方案。如果你想要一些额外的挑战,也可以选择其他配置风格编写流的定义。现在,我们看一下taco Email订单流的Java DSL配置,如程序清单9.5所示。

程序清单9.5 定义接收Email并将其提交为订单的集成流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package tacos.email;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.Pollers;
@Configuration
public class TacoOrderEmailIntegrationConfig {
@Bean
public IntegrationFlow tacoOrderEmailFlow(
EmailProperties emailProps,
EmailToOrderTransformer emailToOrderTransformer,
OrderSubmitMessageHandler orderSubmitHandler) {
return IntegrationFlows
.from(Mail.imapInboundAdapter(emailProps.getImapUrl()),
e -> e.poller(
Pollers.fixedDelay(emailProps.getPollRate())))
.transform(emailToOrderTransformer)
.handle(orderSubmitHandler)
.get();
}
}

根据tacoOrderEmailFlow()方法的定义,taco Email订单流由3个不同的组件组成。

  • MAP Email入站通道适配器:使用IMP URL创建,而URL是根据EmailProperties的getImapUrl()方法创建的,并且会根据EmailProperties中设置的pollRate属性进行轮询。传入的Email会传递给一个通道,然后连接到转换器。
  • 将Email转换成订单对象的转换器:转换器是通过EmailToOrderTransformer实现的,它会注入tacoOrderEmailFlow()方法中。转换所形成的订单会通过另外一个通道传递给最后一个组件。
  • 处理器(作为出站通道适配器):处理器接受订单对象并将其提交至TacoCloud的REST API。

我们只有将Email端点模块作为依赖项添加到项目构建文件中,才能调用Mail.imap InboundAdapter()。Maven依赖如下所示:

1
2
3
4
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-file</artifactId>
</dependency>

EmailToOrderTransformer是Spring Integration Transformer接口的实现,扩展了AbstractMailMessageTransformer(如程序清单9.6所示)。

程序清单9.6 使用集成转换器将传入的Email转换为taco订单

1
2
3
4
5
6
7
8
9
10
11
@Component
public class EmailToOrderTransformer
extends AbstractMailMessageTransformer<Order> {
@Override
protected AbstractIntegrationMessageBuilder<Order>
doTransform(Message mailMessage) throws Exception {
Order tacoOrder = processPayload(mailMessage);
return MessageBuilder.withPayload(tacoOrder);
}
...
}

AbstractMailMessageTransformer是一个很便利的基类,适用于载荷为Email的消息。它会抽取传入消息Email的信息,并将它放到一个Message对象中,传递给doTransform()方法。在doTransform()方法中,我们将Message对象传递给一个名为processPayload()的private方法,将Email解析为Order对象。这个Order对象尽管和主Taco Cloud应用中的Order对象有些相似,但是并不完全相同,这里更加简单一些:

1
2
3
4
5
6
7
8
9
10
11
12
package tacos.email;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
@Data
public class Order {
private final String email;
private List<Taco> tacos = new ArrayList<>();
public void addTaco(Taco taco) {
this.tacos.add(taco);
}
}

这个Order类不包含客户完整的投递信息和账单信息,而是只携带了客户的Email地址(通过传入的Email获取的)。

将Email解析成订单是一项非常重要的任务。实际上,即便最简单的实现也需要几十行代码。这些代码对于进一步讨论Spring Integration和如何实现转换器并没有任何助益。所以,为了节省空间,我在这里省略了processPayload()方法的细节。

EmailToOrderTransformer做的最后一件事情就是返回一个MessageBuilder,让消息的载荷中包含Order对象。MessageBuilder所生成的消息会发送至集成流的最后一个组件:将订单提交至Taco Cloud API的消息处理器。OrderSubmitMessageHandler实现了Spring Integration的GenericHandler,它会处理带有Order载荷的消息,如程序清单9.7所示。

程序清单9.7 通过消息处理器将订单提交至Taco Cloud API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package tacos.email;
import java.util.Map;
import org.springframework.integration.handler.GenericHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
public class OrderSubmitMessageHandler
implements GenericHandler<Order> {
private RestTemplate rest;
private ApiProperties apiProps;
public OrderSubmitMessageHandler(
ApiProperties apiProps, RestTemplate rest) {
this.apiProps = apiProps;
this.rest = rest;
}
@Override
public Object handle(Order order, Map<String, Object> headers) {
rest.postForObject(apiProps.getUrl(), order, String.class);
return null;
}
}

为了满足GenericHandler接口的要求,OrderSubmitMessageHandler重写了handle()方法,这个方法接收传入的Order对象,并使用注入的RestTemplate利用POST请求将Order提交至ApiProperties对象指定的URL。最后,handle()方法返回null,表明这个处理器是流的终点。

这里使用ApiProperties避免在postForObject()时硬编码URL。它是一个配置属性类,如下所示:

1
2
3
4
5
6
@Data
@ConfigurationProperties(prefix="tacocloud.api")
@Component
public class ApiProperties {
private String url;
}

在application.yml中,Taco Cloud API的URL可能会配置如下:

1
2
3
tacocloud:
api:
url: http://api.tacocloud.com

为了让这个应用能够使用RestTemplate,并自动注入OrderSubmitMessageHandler中,我们需要在项目的构建文件中添加SpringBoot web starter依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

这不仅会将RestTemplate添加到类路径中,还会触发Spring MVC的自动配置功能。作为独立的Spring Integration流,这个应用并不需要Spring MVC,更不需要自动配置所提供的嵌入式Tomcat。所以,我们可以在application.yml中通过如下的配置条目禁用Spring MVC的自动配置:

1
2
3
spring:
main:
web-application-type: none

spring.main.web-application-type属性可以设置为servlet、reactive或none。当Spring MVC位于类路径之中时,自动配置功能会将其设置为servlet。我们在这里将其重写为none,所以Spring MVC和Tomcat将不会进行自动配置(我们将会在第11章介绍反应式Web应用是什么样子的)。