10.3 Tạo một luồng tích hợp email
Bạn đã quyết định rằng Taco Cloud nên cho phép khách hàng gửi thiết kế taco và đặt hàng qua email. Bạn phát tờ rơi và đặt quảng cáo mang về trên báo mời mọi người gửi đơn đặt hàng taco qua email. Điều này cực kỳ thành công! Đáng tiếc là… thành công quá mức. Có quá nhiều email đổ về khiến bạn phải thuê thêm nhân lực tạm thời chỉ để đọc email và nhập thông tin đơn hàng vào hệ thống đặt hàng.
Trong phần này, bạn sẽ triển khai một luồng tích hợp (integration flow) để kiểm tra hộp thư đến của Taco Cloud, phân tích nội dung email để lấy thông tin đơn hàng, và gửi các đơn hàng đó vào Taco Cloud để xử lý. Tóm lại, luồng tích hợp mà bạn cần sẽ sử dụng một bộ điều hợp kênh đầu vào (inbound channel adapter) từ mô-đun email endpoint để lấy email từ hộp thư Taco Cloud đưa vào luồng tích hợp.
Bước tiếp theo trong luồng sẽ phân tích email thành các đối tượng đơn hàng (order object), sau đó gửi chúng tới một trình xử lý khác để gửi đơn hàng đến API REST của Taco Cloud, nơi chúng sẽ được xử lý như bất kỳ đơn hàng nào khác. Đầu tiên, hãy định nghĩa một lớp cấu hình đơn giản để lưu các thuộc tính xử lý email của Taco Cloud, như minh họa bên dưới:
package tacos.email;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import lombok.Data;
@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);
}
}Lớp EmailProperties sẽ lưu giữ các thuộc tính được sử dụng để tạo ra một URL IMAP. Luồng tích hợp sẽ dùng URL này để kết nối tới máy chủ email của Taco Cloud và kiểm tra email mới. Các thuộc tính bao gồm tên người dùng và mật khẩu email, tên máy chủ IMAP, hộp thư cần kiểm tra, và tần suất kiểm tra hộp thư (mặc định mỗi 30 giây).
Lớp EmailProperties được chú thích ở cấp lớp bằng @ConfigurationProperties với tiền tố tacocloud.email. Điều này có nghĩa là bạn có thể cấu hình thông tin email trong file application.yml như sau:
tacocloud:
email:
host: imap.tacocloud.com
mailbox: INBOX
username: taco-in-flow
password: 1L0v3T4c0s
poll-rate: 10000Tất nhiên, cấu hình email ở đây chỉ là ví dụ. Bạn sẽ cần điều chỉnh để phù hợp với thông tin máy chủ email thực tế mà bạn sử dụng.
Bạn cũng có thể thấy IDE cảnh báo về “unknown property”. Đó là vì IDE không có metadata để hiểu các thuộc tính tùy chỉnh. Những cảnh báo đó không ảnh hưởng đến việc biên dịch, và bạn có thể bỏ qua chúng. Tuy nhiên, nếu bạn muốn loại bỏ các cảnh báo này, hãy thêm phần phụ thuộc sau vào file build (cũng có sẵn như một tuỳ chọn trong Spring Initializr có tên là “Spring Configuration Processor”):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>Phụ thuộc này hỗ trợ tự động tạo metadata cho các thuộc tính cấu hình tùy chỉnh như những gì bạn đang dùng để cấu hình máy chủ email.
Bây giờ, hãy sử dụng EmailProperties để cấu hình luồng tích hợp. Luồng bạn muốn tạo ra sẽ giống với hình 10.10 bên dưới:
Hình 10.10 Luồng tích hợp để nhận đơn hàng taco qua email
Bạn có hai lựa chọn để định nghĩa luồng này:
- Định nghĩa nó trong chính ứng dụng Taco Cloud. Cuối luồng, một service activator sẽ gọi vào các repository mà bạn đã định nghĩa để tạo đơn hàng.
- Định nghĩa nó như một ứng dụng riêng biệt. Cuối luồng, một service activator sẽ gửi một yêu cầu POST đến API của Taco Cloud để gửi đơn hàng.
Dù bạn chọn cách nào thì cũng không ảnh hưởng nhiều đến luồng, ngoại trừ cách bạn triển khai service activator. Nhưng vì bạn sẽ cần các kiểu dữ liệu đại diện cho taco, đơn hàng và nguyên liệu—có thể hơi khác với các lớp trong ứng dụng chính Taco Cloud—nên bạn sẽ định nghĩa luồng tích hợp trong một ứng dụng riêng biệt để tránh nhầm lẫn với các kiểu dữ liệu hiện tại.
Bạn cũng có thể định nghĩa luồng bằng cấu hình XML, cấu hình Java hoặc DSL Java. Cá nhân tôi thích sự gọn gàng của Java DSL, nên chúng ta sẽ dùng cách này. Tuy nhiên, bạn hoàn toàn có thể thử với các cách cấu hình khác nếu muốn thử thách thêm. Bây giờ, hãy cùng xem cấu hình Java DSL cho luồng nhận đơn hàng taco qua email được thể hiện ở phần tiếp theo.
Listing 10.5 Định nghĩa một luồng tích hợp để nhận email và gửi chúng như các đơn hàng
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;
import org.springframework.integration.mail.dsl.Mail;
@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();
}
}Luồng email đặt hàng taco, như được định nghĩa trong phương thức tacoOrderEmailFlow(), bao gồm ba thành phần riêng biệt sau:
- Bộ điều hợp kênh đầu vào email IMAP — Bộ điều hợp kênh này được tạo bằng URL IMAP được sinh ra từ phương thức
getImapUrl()của lớpEmailPropertiesvà thực hiện kiểm tra email định kỳ theo thời gian trễ được chỉ định trong thuộc tínhpollRatecủaEmailProperties. Các email nhận được sẽ được chuyển đến một kênh kết nối đến bộ chuyển đổi (transformer). - Bộ chuyển đổi chuyển email thành đối tượng đơn hàng — Bộ chuyển đổi này được triển khai trong lớp
EmailToOrderTransformer, được inject vào phương thứctacoOrderEmailFlow(). Các đơn hàng sau khi chuyển đổi sẽ được chuyển tiếp đến thành phần cuối cùng thông qua một kênh khác. - Trình xử lý (đóng vai trò như bộ điều hợp kênh đầu ra) — Trình xử lý này nhận một đối tượng đơn hàng và gửi nó tới REST API của Taco Cloud.
Lệnh gọi đến Mail.imapInboundAdapter() khả dụng là nhờ bạn đã thêm mô-đun endpoint Email làm dependency trong cấu hình build của dự án. Phần phụ thuộc Maven trông như sau:
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mail</artifactId>
</dependency>Lớp EmailToOrderTransformer là một triển khai của interface Transformer trong Spring Integration, bằng cách mở rộng lớp AbstractMailMessageTransformer (được trình bày trong listing bên dưới).
Listing 10.6 Chuyển đổi email đến thành đơn hàng taco bằng transformer trong Spring Integration
package tacos.email;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import org.apache.commons.text.similarity.LevenshteinDistance;
import org.springframework.integration.mail.transformer
.AbstractMailMessageTransformer;
import org.springframework.integration.support
.AbstractIntegrationMessageBuilder;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.stereotype.Component;
@Component
public class EmailToOrderTransformer
extends AbstractMailMessageTransformer<EmailOrder> {
private static final String SUBJECT_KEYWORDS = "TACO ORDER";
@Override
protected AbstractIntegrationMessageBuilder<EmailOrder>
doTransform(Message mailMessage) throws Exception {
EmailOrder tacoOrder = processPayload(mailMessage);
return MessageBuilder.withPayload(tacoOrder);
}
private EmailOrder processPayload(Message mailMessage) {
try {
String subject = mailMessage.getSubject();
if (subject.toUpperCase().contains(SUBJECT_KEYWORDS)) {
String email =
((InternetAddress) mailMessage.getFrom()[0]).getAddress();
String content = mailMessage.getContent().toString();
return parseEmailToOrder(email, content);
}
} catch (MessagingException e) {
} catch (IOException e) {}
return null;
}
private EmailOrder parseEmailToOrder(String email, String content) {
EmailOrder order = new EmailOrder(email);
String[] lines = content.split("\\r?\\n");
for (String line : lines) {
if (line.trim().length() > 0 && line.contains(":")) {
String[] lineSplit = line.split(":");
String tacoName = lineSplit[0].trim();
String ingredients = lineSplit[1].trim();
String[] ingredientsSplit = ingredients.split(",");
List<String> ingredientCodes = new ArrayList<>();
for (String ingredientName : ingredientsSplit) {
String code = lookupIngredientCode(ingredientName.trim());
if (code != null) {
ingredientCodes.add(code);
}
}
Taco taco = new Taco(tacoName);
taco.setIngredients(ingredientCodes);
order.addTaco(taco);
}
}
return order;
}
private String lookupIngredientCode(String ingredientName) {
for (Ingredient ingredient : ALL_INGREDIENTS) {
String ucIngredientName = ingredientName.toUpperCase();
if (LevenshteinDistance.getDefaultInstance()
.apply(ucIngredientName, ingredient.getName()) < 3 ||
ucIngredientName.contains(ingredient.getName()) ||
ingredient.getName().contains(ucIngredientName)) {
return ingredient.getCode();
}
}
return null;
}
private static Ingredient[] ALL_INGREDIENTS = new Ingredient[] {
new Ingredient("FLTO", "FLOUR TORTILLA"),
new Ingredient("COTO", "CORN TORTILLA"),
new Ingredient("GRBF", "GROUND BEEF"),
new Ingredient("CARN", "CARNITAS"),
new Ingredient("TMTO", "TOMATOES"),
new Ingredient("LETC", "LETTUCE"),
new Ingredient("CHED", "CHEDDAR"),
new Ingredient("JACK", "MONTERREY JACK"),
new Ingredient("SLSA", "SALSA"),
new Ingredient("SRCR", "SOUR CREAM")
};
}AbstractMailMessageTransformer là một lớp cơ sở tiện lợi để xử lý các message có payload là email. Nó sẽ lo việc trích xuất thông tin email từ message đầu vào và truyền nó dưới dạng một đối tượng Message vào phương thức doTransform().
Trong phương thức doTransform(), bạn sẽ truyền đối tượng Message này vào một phương thức private có tên processPayload() để phân tích email và tạo ra một đối tượng EmailOrder. Mặc dù có phần giống nhau, nhưng đối tượng EmailOrder này không giống hoàn toàn với đối tượng TacoOrder đã được sử dụng trong ứng dụng chính của Taco Cloud; nó đơn giản hơn một chút, như minh họa bên dưới:
package tacos.email;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
@Data
public class EmailOrder {
private final String email;
private List<Taco> tacos = new ArrayList<>();
public void addTaco(Taco taco) {
this.tacos.add(taco);
}
}Thay vì chứa toàn bộ thông tin giao hàng và thanh toán của khách hàng, lớp EmailOrder này chỉ lưu email của khách hàng, được lấy từ email nhận được.
Phân tích email thành đơn hàng taco không phải là một nhiệm vụ đơn giản. Ngay cả một cách triển khai sơ sài cũng có thể cần đến hàng chục dòng mã. Và những dòng mã đó không giúp ích gì thêm cho phần trình bày về Spring Integration hay cách xây dựng một transformer. Vì vậy, để tiết kiệm không gian, phần chi tiết của phương thức processPayload() sẽ được lược bỏ.
Việc cuối cùng mà EmailToOrderTransformer thực hiện là trả về một đối tượng MessageBuilder với payload chứa đối tượng EmailOrder. Message được tạo ra bởi MessageBuilder này sẽ được gửi đến thành phần cuối cùng trong luồng tích hợp: một message handler thực hiện gửi đơn hàng tới API của Taco Cloud. Lớp OrderSubmitMessageHandler, như được minh họa trong listing kế tiếp, triển khai GenericHandler của Spring Integration để xử lý message với payload là EmailOrder.
Listing 10.7 Gửi đơn hàng đến API của Taco Cloud thông qua message handler
package tacos.email;
import org.springframework.integration.handler.GenericHandler;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
public class OrderSubmitMessageHandler
implements GenericHandler<EmailOrder> {
private RestTemplate rest;
private ApiProperties apiProps;
public OrderSubmitMessageHandler(ApiProperties apiProps, RestTemplate rest) {
this.apiProps = apiProps;
this.rest = rest;
}
@Override
public Object handle(EmailOrder order, MessageHeaders headers) {
rest.postForObject(apiProps.getUrl(), order, String.class);
return null;
}
}Để đáp ứng các yêu cầu của interface GenericHandler, lớp OrderSubmitMessageHandler ghi đè phương thức handle(). Phương thức này nhận đối tượng EmailOrder đầu vào và sử dụng một RestTemplate được inject để gửi EmailOrder đó bằng một yêu cầu POST đến URL được lưu trong một đối tượng ApiProperties (cũng được inject). Cuối cùng, phương thức handle() trả về null để đánh dấu rằng đây là điểm kết thúc của luồng.
ApiProperties được dùng để tránh việc hardcode URL trực tiếp trong lời gọi đến postForObject(). Đây là một lớp cấu hình properties có dạng như sau:
@package tacos.email;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import lombok.Data;
@Data
@ConfigurationProperties(prefix = "tacocloud.api")
@Component
public class ApiProperties {
private String url;
}Và trong application.yml, URL cho API của Taco Cloud có thể được cấu hình như sau:
tacocloud:
api:
url: http://localhost:8080/orders/fromEmailĐể RestTemplate có thể được sử dụng và inject vào OrderSubmitMessageHandler, bạn cần thêm starter web của Spring Boot vào cấu hình build như sau:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>Mặc dù điều này giúp RestTemplate có mặt trong classpath, nó cũng sẽ kích hoạt cấu hình tự động (autoconfiguration) cho Spring MVC. Nhưng vì đây là một ứng dụng Spring Integration độc lập, nên bạn không cần Spring MVC hay cả Tomcat nhúng mà autoconfiguration sẽ cung cấp. Do đó, bạn nên vô hiệu hóa cấu hình tự động Spring MVC bằng cách thêm dòng sau vào application.yml:
spring:
main:
web-application-type: noneThuộc tính spring.main.web-application-type có thể được đặt là servlet, reactive, hoặc none. Khi Spring MVC có trong classpath, autoconfiguration sẽ tự động đặt giá trị này là servlet. Nhưng ở đây, bạn ghi đè nó thành none để Spring MVC và Tomcat sẽ không được tự động cấu hình. (Chúng ta sẽ nói kỹ hơn về ứng dụng web reactive trong chương 12.)
