春季集成与邮件支持完整堆栈应用程序。
#react #java #spring #springintegration

在本教程中,您将学习使用Spring Integration从Gmail接收电子邮件,使用Spring MVC创建API端点,并使用React从后端API获取数据。在本教程中,我们将跟踪付款到银行帐户中,将信息存储在mongoDB数据库中,并在React应用中显示交易列表。

场景

假设小型企业主需要他或她的帐户部门来跟踪客户或客户购买商品或服务的付款,而无需依靠任何平台来做到这一点。出于隐私原因,企业主也无法授予帐户部门访问该企业电子邮件的访问。

我们将创建一个全栈React和Spring Boot应用程序。我们还将创建一个Spring Integration Workflow,该工作流程倾听新的交易通知电子邮件,并将交易数据保留在MongoDB数据库中。 React应用程序将消耗交易数据并向帐户部门的工作人员显示交易列表。

春季整合的简介。

春季集成是建立企业集成解决方案的简单模型。春季集成扩展了春季编程模型以支持Enterprise Integration Patterns。 Spring Integration提供了基于春季的应用程序内的轻型消息,并通过声明的适配器支持外部系统。

我们将使用Spring Boot和其他春季项目,例如Spring MVC,Spring Data JPA和Spring Integration。

环境设置

我将在https://start.spring.io上使用Spring Initializer创建Spring应用程序。您还可以使用Spring Tool Suite启动一个新项目。弹簧工具套件是一个春季开发环境,为Eclipse IDE和Visual Studio代码提供了扩展。

Image of Spring initializer homepage

通过单击弹簧初始化器网页上的“生成”按钮下载新的春季项目。接下来是解压缩文件夹并在您喜欢的文本编辑器IDE中打开项目。我目前将VSCODE与Spring Tool Suite一起使用。

这是我们的pom.xml依赖项列表的样子;

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-integration</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-http</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

我们需要添加其他尚无弹簧启动器项目的依赖项。我们还应该删除春季综合HTTP,因为我们不需要它。 Spring-Antegration-HTTP是一个端点适配器,使Spring Integration能够使用HTTP协议与外部系统集成。我们将用春季整合邮件依赖替换春季整合。

<dependencies>
  <dependency>
    <groupId>org.eclipse.angus</groupId>
    <artifactId>angus-mail</artifactId>
  </dependency>
  <dependency>
    <groupId>com.sun.mail</groupId>
    <artifactId>jakarta.mail</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-mail</artifactId>
    <version>6.1.0</version>
  </dependency>
  .... list of dependencies
</dependencies>

“ Angus-Mail”依赖关系是雅加达邮件的SMTP协议提供商,而“ Jakarta-bail”依赖项通过SMTP,POP3和IMAP协议发送和接收电子邮件。

创建集成流

让我们创建一个基本的集成流,该流与Gmail连接并将消息记录到标准输出中,说我们已经收到了电子邮件。
在Spring Integration中创建集成流的方法有不同的方法

  • 通过XML配置
  • 通过Java配置
  • 通过使用DSL(域特定语言)的Java配置)

我将使用DSL选项使用Java配置,因为它是简短且易于阅读的。

我将在com中创建一个新文件,“ mailIntegrationConfig.java ”。 Akinwalehabib.transactionTracker软件包。该文件将包含我们的基本集成流程配置。

package com.akinwalehabib.transactiontracker;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.Pollers;
import org.springframework.integration.mail.dsl.Mail;

@Configuration
public class MailIntegrationConfig {

  @Bean
  public IntegrationFlow mainIntegration(
    EmailProperties props
  ) {
    return IntegrationFlow
      .from(
        Mail.imapInboundAdapter(props.getImapUrl())
          .shouldDeleteMessages(false)
          .simpleContent(true)
          .autoCloseFolder(false),
        e -> e.poller(
          Pollers.fixedDelay(props.getPollRate())
        )
      )
      .handle(message -> {
        System.out.println("New message received: " + message);
      })
      .get();
  }
}

我们创建了一个类并注释了@configuration,这表明Spring是一个配置类,它将为Spring应用程序上下文提供BEAN。

在MailIntegrationConfig类的MailIntegration方法中,我们注入了EmailProperties类,我们需要下一步创建。我们将使用emailProperties类从application.yml文件注入配置属性。

package com.akinwalehabib.transactiontracker;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import lombok.Data;

@Data
@ConfigurationProperties(prefix = "email")
@Component
public class EmailProperties {

  private String username;
  private String password;
  private String host;
  private String port;
  private String mailbox;
  private long pollRate = 30000;

  public String getImapUrl() {
    return String.format("imaps://%s:%s@%s:%s/%s", this.username, this.password, this.host, this.port, this.mailbox);
  }
}

我们用@data注释了电子邮件properties。 @data来自Lombok依赖性,该依赖性生成了我们的设定器,Getter,ToString和Hashcode,从而为我们节省了一些钥匙。 @component指示这是一个组件,使其成为组件扫描或自动检测的候选者。 @configurationProperties注释注入春季环境的配置属性。

属性用户名,密码,主机,端口,邮箱和轮询是我们将从春季环境中注入的详细信息,我们将添加到application.yml文件中。

有不同的方法为Spring提供配置属性,但是我们将在“ SRC/MAIN/RESOSDER”文件夹中使用application.yml文件。继续将“ application.properties”文件重命名为“ application.yml”。

email:
  username: akinwalehabib
  password: somerandomstring
  host: imap.gmail.com
  port: 993
  mailbox: INBOX
  pollRate: 30000

在没有@domainname的情况下,将“用户名”替换为“用户名”,然后添加您的应用程序密码。要使用Gmail,您需要创建一个应用程序密码并代替实际密码。请参阅此处的说明:https://support.google.com/mail/answer/185833?hl=en

继续运行您的应用程序,然后将电子邮件发送到配置的电子邮件地址。弹簧应用程序将在电子邮件到达时将消息记录到终端。

💡 New message received: GenericMessage [payload=org.springframework.integration.mail.AbstractMailReceiver$IntegrationMimeMessage@249c8b65, headers={closeableResource=org.springframework.integration.mail.AbstractMailReceiver$$Lambda$916 /0x0000000801155BE0@3B6D02DE,ID = 64B0E7D7-70F6-9432-260D-A1FC0941874B,TIMESTAMP = 1688931330198}]

因为我们只想跟踪付款,所以我们只关心银行的交易通知电子邮件。在春季集成中,我们可以将过滤器放在集成流中,以允许或禁止从流程中进行下一步的消息。
我们将添加一个检查电子邮件主题的过滤器。如果它包含关键字“事务警报”,我们将将消息传递给集成流的下一步。我们还可以在我们的过滤方式中添加其他支票,例如检查发件人是否是我们的银行的通知电子邮件地址等。

....
import jakarta.mail.Message;
import jakarta.mail.MessagingException;

@Configuration
public class MailIntegrationConfig {

  private final String SUBJECT_KEYWORDS = "TRANSACTION ALERT";

  @Bean
  public IntegrationFlow mainIntegration(
    EmailProperties props
  ) {
    return IntegrationFlow
      .from(
        Mail.imapInboundAdapter(props.getImapUrl())
          .shouldDeleteMessages(false)
          .simpleContent(true)
          .autoCloseFolder(false),
        e -> e.poller(
          Pollers.fixedDelay(props.getPollRate())
        )
      )
      .<Message>filter((Message) -> {
        boolean containsKeyword = false;
        try {
          containsKeyword = Message.getSubject().toUpperCase().contains(SUBJECT_KEYWORDS);
        } catch (MessagingException e1) {
          e1.printStackTrace();
        }

        return containsKeyword;
      })
      .handle(message -> {
        System.out.println("New message received: " + message);
      })
      .get();
  }
}

过滤器方法使用返回布尔值的匿名函数。如果来自匿名函数的返回值为真,则仅将消息传递到集成流中的下一步。

如果您发送了一个主题不包含关键字“事务警报”的电子邮件,则应在终端中查看以下消息。

💡 The message [GenericMessage [payload=org.springframework.integration.mail.AbstractMailReceiver$IntegrationMimeMessage@20625346, headers={closeableResource=org.springframework.integration.mail.AbstractMailReceiver$$Lambda$953/ 0x000000080118bc10@9bc0049, id=bdb31273-a3fd-3d6e-ef2e-4a9f98942254, timestamp=1688935506023}]] has been rejected in filter: bean 'mainIntegration.filter#0' for component 'mainIntegration.org.springframework.integration.config.ConsumerEndpointFactoryBean #0';定义在:“类路径资源[com/akinwalehabib/transactionTracker/mailIntegrationConfig.class]';来自来源:'Bean方法Mainintegrationâ

下一步是将收到的消息转换为对象。我们将创建一个域模型,然后使用Spring Data MongoDB在数据库中坚持下去。

让我们将Spring Data MongoDB依赖性添加到我们的项目中,然后添加MongoDB配置。

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

然后更新我们的application.yml文件。

....
spring:
  data:
    mongodb:
      host: localhost
      port: 27017
      database: emailintegration

创建我们的域模型。


package com.akinwalehabib.transactiontracker;

import java.time.LocalDateTime;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@NoArgsConstructor
@ToString
@Document(collection = "emails")
public class Email {

  @Id
  private String id;
  private String email;
  private String subject;
  private Double amount;
  private String remarks;
  private LocalDateTime receiptDate;
  private String content;

  public Email (String email, String subject, String content) {
    this.email = email;
    this.subject = subject;
    this.content = content.toUpperCase();

    this.amount = retrieveAmount(content);
    this.receiptDate = retrieveReceiptDate(content);
    this.remarks = retrieveRemarks(content);
  }

  public double retrieveAmount(String content) {
    Pattern pattern = Pattern.compile("Amount : NGN [1-9]\\d?(?:,\\d{3})*(?:\\.\\d{2})");
    Matcher matcher = pattern.matcher(content);
    String amountString = "";

    if (matcher.find()) {
      String substring = matcher.group();
      String[] substringParts = substring
        .split(":");                                        
      String[] substringAmountPart = substringParts[1]
        .trim()
        .split(" ");                                        
      amountString = substringAmountPart[1].replace(",", "");
      double value = Double.parseDouble(amountString);
      return value;
    }

    return 0.0;
  }

  public String retrieveRemarks(String content) {
    Pattern pattern = Pattern.compile("Remarks :([\\s]+\\w+\\s+[\\w]+)+");
    Matcher matcher = pattern.matcher(content);
    String remarks = "";

    if (matcher.find()) {
      String substring = matcher.group();
      remarks = substring.split(":")[1]
        .trim();
    }

    return remarks;
  }

  public LocalDateTime retrieveReceiptDate(String content) {
    Pattern pattern = Pattern.compile("Time of Transaction : (\\d+-\\d+-\\d+ \\d+\\d+:\\d+)");
    Matcher matcher = pattern.matcher(content);
    LocalDateTime receiptDate = LocalDateTime.now();

    if (matcher.find()) {
      String substring = matcher.group();                                 
      String[] DateTimeParts = substring.split(":");                
      String[] DateParts = DateTimeParts[1].trim().split("-"); 

      int day = Integer.parseInt(DateParts[0]);
      int month = Integer.parseInt(DateParts[1]);
      int year = Integer.parseInt(DateParts[2].substring(0, DateParts[2].length() - 3));

      int hour = Integer.parseInt(DateParts[2]
        .substring(DateParts[2].length() - 2)
        .trim());
      int minute = Integer.parseInt(DateTimeParts[2]);
      receiptDate = LocalDateTime.of(year, month, day, hour, minute);
    }

    return receiptDate;
  }
}

我们用@data,@noargsconstructor和Lombok依赖项注释了电子邮件域模型和@ToString。我们还添加了@document(Collection =“电子邮件”)“ Spring-Data-Mongodb”的注释,该注释指定了我们将存储文档的数据库集合。

在电子邮件域模型中,我们想存储主题,收到的金额,交易备注和收据日期。我们将从电子邮件内容中检索金额,交易备注和收据日期,这就是为什么我们拥有 retereveamount retrieveremarks retrievereceiptdate >方法。

我们还需要创建一个扩展MongorePository的界面,以便我们的应用程序可以将电子邮件域保存到数据库中,还可以从数据库中检索信息。

package com.akinwalehabib.transactiontracker;

import org.springframework.data.mongodb.repository.MongoRepository;

public interface EmailRepository extends MongoRepository<Email, String>{}

我们应该创建一个类,以将传入的交易通知电子邮件转换为电子邮件域对象。然后,我们将在我们的集成流中创建一个变压器,该流程使用新创建的类。

package com.akinwalehabib.transactiontracker;

import java.io.IOException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.integration.mail.transformer.AbstractMailMessageTransformer;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.stereotype.Component;

import com.akinwalehabib.transactiontracker.Email;

import jakarta.mail.BodyPart;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.ContentType;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMultipart;

@Component
public class EmailTransformer extends AbstractMailMessageTransformer<Email> {

  private static Logger log = LoggerFactory.getLogger(EmailTransformer.class);
  // private boolean textIsHtml = false;

  @Override
  protected AbstractIntegrationMessageBuilder<Email> doTransform(Message mailMessage) {
    Email email = processPayload(mailMessage);
    return MessageBuilder.withPayload(email);
  }

  private Email processPayload(Message mailMessage) {
    try {
      String subject = mailMessage.getSubject();
      String email = ((InternetAddress) mailMessage.getFrom()[0]).getAddress();
      String content = getTextFromMessage(mailMessage);

      return parseEmail(email, subject, content);
    } catch (MessagingException e) {
      log.error("MessagingException: {}", e);
    } catch (Exception e) {
      log.error("IOException: {}", e);
    }

    return null;
  }

  private String getTextFromMessage(Message message) throws IOException, MessagingException {
    String result = "";
    if (message.isMimeType("text/plain")) {
      result = message.getContent().toString();
    } else if (message.isMimeType("multipart/*")) {
      MimeMultipart mimeMultipart = (MimeMultipart) message.getContent();
      result = getTextFromMimeMultipart(mimeMultipart);
    }
    return result;
  }

  private String getTextFromMimeMultipart(MimeMultipart mimeMultipart) throws IOException, MessagingException {
    int count = mimeMultipart.getCount();
    if (count == 0) throw new MessagingException("Multipart with no body parts not supported.");

    boolean multipartAlt = new ContentType(mimeMultipart.getContentType()).match("multipart/alternative");
    if (multipartAlt) {
      return getTextFromBodyPart(mimeMultipart.getBodyPart(count - 1));
    }

    String result = "";
    for (int i = 0; i < count; i++) {
        BodyPart bodyPart = mimeMultipart.getBodyPart(i);
        result += getTextFromBodyPart(bodyPart);
    }
    return result;
  }

  private String getTextFromBodyPart(BodyPart bodyPart) throws IOException, MessagingException {
    String result = "";
    if (bodyPart.isMimeType("text/plain")) {
      result = (String) bodyPart.getContent();
    } else if (bodyPart.isMimeType("text/html")) {
      String html = (String) bodyPart.getContent();
      result = org.jsoup.Jsoup.parse(html).text();
    } else if (bodyPart.getContent() instanceof MimeMultipart){
      result = getTextFromMimeMultipart((MimeMultipart)bodyPart.getContent());
    }

    return result;
  }

  private Email parseEmail(String senderEmailAddress, String subject, String content) {
    Email email = new Email(senderEmailAddress, subject, content);
    return email;
  }
}

电子邮件transformer很长。它扩展了AbstractMailMailMessAgetransFormer ,然后覆盖 AbstractIntegrationMessageBuilder 。它从电子邮件中检索主题和内容,然后从电子邮件域模型类中创建对象。电子邮件域类负责检索传递给它的电子邮件内容的金额,备注和收据日期。

我们需要向我们的应用程序添加另一个依赖关系,我们用来解析HTML并在电子邮件Transformer类中提取文本内容。

<dependency>
        <groupId>org.jsoup</groupId>
        <artifactId>jsoup</artifactId>
        <version>1.16.1</version>
</dependency>

然后将变压器添加到我们的集成流中。请注意,我们如何将EmailTransFormer类注入该方法,然后在我们的集成流中使用。我们还注入了电子邮件repository,并在句柄回调功能中使用了它来持续数据库中的交易详细信息。

@Bean
  public IntegrationFlow mainIntegration(
    EmailProperties props,
    EmailTransformer emailTransformer,
    EmailRepository emailRepository
  ) {
    return IntegrationFlow
      .from(
        Mail.imapInboundAdapter(props.getImapUrl())
          .shouldDeleteMessages(false)
          .simpleContent(true)
          .autoCloseFolder(false),
        e -> e.poller(
          Pollers.fixedDelay(props.getPollRate())
        )
      )
      .<Message>filter((Message) -> {
        boolean containsKeyword = false;
        try {
          containsKeyword = Message.getSubject().toUpperCase().contains(SUBJECT_KEYWORDS);
        } catch (MessagingException e1) {
          e1.printStackTrace();
        }

        return containsKeyword;
      })
      .transform(emailTransformer)
      .handle(message -> {
        Email email = (Email) message.getPayload();
        emailRepository.save(email);
      })
      .get();
  }

这是我银行的示例交易通知电子邮件。该电子邮件由不同的MimeBodyParts组成,主要内容是html。

继续运行您的应用程序,然后将电子邮件发送到您配置的电子邮件地址。确保电子邮件主题包含“事务警报”。另外,请确保电子邮件的内容具有“备注”,“金额”和“交易时间”,如上所示。

Screenshot of transaction notification email

运行应用程序后,检查数据库,您应该在数据库中存储的事务详细信息。请参阅下面的屏幕截图。

创建API端点

我们将创建一个API端点,以揭示存储在MongoDB数据库中的交易列表。我们已经通过Spring Initializer添加了“ Spring-Boot-Starter-Web”依赖项。
创建一个名为“ emailIntegrationapi.java”的新文件。这个新文件将包含我们的API端点。

package com.akinwalehabib.transactiontracker;

import java.util.List;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(path = "/api/payments",
                produces = "application/json")
public class EmailIntegrationAPI {

  private EmailRepository emailRepository;

  public EmailIntegrationAPI(EmailRepository emailRepository) {
    this.emailRepository = emailRepository;
  }

  @GetMapping
  public List<Email> getPaymenets() {
    List<Email> payments = emailRepository.findAll();
    return payments;
  }
}

运行应用程序并使用您喜欢的浏览器访问http://localhost:8080/api/payments。您应该有一个包含收到的付款通知电子邮件的JSON响应。

Screenshot of JSON response by API endpoint

创建React客户端

让我们创建一个Web客户端以与新创建的API进行交互。我们将使用“ Create-React-App”快速在 src/frontend 文件夹中创建一个React应用程序。确保您的春季申请在继续之前运行。

npx create-react-app src/frontend && cd src/frontend

package.json中添加一个“代理”键,它有助于避免CORS问题,并将未知请求重定向到开发过程中配置的主机或端口。

"proxy": "http://localhost:8080"

让我们安装MUI组件库。 MUI提供了一个数据杂交组件,非常适合我们的用例。

npm install @mui/material @mui/styled-engine-sc styled-components @mui/x-data-gridnpm install @fontsource/roboto @mui/x-data-grid                                                                                                                  ─╯
npm install @fontsource/roboto @mui/x-data-grid

我将创建一个名为“付款”的新组件。付款组件将在SRC/文件夹/SRC文件夹中。该组件将从我们的API获取所有付款数据,然后在DOM中填充数据杂志。


import React, { useEffect, useState } from 'react'
import {
  Box,
  Skeleton
} from '@mui/material';
import { DataGrid } from '@mui/x-data-grid';

const columns = [
  { field: 'id', headerName: 'ID', width: 70 },
  { field: 'amount', headerName: 'Amount', width: 130, type: 'number' },
  { field: 'receiptDate', headerName: 'Receipt Date', width: 130 },
  { field: 'remarks', headerName: 'Remarks', width: 700 }
];

function PaymentsSkeleton() {
  return (
    <>
      <Skeleton variant="rectangular" animation="wave" width={"100%"} height={"80vh"} />
    </>
  )
}

function Payments() {
  const [payments, setPayments] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function getTransactions() {
      let response = await fetch("/api/payments");
      return await response.json();
    }

    getTransactions()
      .then(payments => {
        setPayments(payments);
        setLoading(false);
      })
      .catch(err => console.error('Err: ' + err));
  }, [])

  if (loading) {
    return <PaymentsSkeleton />
  }

  return (
    <Box>
      <DataGrid
        columns={columns}
        rows={payments}
        initialState={{
          pagination: {
            paginationModel: { page: 0, pageSize: 20 },
          },
        }}
        pageSizeOptions={[ 5, 10, 15, 20, 50, 100 ]}
        checkboxSelection
      />
    </Box>
  );
}

export default Payments

在src/frontend/src文件夹中编辑app.js文件以显示付款组件。

import * as React from 'react'
import './App.css';
import Payments from './Payments';
import { Container, CssBaseline, Typography } from '@mui/material';

function App() {
  return (
    <>
      <CssBaseline />
      <Container>
        <Typography variant="h2" gutterBottom>
          Spring Integration - Mail
        </Typography>

        <Typography variant="subtitle1" gutterBottom>
          Payments received
        </Typography>

        <hr />

        <Payments />
      </Container>
    </>
  ) 
  ;
}

export default App;

使用浏览器访问http://localhost:3000,您的网页应该看起来像这样。

Screenshot of React app homepage showing DataGrid populated with transactions fetched from backend API

结论

我们已经学会了如何使用弹簧集成邮件端点创建春季集成流。我们使用春季数据mongoDB持续了我们的域模型。我们还创建了一个具有GET端点的API。总之,我们开发了一个基于React和MUI的Web客户端以与我们的后端API进行交互。

春季集成非常适合使用企业集成模式开发企业集成解决方案。在https://docs.spring.io/spring-integration/docs/current/reference/html/index.html上了解更多信息。

我相信,在开放银行API中,申请应能够与银行服务集成并聆听诸如借方的事件。

请随时分享您的想法,特别是您使用春季整合面临的挑战。