在 Spring Boot 中使用 SendGrid 发送电子邮件
1、概览
无论是用户注册、密码重置还是促销活动,发送电子邮件都是现代 Web 应用的一项重要功能。
本文将带你了解如何在 Spring Boot 应用中使用 SendGrid 发送电子邮件。
2、SendGrid 设置
在开始之前,我们首先需要一个 SendGrid 账户。SendGrid 提供了免费套餐,允许我们每天发送多达 100 封电子邮件,这对于演示来说已经足够了。
注册完成后,需要创建一个 API Key 来对我们发送到 SendGrid 服务的请求进行 身份认证。
3、项目设置
在开始使用 SendGrid 发送电子邮件之前,需要添加 SDK 依赖并配置应用。
3.1、依赖
首先,在项目的 pom.xml
文件中添加 SendGrid SDK 依赖:
<dependency>
<groupId>com.sendgrid</groupId>
<artifactId>sendgrid-java</artifactId>
<version>4.10.2</version>
</dependency>
该依赖为我们提供了与 SendGrid 服务交互和从应用发送电子邮件所需的类。
3.2、定义 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
配置了属性后,引用它们来定义必要的 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、发送简单的电子邮件
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、发送带附件的电子邮件
除了发送简单的纯文本电子邮件,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、用动态模板发送电子邮件
SendGrid 还允许我们使用 HTML 和 Handlebars 语法 创建动态电子邮件模板。
以向用户发送个性化 “喝水提醒” 电子邮件为例。
6.1、创建 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
在 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、发送模板电子邮件
配置了模板 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
现在,我们已经使用 SendGrid 实现了发送电子邮件的功能,接下来看看如何测试这种集成。
测试外部服务有一点麻烦,因为我们不想在测试过程中实际调用 SendGrid 的 API。
我们可以使用 MockServer,它可以让我们模拟 SendGrid 的外部调用。
7.1、配置测试环境
在编写测试之前,先在 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 客户端
为了将 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 请求
最后,编写一个测试用例来验证我们的 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、总结
本文介绍了如何在 Spring Boot 中使用 SendGrid 发送电子邮件,首先介绍了如何整合、配置 SendGrid,然后介绍了发送简单电子邮件、带附件电子邮件和动态 HTML 模板电子邮件等功能,最后,使用 MockServer 编写集成测试来验证应用是否向 SendGrid 发送了正确的请求。
Ref:https://www.baeldung.com/java-email-sendgrid