📖 概述
Java模板异步邮件发送是一种高效的邮件处理方案,它结合了以下几个关键技术:
🎨 模板引擎
使用Thymeleaf或Freemarker等模板引擎,支持动态内容生成,提供丰富的邮件样式。
⚡ 异步处理
采用异步发送机制,避免阻塞主线程,提高系统响应速度和用户体验。
📧 JavaMail API
基于标准的JavaMail API,支持多种邮件协议,兼容性强。
🔧 Spring Boot集成
与Spring Boot无缝集成,配置简单,支持自动配置和依赖注入。
🔄 发送流程
1
准备邮件数据和模板参数
2
模板引擎渲染邮件内容
3
异步队列接收邮件任务
4
后台线程处理邮件发送
5
记录发送结果和异常处理
📦 依赖配置
Maven依赖
在pom.xml中添加以下依赖:
<dependencies>
<!-- Spring Boot邮件启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- Thymeleaf模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 异步支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 可选:Redis支持(用于邮件队列) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
⚙️ 邮件配置
application.yml配置
spring:
# 邮件配置
mail:
host: smtp.gmail.com
port: 587
username: your-email@gmail.com
password: your-app-password
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
# 模板配置
thymeleaf:
prefix: classpath:/templates/
suffix: .html
mode: HTML
encoding: UTF-8
cache: false
# 异步配置
task:
execution:
pool:
core-size: 5
max-size: 20
queue-capacity: 200
thread-name-prefix: mail-task-
Java配置类
@Configuration
@EnableAsync
public class MailConfig {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("mail-task-");
executor.initialize();
return executor;
}
@Bean
public TemplateEngine templateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
return templateEngine;
}
@Bean
public ITemplateResolver templateResolver() {
ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver();
resolver.setPrefix("templates/");
resolver.setSuffix(".html");
resolver.setTemplateMode(TemplateMode.HTML);
resolver.setCharacterEncoding("UTF-8");
resolver.setCacheable(false);
return resolver;
}
}
📝 邮件模板
创建邮件模板
在 src/main/resources/templates/
目录下创建邮件模板:
欢迎邮件模板 (welcome.html)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>欢迎加入我们</title>
<style>
.email-container {
max-width: 600px;
margin: 0 auto;
font-family: Arial, sans-serif;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.content {
padding: 30px;
background: #ffffff;
}
.button {
display: inline-block;
padding: 12px 30px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 5px;
margin: 20px 0;
}
.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<h1>欢迎加入我们!</h1>
</div>
<div class="content">
<h2>亲爱的 <span th:text="${username}">用户</span>,</h2>
<p>感谢您注册我们的服务!您的账户已经成功创建。</p>
<div th:if="${activationRequired}">
<p>请点击下面的按钮激活您的账户:</p>
<a th:href="${activationLink}" class="button">激活账户</a>
</div>
<div th:if="${!activationRequired}">
<p>您现在可以开始使用我们的服务了!</p>
<a th:href="${loginLink}" class="button">立即登录</a>
</div>
<h3>账户信息:</h3>
<ul>
<li>邮箱:<span th:text="${email}">email@example.com</span></li>
<li>注册时间:<span th:text="${#dates.format(registerTime, 'yyyy-MM-dd HH:mm')}">2024-01-01 12:00</span></li>
</ul>
</div>
<div class="footer">
<p>如果您有任何问题,请联系我们的客服团队。</p>
<p>© 2024 我们的公司. 保留所有权利。</p>
</div>
</div>
</body>
</html>
🎯 邮件服务
邮件服务接口
public interface MailService {
/**
* 发送简单文本邮件
*/
void sendSimpleMail(String to, String subject, String content);
/**
* 发送HTML邮件
*/
void sendHtmlMail(String to, String subject, String content);
/**
* 发送模板邮件
*/
void sendTemplateMail(String to, String subject, String templateName, Map<String, Object> variables);
/**
* 异步发送模板邮件
*/
CompletableFuture<Boolean> sendTemplateMailAsync(String to, String subject, String templateName, Map<String, Object> variables);
/**
* 批量发送邮件
*/
void sendBatchMail(List<String> toList, String subject, String templateName, Map<String, Object> variables);
}
邮件服务实现
@Service
@Slf4j
public class MailServiceImpl implements MailService {
@Autowired
private JavaMailSender mailSender;
@Autowired
private TemplateEngine templateEngine;
@Value("${spring.mail.username}")
private String from;
@Override
public void sendSimpleMail(String to, String subject, String content) {
try {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(content);
mailSender.send(message);
log.info("简单邮件发送成功:{}", to);
} catch (Exception e) {
log.error("简单邮件发送失败:{}", e.getMessage());
throw new RuntimeException("邮件发送失败", e);
}
}
@Override
public void sendHtmlMail(String to, String subject, String content) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
mailSender.send(message);
log.info("HTML邮件发送成功:{}", to);
} catch (Exception e) {
log.error("HTML邮件发送失败:{}", e.getMessage());
throw new RuntimeException("邮件发送失败", e);
}
}
@Override
public void sendTemplateMail(String to, String subject, String templateName, Map<String, Object> variables) {
try {
Context context = new Context();
context.setVariables(variables);
String content = templateEngine.process(templateName, context);
sendHtmlMail(to, subject, content);
log.info("模板邮件发送成功:{}", to);
} catch (Exception e) {
log.error("模板邮件发送失败:{}", e.getMessage());
throw new RuntimeException("邮件发送失败", e);
}
}
@Override
@Async
public CompletableFuture<Boolean> sendTemplateMailAsync(String to, String subject, String templateName, Map<String, Object> variables) {
try {
sendTemplateMail(to, subject, templateName, variables);
return CompletableFuture.completedFuture(true);
} catch (Exception e) {
log.error("异步邮件发送失败:{}", e.getMessage());
return CompletableFuture.completedFuture(false);
}
}
@Override
@Async
public void sendBatchMail(List<String> toList, String subject, String templateName, Map<String, Object> variables) {
toList.parallelStream().forEach(to -> {
try {
sendTemplateMail(to, subject, templateName, variables);
} catch (Exception e) {
log.error("批量邮件发送失败,收件人:{},错误:{}", to, e.getMessage());
}
});
}
}
⚡ 异步处理
异步配置
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("async-mail-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, objects) -> {
log.error("异步执行异常:", throwable);
log.error("异步方法:{}", method.getName());
log.error("异步参数:{}", Arrays.toString(objects));
};
}
}
邮件队列服务
@Service
@Slf4j
public class MailQueueService {
@Autowired
private MailService mailService;
@Autowired
private StringRedisTemplate redisTemplate;
private static final String MAIL_QUEUE_KEY = "mail:queue";
/**
* 将邮件任务加入队列
*/
public void pushToQueue(MailTask mailTask) {
try {
String taskJson = JSON.toJSONString(mailTask);
redisTemplate.opsForList().leftPush(MAIL_QUEUE_KEY, taskJson);
log.info("邮件任务已加入队列:{}", mailTask.getTo());
} catch (Exception e) {
log.error("邮件任务加入队列失败:{}", e.getMessage());
}
}
/**
* 从队列中消费邮件任务
*/
@Scheduled(fixedDelay = 1000)
public void consumeMailQueue() {
try {
String taskJson = redisTemplate.opsForList().rightPop(MAIL_QUEUE_KEY);
if (taskJson != null) {
MailTask mailTask = JSON.parseObject(taskJson, MailTask.class);
processMailTask(mailTask);
}
} catch (Exception e) {
log.error("消费邮件队列失败:{}", e.getMessage());
}
}
@Async
private void processMailTask(MailTask mailTask) {
try {
mailService.sendTemplateMail(
mailTask.getTo(),
mailTask.getSubject(),
mailTask.getTemplateName(),
mailTask.getVariables()
);
log.info("邮件任务处理成功:{}", mailTask.getTo());
} catch (Exception e) {
log.error("邮件任务处理失败:{}", e.getMessage());
// 可以实现重试机制
retryMailTask(mailTask);
}
}
private void retryMailTask(MailTask mailTask) {
if (mailTask.getRetryCount() < 3) {
mailTask.setRetryCount(mailTask.getRetryCount() + 1);
pushToQueue(mailTask);
log.info("邮件任务重试:{},次数:{}", mailTask.getTo(), mailTask.getRetryCount());
} else {
log.error("邮件任务重试次数超限:{}", mailTask.getTo());
}
}
}
🚀 使用示例
控制器示例
@RestController
@RequestMapping("/api/mail")
@Slf4j
public class MailController {
@Autowired
private MailService mailService;
@Autowired
private MailQueueService mailQueueService;
/**
* 发送欢迎邮件
*/
@PostMapping("/welcome")
public ResponseEntity<String> sendWelcomeMail(@RequestBody WelcomeMailRequest request) {
try {
Map<String, Object> variables = new HashMap<>();
variables.put("username", request.getUsername());
variables.put("email", request.getEmail());
variables.put("registerTime", new Date());
variables.put("activationRequired", request.isActivationRequired());
variables.put("activationLink", request.getActivationLink());
variables.put("loginLink", request.getLoginLink());
// 异步发送
CompletableFuture<Boolean> future = mailService.sendTemplateMailAsync(
request.getEmail(),
"欢迎加入我们!",
"welcome",
variables
);
return ResponseEntity.ok("邮件发送任务已提交");
} catch (Exception e) {
log.error("发送欢迎邮件失败:{}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("邮件发送失败");
}
}
/**
* 批量发送邮件
*/
@PostMapping("/batch")
public ResponseEntity<String> sendBatchMail(@RequestBody BatchMailRequest request) {
try {
Map<String, Object> variables = new HashMap<>();
variables.put("content", request.getContent());
variables.put("sendTime", new Date());
mailService.sendBatchMail(
request.getToList(),
request.getSubject(),
request.getTemplateName(),
variables
);
return ResponseEntity.ok("批量邮件发送任务已提交");
} catch (Exception e) {
log.error("批量发送邮件失败:{}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("批量邮件发送失败");
}
}
/**
* 使用队列发送邮件
*/
@PostMapping("/queue")
public ResponseEntity<String> sendMailWithQueue(@RequestBody MailTask mailTask) {
try {
mailQueueService.pushToQueue(mailTask);
return ResponseEntity.ok("邮件已加入发送队列");
} catch (Exception e) {
log.error("邮件加入队列失败:{}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("邮件队列处理失败");
}
}
}
数据传输对象
@Data
public class WelcomeMailRequest {
private String username;
private String email;
private boolean activationRequired;
private String activationLink;
private String loginLink;
}
@Data
public class BatchMailRequest {
private List<String> toList;
private String subject;
private String templateName;
private String content;
}
@Data
public class MailTask {
private String to;
private String subject;
private String templateName;
private Map<String, Object> variables;
private int retryCount = 0;
private Date createTime = new Date();
}
💡 最佳实践
🔧 配置管理
- 使用配置文件管理邮件服务器设置
- 支持多环境配置(开发、测试、生产)
- 敏感信息使用环境变量或配置中心
- 支持动态配置更新
📈 性能优化
- 使用连接池管理邮件连接
- 合理设置线程池大小
- 批量发送时控制并发数
- 模板缓存提高渲染性能
🛡️ 安全考虑
- 使用应用密码而非账户密码
- 启用SSL/TLS加密传输
- 验证邮件地址格式
- 防止邮件注入攻击
📊 监控告警
- 记录邮件发送成功率
- 监控队列长度和处理速度
- 设置异常告警机制
- 定期清理失效任务
💎 核心建议
- 异步处理:邮件发送应该异步进行,避免阻塞用户操作
- 失败重试:实现合理的重试机制,处理网络波动和临时故障
- 限流控制:避免短时间内大量邮件发送被邮件服务商限制
- 模板管理:统一管理邮件模板,支持版本控制和预览功能
🔧 高级功能
邮件监控服务
@Component
@Slf4j
public class MailMonitorService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String MAIL_STATS_KEY = "mail:stats";
private static final String MAIL_FAILED_KEY = "mail:failed";
/**
* 记录邮件发送统计
*/
public void recordMailStats(String type, boolean success) {
try {
String date = LocalDate.now().toString();
String key = MAIL_STATS_KEY + ":" + date;
if (success) {
redisTemplate.opsForHash().increment(key, type + ":success", 1);
} else {
redisTemplate.opsForHash().increment(key, type + ":failed", 1);
// 记录失败详情
recordFailedMail(type, new Date());
}
// 设置过期时间30天
redisTemplate.expire(key, Duration.ofDays(30));
} catch (Exception e) {
log.error("记录邮件统计失败:{}", e.getMessage());
}
}
/**
* 记录失败的邮件
*/
private void recordFailedMail(String type, Date failTime) {
try {
Map<String, Object> failedInfo = new HashMap<>();
failedInfo.put("type", type);
failedInfo.put("failTime", failTime);
failedInfo.put("timestamp", System.currentTimeMillis());
redisTemplate.opsForList().leftPush(MAIL_FAILED_KEY, JSON.toJSONString(failedInfo));
// 只保留最近1000条失败记录
redisTemplate.opsForList().trim(MAIL_FAILED_KEY, 0, 999);
} catch (Exception e) {
log.error("记录失败邮件失败:{}", e.getMessage());
}
}
/**
* 获取邮件发送统计
*/
public Map<String, Object> getMailStats(String date) {
try {
String key = MAIL_STATS_KEY + ":" + date;
return redisTemplate.opsForHash().entries(key);
} catch (Exception e) {
log.error("获取邮件统计失败:{}", e.getMessage());
return new HashMap<>();
}
}
/**
* 健康检查
*/
@Scheduled(fixedRate = 300000) // 5分钟检查一次
public void healthCheck() {
try {
// 检查队列长度
Long queueSize = redisTemplate.opsForList().size("mail:queue");
if (queueSize != null && queueSize > 1000) {
log.warn("邮件队列积压严重,当前长度:{}", queueSize);
// 这里可以发送告警
}
// 检查最近的失败率
Map<String, Object> todayStats = getMailStats(LocalDate.now().toString());
if (!todayStats.isEmpty()) {
calculateAndLogFailureRate(todayStats);
}
} catch (Exception e) {
log.error("邮件服务健康检查失败:{}", e.getMessage());
}
}
private void calculateAndLogFailureRate(Map<String, Object> stats) {
int totalSuccess = 0;
int totalFailed = 0;
for (Map.Entry<String, Object> entry : stats.entrySet()) {
String key = entry.getKey();
int value = Integer.parseInt(entry.getValue().toString());
if (key.endsWith(":success")) {
totalSuccess += value;
} else if (key.endsWith(":failed")) {
totalFailed += value;
}
}
if (totalSuccess + totalFailed > 0) {
double failureRate = (double) totalFailed / (totalSuccess + totalFailed) * 100;
log.info("今日邮件发送统计 - 成功:{},失败:{},失败率:{:.2f}%",
totalSuccess, totalFailed, failureRate);
if (failureRate > 10) { // 失败率超过10%告警
log.warn("邮件失败率过高:{:.2f}%", failureRate);
}
}
}
}
邮件模板管理器
@Component
@Slf4j
public class MailTemplateManager {
@Autowired
private TemplateEngine templateEngine;
@Autowired
private StringRedisTemplate redisTemplate;
private static final String TEMPLATE_CACHE_KEY = "mail:template:cache:";
/**
* 渲染邮件模板
*/
public String renderTemplate(String templateName, Map<String, Object> variables) {
try {
// 先从缓存获取
String cachedTemplate = getCachedTemplate(templateName);
if (cachedTemplate != null) {
return processTemplate(cachedTemplate, variables);
}
// 渲染模板
Context context = new Context();
context.setVariables(variables);
String content = templateEngine.process(templateName, context);
// 缓存模板(仅在生产环境)
if (isProductionEnvironment()) {
cacheTemplate(templateName, content);
}
return content;
} catch (Exception e) {
log.error("渲染邮件模板失败:{}", e.getMessage());
throw new RuntimeException("模板渲染失败", e);
}
}
/**
* 预览邮件模板
*/
public String previewTemplate(String templateName, Map<String, Object> variables) {
try {
Context context = new Context();
context.setVariables(variables);
return templateEngine.process(templateName, context);
} catch (Exception e) {
log.error("预览邮件模板失败:{}", e.getMessage());
throw new RuntimeException("模板预览失败", e);
}
}
/**
* 验证模板语法
*/
public boolean validateTemplate(String templateName) {
try {
Map<String, Object> testVariables = createTestVariables();
renderTemplate(templateName, testVariables);
return true;
} catch (Exception e) {
log.error("模板验证失败:{}", e.getMessage());
return false;
}
}
private Map<String, Object> createTestVariables() {
Map<String, Object> variables = new HashMap<>();
variables.put("username", "测试用户");
variables.put("email", "test@example.com");
variables.put("registerTime", new Date());
variables.put("activationRequired", true);
variables.put("activationLink", "http://example.com/activate");
variables.put("loginLink", "http://example.com/login");
return variables;
}
private String getCachedTemplate(String templateName) {
try {
return redisTemplate.opsForValue().get(TEMPLATE_CACHE_KEY + templateName);
} catch (Exception e) {
log.warn("获取模板缓存失败:{}", e.getMessage());
return null;
}
}
private void cacheTemplate(String templateName, String content) {
try {
redisTemplate.opsForValue().set(
TEMPLATE_CACHE_KEY + templateName,
content,
Duration.ofHours(1)
);
} catch (Exception e) {
log.warn("缓存模板失败:{}", e.getMessage());
}
}
private String processTemplate(String cachedTemplate, Map<String, Object> variables) {
// 这里可以实现简单的变量替换逻辑
// 或者使用其他轻量级模板引擎
return cachedTemplate;
}
private boolean isProductionEnvironment() {
// 判断是否为生产环境
return "production".equals(System.getProperty("spring.profiles.active"));
}
}
邮件发送限流器
@Component
@Slf4j
public class MailRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String RATE_LIMIT_KEY = "mail:rate:limit:";
/**
* 检查是否可以发送邮件
*/
public boolean canSendMail(String identifier, int maxCount, Duration duration) {
try {
String key = RATE_LIMIT_KEY + identifier;
String countStr = redisTemplate.opsForValue().get(key);
if (countStr == null) {
// 第一次发送
redisTemplate.opsForValue().set(key, "1", duration);
return true;
}
int currentCount = Integer.parseInt(countStr);
if (currentCount >= maxCount) {
log.warn("邮件发送频率超限:{},当前次数:{}", identifier, currentCount);
return false;
}
// 增加计数
redisTemplate.opsForValue().increment(key);
return true;
} catch (Exception e) {
log.error("检查邮件发送限制失败:{}", e.getMessage());
return true; // 异常情况下允许发送
}
}
/**
* 全局限流检查
*/
public boolean canSendGlobalMail() {
return canSendMail("global", 1000, Duration.ofMinutes(1)); // 每分钟最多1000封
}
/**
* 用户限流检查
*/
public boolean canSendUserMail(String userEmail) {
return canSendMail("user:" + userEmail, 10, Duration.ofMinutes(10)); // 每10分钟最多10封
}
/**
* IP限流检查
*/
public boolean canSendIpMail(String ipAddress) {
return canSendMail("ip:" + ipAddress, 50, Duration.ofMinutes(5)); // 每5分钟最多50封
}
}
🧪 测试用例
@SpringBootTest
@TestPropertySource(properties = {
"spring.mail.host=smtp.gmail.com",
"spring.mail.port=587",
"spring.mail.username=test@gmail.com",
"spring.mail.password=testpassword"
})
class MailServiceTest {
@Autowired
private MailService mailService;
@MockBean
private JavaMailSender mailSender;
@Test
void testSendSimpleMail() {
// Given
String to = "recipient@example.com";
String subject = "测试邮件";
String content = "这是一封测试邮件";
// When
mailService.sendSimpleMail(to, subject, content);
// Then
verify(mailSender, times(1)).send(any(SimpleMailMessage.class));
}
@Test
void testSendTemplateMail() {
// Given
String to = "recipient@example.com";
String subject = "欢迎邮件";
String templateName = "welcome";
Map<String, Object> variables = new HashMap<>();
variables.put("username", "测试用户");
variables.put("email", to);
// When
mailService.sendTemplateMail(to, subject, templateName, variables);
// Then
verify(mailSender, times(1)).send(any(MimeMessage.class));
}
@Test
void testSendTemplateMailAsync() throws Exception {
// Given
String to = "recipient@example.com";
String subject = "异步邮件";
String templateName = "welcome";
Map<String, Object> variables = new HashMap<>();
variables.put("username", "测试用户");
// When
CompletableFuture<Boolean> future = mailService.sendTemplateMailAsync(to, subject, templateName, variables);
Boolean result = future.get(5, TimeUnit.SECONDS);
// Then
assertTrue(result);
verify(mailSender, times(1)).send(any(MimeMessage.class));
}
}
🔍 故障排查
🚨 常见问题
- 认证失败:检查邮箱账号密码,确保开启了SMTP服务
- 连接超时:检查网络连接,确认SMTP服务器地址和端口
- 发送频率限制:检查邮件服务商的发送限制
- 模板渲染错误:检查模板语法和变量名
🔧 调试技巧
- 开启JavaMail调试日志:
mail.debug=true
- 使用邮件测试工具验证SMTP配置
- 检查防火墙和代理设置
- 监控邮件队列和线程池状态
📋 日志配置
- 记录邮件发送的详细日志
- 分级记录不同类型的异常
- 使用结构化日志便于分析
- 定期清理过期日志文件
⚠️ 注意事项
- 避免在邮件内容中包含敏感信息
- 合理设置邮件重试次数和间隔
- 定期更新邮件模板和配置
- 监控邮件服务的健康状态