51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

在 Spring Boot 中使用 SendGrid 发送电子邮件

1、概览 {#1概览}

无论是用户注册、密码重置还是促销活动,发送电子邮件都是现代 Web 应用的一项重要功能。

本文将带你了解如何在 Spring Boot 应用中使用 SendGrid 发送电子邮件。

2、SendGrid 设置 {#2sendgrid-设置}

在开始之前,我们首先需要一个 SendGrid 账户。SendGrid 提供了免费套餐,允许我们每天发送多达 100 封电子邮件,这对于演示来说已经足够了。

注册完成后,需要创建一个 API Key 来对我们发送到 SendGrid 服务的请求进行 身份认证

3、项目设置 {#3项目设置}

在开始使用 SendGrid 发送电子邮件之前,需要添加 SDK 依赖并配置应用。

3.1、依赖 {#31依赖}

首先,在项目的 pom.xml 文件中添加 SendGrid SDK 依赖:

<dependency>
    <groupId>com.sendgrid</groupId>
    <artifactId>sendgrid-java</artifactId>
    <version>4.10.2</version>
</dependency>

该依赖为我们提供了与 SendGrid 服务交互和从应用发送电子邮件所需的类。

3.2、定义 SendGrid 配置属性 {#32定义-sendgrid-配置属性}

现在,为了与 SendGrid 服务交互并向用户发送电子邮件,我们需要配置 API Key 以验证 API 请求。我们还需要配置发件人姓名和电子邮件地址,它们应与我们在 SendGrid 账户中设置的发件人身份相匹配。

我们在项目的 application.yaml 文件中配置这些属性,并使用 @ConfigurationProperties 将这些值映射到 POJO ,Service 层在与 SendGrid 交互时会引用配置的 POJO

@Validated
@ConfigurationProperties(prefix = "com.baeldung.sendgrid")
class SendGridConfigurationProperties {
    @NotBlank
    @Pattern(regexp = "^SG[0-9a-zA-Z._]{67}$")
    private String apiKey;

    @Email
    @NotBlank
    private String fromEmail;

    @NotBlank
    private String fromName;

    // 标准的 Getter / Setter
}

如上。还添加了 @Validated 验证注解,以确保正确配置所有必要属性。如果定义的任何验证失败,Spring ApplicationContext 将无法启动("快速失败" 原则)。

下面是 application.yaml 文件的配置片段,其中定义了将要自动映射到 SendGridConfigurationProperties 类的所需属性:

com:
  baeldung:
    sendgrid:
      api-key: ${SENDGRID_API_KEY}
      from-email: ${SENDGRID_FROM_EMAIL}
      from-name: ${SENDGRID_FROM_NAME}

我们使用 ${} 属性占位符从 环境变量 中加载属性值。这种设置允许我们将 SendGrid 属性外部化,并在应用中轻松访问它们。

3.3、配置 SendGrid Bean {#33配置-sendgrid-bean}

配置了属性后,引用它们来定义必要的 Bean:

@Configuration
@EnableConfigurationProperties(SendGridConfigurationProperties.class)
class SendGridConfiguration {
    private final SendGridConfigurationProperties sendGridConfigurationProperties;

    // 构造函数注入
    public SendGridConfiguration (SendGridConfigurationProperties sendGridConfigurationProperties){
        this.sendGridConfigurationProperties = sendGridConfigurationProperties;
    }

    @Bean
    public SendGrid sendGrid() {
        String apiKey = sendGridConfigurationProperties.getApiKey();
        return new SendGrid(apiKey);
    }
}

通过构造函数注入,我们注入了之前创建的 SendGridConfigurationProperties 类的实例。然后,我们使用配置的 API Key 创建 SendGrid Bean。

接下来,创建一个 Bean 来代表所有我们发送的邮件的发件人:

@Bean
public Email fromEmail() {
    String fromEmail = sendGridConfigurationProperties.getFromEmail();
    String fromName = sendGridConfigurationProperties.getFromName();
    return new Email(fromEmail, fromName);
}

有了这些 Bean,我们就可以在 Service 层中自动装配它们,以便与 SendGrid 服务交互。

4、发送简单的电子邮件 {#4发送简单的电子邮件}

Bean 定义好后,让我们创建一个 EmailDispatcher 类并引用它们来发送一封简单的电子邮件:

private static final String EMAIL_ENDPOINT = "mail/send";

public void dispatchEmail(String emailId, String subject, String body) {
    Email toEmail = new Email(emailId);
    Content content = new Content("text/plain", body);
    Mail mail = new Mail(fromEmail, subject, toEmail, content);

    Request request = new Request();
    request.setMethod(Method.POST);
    request.setEndpoint(EMAIL_ENDPOINT);
    request.setBody(mail.build());

    sendGrid.api(request);
}

dispatchEmail() 方法中,我们创建了一个新的 Mail 对象,代表我们要发送的电子邮件,然后将其设置为 Request 对象的请求体(Request Body)。

最后,使用 SendGrid Bean 将请求发送到 SendGrid 服务。

5、发送带附件的电子邮件 {#5发送带附件的电子邮件}

除了发送简单的纯文本电子邮件,SendGrid 还允许我们发送带附件的电子邮件。

首先,创建一个 helper 方法,用于将 MultipartFile 转换为 SendGrid SDK 中的 Attachments 对象:

private Attachments createAttachment(MultipartFile file) {
    byte[] encodedFileContent = Base64.getEncoder().encode(file.getBytes());
    Attachments attachment = new Attachments();
    attachment.setDisposition("attachment");
    attachment.setType(file.getContentType());
    attachment.setFilename(file.getOriginalFilename());
    attachment.setContent(new String(encodedFileContent, StandardCharsets.UTF_8));
    return attachment;
}

createAttachment() 方法中,我们创建一个新的 Attachments 对象,并根据 MultipartFile 参数设置其属性。

注意,在将文件内容设置到 Attachments 对象之前,我们对其进行了 Base64 编码。

接下来,更新 dispatchEmail() 方法,使其接受一个可选的 MultipartFile 对象集合:

public void dispatchEmail(String emailId, String subject, String body, List<MultipartFile> files) {
    // ... 同上

    if (files != null && !files.isEmpty()) {
        for (MultipartFile file : files) {
            Attachments attachment = createAttachment(file);
            mail.addAttachments(attachment);
        }
    }

    // ...  同上
}

遍历 files 参数中的每个文件,使用 createAttachment() 方法创建相应的 Attachments 对象,并将其添加到 Mail 对象中。该方法的其余部分保持不变。

6、用动态模板发送电子邮件 {#6用动态模板发送电子邮件}

SendGrid 还允许我们使用 HTMLHandlebars 语法 创建动态电子邮件模板。

以向用户发送个性化 "喝水提醒" 电子邮件为例。

6.1、创建 HTML 模板 {#61创建-html-模板}

首先,要为 "喝水提醒" 电子邮件创建 HTML 模板:

<html>
    <head>
        <style>
            body { font-family: Arial; line-height: 2; text-align: Center; }
            h2 { color: DeepSkyBlue; }
            .alert { background: Red; color: White; padding: 1rem; font-size: 1.5rem; font-weight: bold; }
            .message { border: .3rem solid DeepSkyBlue; padding: 1rem; margin-top: 1rem; }
            .status { background: LightCyan; padding: 1rem; margin-top: 1rem; }
        </style>
    </head>
    <body>
        <div class="alert">⚠️ URGENT HYDRATION ALERT ⚠️</div>
        <div class="message">
            <h2>It's time to drink water!</h2>
            <p>Hey {{name}}, this is your friendly reminder to stay hydrated. Your body will thank you!</p>
            <div class="status">
                <p><strong>Last drink:</strong> {{lastDrinkTime}}</p>
                <p><strong>Hydration status:</strong> {{hydrationStatus}}</p>
            </div>
        </div>
    </body>
</html>

在模板中,我们使用 Handlebars 语法定义了 {{name}}, {{lastDrinkTime}}{{hydrationStatus}} 的占位符。发送电子邮件时,将用实际值替换这些占位符。

我们还使用了内联 CSS 来美化电子邮件模板。

6.2、配置模板 ID {#62配置模板-id}

SendGrid 中创建了模板后,需要为它分配一个唯一的模板 ID。

要保存这个模板 ID,我们可以在 SendGridConfigurationProperties 类中定义一个嵌套类:

@Valid
private HydrationAlertNotification hydrationAlertNotification = new HydrationAlertNotification();

class HydrationAlertNotification {
    @NotBlank
    @Pattern(regexp = "^d-[a-f0-9]{32}$")
    private String templateId;

    // Getter / Setter 方法省略
}

再次添加 @Valid 校验注解,以确保正确配置模板 ID 并使其符合预期格式。

同样,在 application.yaml 文件中添加相应的模板 ID 属性:

com:
  baeldung:
    sendgrid:
      hydration-alert-notification:
        template-id: ${HYDRATION_ALERT_TEMPLATE_ID}

发送 "喝水提醒" 电子邮件时,我们将在 EmailDispatcher 类中使用此配置的模板 ID。

6.3、发送模板电子邮件 {#63发送模板电子邮件}

配置了模板 ID 后,让我们创建一个自定义 Personalization 类来保存我们的占位符 KEY 名及其相应的值:

class DynamicTemplatePersonalization extends Personalization {
    private final Map<String, Object> dynamicTemplateData = new HashMap<>();

    public void add(String key, String value) {
        dynamicTemplateData.put(key, value);
    }

    @Override
    public Map<String, Object> getDynamicTemplateData() {
        return dynamicTemplateData;
    }
}

覆写 getDynamicTemplateData() 方法来返回 dynamicTemplateData Map,并使用 add() 方法对其进行填充。

现在,创建一个新的 Service 方法来发送 "喝水提醒":

public void dispatchHydrationAlert(String emailId, String username) {
    Email toEmail = new Email(emailId);
    String templateId = sendGridConfigurationProperties.getHydrationAlertNotification().getTemplateId();

    DynamicTemplatePersonalization personalization = new DynamicTemplatePersonalization();
    personalization.add("name", username);
    personalization.add("lastDrinkTime", "Way too long ago");
    personalization.add("hydrationStatus", "Thirsty as a camel");
    personalization.addTo(toEmail);

    Mail mail = new Mail();
    mail.setFrom(fromEmail);
    mail.setTemplateId(templateId);
    mail.addPersonalization(personalization);

    // ... 发送请求流程与之前相同   
}

dispatchHydrationAlert() 方法中,我们创建了 DynamicTemplatePersonalization 类的实例,并为 HTML 模板中定义的占位符添加了自定义值。

然后,在向 SendGrid 发送请求之前,我们会在 Mail 对象上设置该 personalization 对象和 templateId

SendGrid 将使用提供的动态数据替换我们 HTML 模板中的占位符。这有助于我们向用户发送个性化的邮件,同时保持一致的设计和布局。

7、测试 SendGrid {#7测试-sendgrid}

现在,我们已经使用 SendGrid 实现了发送电子邮件的功能,接下来看看如何测试这种集成。

测试外部服务有一点麻烦,因为我们不想在测试过程中实际调用 SendGrid 的 API。

我们可以使用 MockServer,它可以让我们模拟 SendGrid 的外部调用。

7.1、配置测试环境 {#71配置测试环境}

在编写测试之前,先在 src/test/resources 目录中创建一个包含以下内容的 application-integration-test.yaml 文件:

com:
  baeldung:
    sendgrid:
      api-key: SG0101010101010101010101010101010101010101010101010101010101010101010
      from-email: no-reply@baeldung.com
      from-name: Baeldung
      hydration-alert-notification:
        template-id: d-01010101010101010101010101010101

这些虚拟值绕过了我们之前在 SendGridConfigurationProperties 类中配置的验证。

现在,创建测试类:

@SpringBootTest
@ActiveProfiles("integration-test")
@MockServerTest("server.url=http://localhost:${mockServerPort}")
@EnableConfigurationProperties(SendGridConfigurationProperties.class)
class EmailDispatcherIntegrationTest {
    private MockServerClient mockServerClient;

    @Autowired
    private EmailDispatcher emailDispatcher;
    
    @Autowired
    private SendGridConfigurationProperties sendGridConfigurationProperties;
    
    private static final String SENDGRID_EMAIL_API_PATH = "/v3/mail/send";
}

我们使用 @ActiveProfiles 注解加载特定于集成测试的属性。

我们还使用 @MockServerTest 注解启动了一个 MockServer 实例,并创建了一个带有 ${mockServerPort} 占位符的 server.url 测试属性。这将被 MockServer 选择的空闲端口替换,我们会在下一节中引用这个端口,在那里我们要配置我们的自定义 SendGrid REST 客户端。

7.2、配置自定义 SendGrid REST 客户端 {#72配置自定义-sendgrid-rest-客户端}

为了将 SendGrid API 请求路由到 MockServer ,我们需要为 SendGrid SDK 配置一个自定义 REST 客户端。

创建一个 @TestConfiguration 类,该类定义了一个带有自定义 HttpClient 的新 SendGrid Bean:

@TestConfiguration
@EnableConfigurationProperties(SendGridConfigurationProperties.class)
class TestSendGridConfiguration {
    @Value("${server.url}")
    private URI serverUrl;

    @Autowired
    private SendGridConfigurationProperties sendGridConfigurationProperties;

    @Bean
    @Primary
    public SendGrid testSendGrid() {
        SSLContext sslContext = SSLContextBuilder.create()
          .loadTrustMaterial((chain, authType) -> true)
          .build();

        HttpClientBuilder clientBuilder = HttpClientBuilder.create()
          .setSSLContext(sslContext)
          .setProxy(new HttpHost(serverUrl.getHost(), serverUrl.getPort()));

        Client client = new Client(clientBuilder.build(), true);
        client.buildUri(serverUrl.toString(), null, null);

        String apiKey = sendGridConfigurationProperties.getApiKey();
        return new SendGrid(apiKey, client);
    }
}

TestSendGridConfiguration 类中,我们创建了一个自定义客户端,通过 server.url 属性指定的代理服务器路由所有请求。我们还配置了 SSL Context 以信任所有证书,因为 MockServer 默认使用自签名证书。

要在集成测试中使用此测试配置,需要在测试类中添加 @ContextConfiguration 注解:

@ContextConfiguration(classes = TestSendGridConfiguration.class)

这将确保我们的应用在运行集成测试时使用的是我们在 TestSendGridConfiguration 类中定义的 Bean,而不是在 SendGridConfiguration 类中定义的 Bean。

7.3、验证 SendGrid 请求 {#73验证-sendgrid-请求}

最后,编写一个测试用例来验证我们的 dispatchEmail() 方法是否向 SendGrid 发送了预期的请求:

// 设置测试数据
String toEmail = RandomString.make() + "@baeldung.it";
String emailSubject = RandomString.make();
String emailBody = RandomString.make();
String fromName = sendGridConfigurationProperties.getFromName();
String fromEmail = sendGridConfigurationProperties.getFromEmail();
String apiKey = sendGridConfigurationProperties.getApiKey();

// 创建 JSON 请求体
String jsonBody = String.format("""
    {
        "from": {
            "name": "%s",
            "email": "%s"
        },
        "subject": "%s",
        "personalizations": [{
            "to": [{
                "email": "%s"
            }]
        }],
        "content": [{
            "value": "%s"
        }]
    }
    """, fromName, fromEmail, emailSubject, toEmail, emailBody);

// 配置模拟服务器预期值
mockServerClient
  .when(request()
    .withMethod("POST")
    .withPath(SENDGRID_EMAIL_API_PATH)
    .withHeader("Authorization", "Bearer " + apiKey)
    .withBody(new JsonBody(jsonBody, MatchType.ONLY_MATCHING_FIELDS)
  ))
  .respond(response().withStatusCode(202));

// 调用被测方法
emailDispatcher.dispatchEmail(toEmail, emailSubject, emailBody);

// 验证请求是否符合预期
mockServerClient
  .verify(request()
    .withMethod("POST")
    .withPath(SENDGRID_EMAIL_API_PATH)
    .withHeader("Authorization", "Bearer " + apiKey)
    .withBody(new JsonBody(jsonBody, MatchType.ONLY_MATCHING_FIELDS)
  ), VerificationTimes.once());

在我们的测试方法中,首先设置了测试数据,并为 SendGrid 请求创建了预期的 JSON 请求体。然后,对 MockServer 进行配置,使其能够接收到发送到 SendGrid API 路径的 POST 请求,并包含 Authorization Header 和 JSON 请求体。我们还指示 MockServer 在发出请求时响应 202 状态代码。

接下来,使用测试数据调用 dispatchEmail() 方法,并验证是否向 MockServer 发送了符合预期的请求。

通过使用 MockServer 来模拟 SendGrid API,可以确保我们的集成能够按照预期运行,而无需实际发送任何电子邮件或产生任何费用。

8、总结 {#8总结}

本文介绍了如何在 Spring Boot 中使用 SendGrid 发送电子邮件,首先介绍了如何整合、配置 SendGrid,然后介绍了发送简单电子邮件、带附件电子邮件和动态 HTML 模板电子邮件等功能,最后,使用 MockServer 编写集成测试来验证应用是否向 SendGrid 发送了正确的请求。


Ref:https://www.baeldung.com/java-email-sendgrid

赞(1)
未经允许不得转载:工具盒子 » 在 Spring Boot 中使用 SendGrid 发送电子邮件