1. 概述
在现代Web应用中,发送邮件是核心功能之一,无论是用户注册、密码重置还是营销活动都离不开它。
本教程将探讨如何在Spring Boot应用中集成SendGrid发送邮件。我们将逐步完成必要配置,并针对不同场景实现邮件发送功能。
2. 配置SendGrid
要跟随本教程,首先需要注册SendGrid账户。SendGrid提供免费套餐,每天可发送100封邮件,足够我们演示使用。
注册完成后,需要创建API密钥来认证对SendGrid服务的请求。
最后,必须验证发件人身份才能成功发送邮件。
3. 项目搭建
在开始使用SendGrid发送邮件前,需要添加SDK依赖并正确配置应用。
3.1. 依赖项
首先将SendGrid SDK依赖添加到项目的pom.xml文件:
<dependency>
<groupId>com.sendgrid</groupId>
<artifactId>sendgrid-java</artifactId>
<version>4.10.2</version>
</dependency>
此依赖提供了与SendGrid服务交互所需的类,使应用能够发送邮件。
3.2. 定义SendGrid配置属性
要与SendGrid服务交互并发送邮件,需要配置API密钥进行认证。同时需要配置发件人名称和邮箱地址,这些必须与SendGrid账户中设置的发件人身份匹配。
我们将这些属性存储在项目的application.yaml文件中,并使用*@ConfigurationProperties*将值映射到POJO,服务层在调用SendGrid时会引用这个类:
@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方法
}
我们还添加了验证注解确保所有必需属性正确配置。如果任何验证失败,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;
// 标准构造函数
@Bean
public SendGrid sendGrid() {
String apiKey = sendGridConfigurationProperties.getApiKey();
return new SendGrid(apiKey);
}
}
使用构造函数注入,我们注入之前创建的SendGridConfigurationProperties实例。然后使用配置的API密钥创建SendGrid Bean。
接下来,创建一个Bean表示所有外发邮件的发件人:
@Bean
public Email fromEmail() {
String fromEmail = sendGridConfigurationProperties.getFromEmail();
String fromName = sendGridConfigurationProperties.getFromName();
return new Email(fromEmail, fromName);
}
有了这些Bean,就可以在服务层自动装配它们来与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对象的请求体。
最后,使用SendGrid Bean将request发送到SendGrid服务。
5. 发送带附件的邮件
除了发送简单纯文本邮件,SendGrid还支持发送带附件的邮件。
首先,创建一个辅助方法将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">⚠️ 紧急补水提醒 ⚠️</div>
<div class="message">
<h2>该喝水了!</h2>
<p>嘿 {{name}},这是你的友好补水提醒。你的身体会感谢你的!</p>
<div class="status">
<p><strong>上次喝水时间:</strong> {{lastDrinkTime}}</p>
<p><strong>补水状态:</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方法
}
我们再次添加验证注解,确保正确配置模板ID且格式符合预期。
类似地,在application.yaml文件中添加对应的模板ID属性:
com:
baeldung:
sendgrid:
hydration-alert-notification:
template-id: ${HYDRATION_ALERT_TEMPLATE_ID}
在EmailDispatcher类中发送补水提醒邮件时,将使用这个配置的模板ID。
6.3. 发送模板邮件
现在配置好模板ID,创建自定义Personalization类来保存占位符键名和对应值:
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映射,该映射通过add()*方法填充**。
现在创建新的服务方法发送补水提醒:
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", "很久以前");
personalization.add("hydrationStatus", "渴得像骆驼");
personalization.addTo(toEmail);
Mail mail = new Mail();
mail.setFrom(fromEmail);
mail.setTemplateId(templateId);
mail.addPersonalization(personalization);
// ... 发送请求过程同前
}
在dispatchHydrationAlert()方法中,我们创建DynamicTemplatePersonalization实例,并为HTML模板中定义的占位符添加自定义值。
然后,在将请求发送到SendGrid之前,将此personalization对象和templateId设置到Mail对象上。
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: test@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类中,我们创建自定义Client,将所有请求通过server.url属性指定的代理服务器路由。我们还配置SSL上下文信任所有证书,因为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);
// 配置MockServer预期行为
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头和JSON请求体。我们还指示MockServer在收到此请求时响应202状态码。
接着,使用测试数据调用*dispatchEmail()*方法,并验证是否向MockServer发送了预期请求(仅一次)。
通过使用MockServer模拟SendGrid API,我们确保集成按预期工作,而无需实际发送任何邮件或产生任何费用。
8. 总结
本文探讨了如何在Spring Boot应用中使用SendGrid发送邮件。
我们逐步完成了必要配置,实现了发送简单邮件、带附件邮件以及使用动态模板的HTML邮件功能。
最后,为了验证应用是否向SendGrid发送了正确请求,我们使用MockServer编写了集成测试。
一如既往,本文使用的所有代码示例可在GitHub上获取。