基于 QQ 邮箱的邮件配置与异常通知
✅ QQ 邮箱 SMTP 配置✅ 多种邮件类型支持(文本、HTML、附件、模板)✅ 完善的异常通知系统✅ 全局异常捕获与邮件告警✅ 邮件发送队列与异步处理✅ 频率限制与健康检查。
1. 为什么选择 QQ 邮箱?
1.1 QQ 邮箱的优势
-
免费:个人用户免费使用
-
稳定:腾讯企业级服务,高可用性
-
简单:SMTP/POP3 配置简单
-
安全:支持 SSL/TLS 加密
-
普及率高:国内用户覆盖面广
1.2 应用场景
-
系统异常告警
-
用户注册/登录通知
-
密码找回
-
业务操作提醒
-
定时报表发送
2. QQ 邮箱准备工作
2.1 开启 SMTP 服务
-
登录 QQ 邮箱:mail.qq.com
-
进入设置:点击左上角"设置" → 选择"账户"
-
开启服务:
-
找到 "POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务"
-
开启 "IMAP/SMTP服务" 或 "POP3/SMTP服务"
-
点击"开启"
-
-
获取授权码:
-
按照提示发送短信验证
-
获取 16 位授权码(重要!后续配置使用)
-
⚠️ 授权码相当于密码,请妥善保管
-
2.2 QQ 邮箱 SMTP 服务器信息
| 配置项 | 值 |
|---|---|
| SMTP 服务器 | smtp.qq.com |
| SMTP 端口(SSL) | 465 或 587 |
| 启用 SSL | 是 |
| 发件人邮箱 | your-qq-number@qq.com |
| 授权码 | 开启服务后获得的 16 位字符串 |
3. 项目配置(Spring Boot)
3.1 引入依赖(pom.xml)
xml
<!-- 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>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
3.2 配置文件(application.yml)
yaml
spring:
mail:
# QQ 邮箱配置
host: smtp.qq.com
port: 465 # SSL 端口
username: 123456789@qq.com # 替换为你的 QQ 邮箱
password: abcdefghijklmnop # 授权码,不是 QQ 密码!
# SSL 配置
properties:
mail:
smtp:
auth: true
ssl:
enable: true
socketFactory:
class: javax.net.ssl.SSLSocketFactory
port: 465
starttls:
enable: true
required: true
# 调试模式(生产环境关闭)
debug: false
# 编码配置
default-encoding: UTF-8
protocol: smtp
test-connection: false # 启动时测试连接
# 自定义邮件配置
mail:
# 异常通知接收者(多个邮箱用逗号分隔)
alert-recipients: admin@example.com, ops@example.com
# 是否启用邮件通知
alert-enabled: true
# 发送者显示名称
from-name: 系统监控中心
3.3 配置类(可选)
java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;
import java.util.Properties;
@Configuration
public class MailConfig {
@Value("${spring.mail.host}")
private String host;
@Value("${spring.mail.port}")
private Integer port;
@Value("${spring.mail.username}")
private String username;
@Value("${spring.mail.password}")
private String password;
@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(host);
mailSender.setPort(port);
mailSender.setUsername(username);
mailSender.setPassword(password);
Properties props = mailSender.getJavaMailProperties();
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.ssl.enable", "true");
props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
props.put("mail.smtp.socketFactory.port", port);
props.put("mail.debug", "false");
return mailSender;
}
@Bean
public ITemplateResolver thymeleafTemplateResolver() {
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
templateResolver.setPrefix("templates/mail/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode("HTML");
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
@Bean
public SpringTemplateEngine thymeleafTemplateEngine(ITemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
}
4. 邮件发送服务实现
4.1 基础邮件服务
java
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring5.SpringTemplateEngine;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.File;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class MailService {
private final JavaMailSender mailSender;
private final SpringTemplateEngine templateEngine;
@Value("${spring.mail.username}")
private String from;
@Value("${mail.from-name:系统通知}")
private String fromName;
/**
* 发送简单文本邮件
* @param to 收件人
* @param subject 主题
* @param content 内容
*/
public void sendSimpleMail(String to, String subject, String content) {
try {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to.split(","));
message.setSubject(subject);
message.setText(content);
mailSender.send(message);
log.info("简单邮件发送成功: to={}, subject={}", to, subject);
} catch (Exception e) {
log.error("简单邮件发送失败: to={}, subject={}", to, subject, e);
throw new RuntimeException("邮件发送失败", e);
}
}
/**
* 发送 HTML 邮件
* @param to 收件人(多个用逗号分隔)
* @param subject 主题
* @param htmlContent HTML 内容
*/
public void sendHtmlMail(String to, String subject, String htmlContent) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from, fromName);
helper.setTo(to.split(","));
helper.setSubject(subject);
helper.setText(htmlContent, true);
mailSender.send(message);
log.info("HTML邮件发送成功: to={}, subject={}", to, subject);
} catch (Exception e) {
log.error("HTML邮件发送失败: to={}, subject={}", to, subject, e);
throw new RuntimeException("邮件发送失败", e);
}
}
/**
* 发送带附件的邮件
* @param to 收件人
* @param subject 主题
* @param htmlContent HTML 内容
* @param attachments 附件列表(文件路径 -> 附件名称)
*/
public void sendAttachmentMail(String to, String subject, String htmlContent,
Map<String, String> attachments) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from, fromName);
helper.setTo(to.split(","));
helper.setSubject(subject);
helper.setText(htmlContent, true);
// 添加附件
if (attachments != null && !attachments.isEmpty()) {
for (Map.Entry<String, String> entry : attachments.entrySet()) {
FileSystemResource file = new FileSystemResource(new File(entry.getKey()));
helper.addAttachment(entry.getValue(), file);
}
}
mailSender.send(message);
log.info("带附件邮件发送成功: to={}, subject={}", to, subject);
} catch (Exception e) {
log.error("带附件邮件发送失败: to={}, subject={}", to, subject, e);
throw new RuntimeException("邮件发送失败", e);
}
}
/**
* 使用 Thymeleaf 模板发送邮件
* @param to 收件人
* @param subject 主题
* @param templateName 模板名称(不含后缀)
* @param variables 模板变量
*/
public void sendTemplateMail(String to, String subject,
String templateName, Map<String, Object> variables) {
try {
Context context = new Context();
if (variables != null) {
variables.forEach(context::setVariable);
}
String htmlContent = templateEngine.process(templateName, context);
sendHtmlMail(to, subject, htmlContent);
log.info("模板邮件发送成功: to={}, subject={}, template={}", to, subject, templateName);
} catch (Exception e) {
log.error("模板邮件发送失败: to={}, subject={}, template={}", to, subject, templateName, e);
throw new RuntimeException("邮件发送失败", e);
}
}
}
4.2 邮件模板示例
创建模板文件:src/main/resources/templates/mail/exception-alert.html
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>系统异常通知</title>
<style>
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
margin: 0;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 24px;
}
.header .time {
margin-top: 10px;
opacity: 0.9;
font-size: 14px;
}
.content {
padding: 30px;
}
.exception-info {
background: #fff3f3;
border-left: 4px solid #f44336;
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
}
.exception-name {
font-weight: bold;
color: #f44336;
margin-bottom: 10px;
font-size: 18px;
}
.exception-message {
font-family: monospace;
background: #f8f8f8;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
}
.stack-trace {
background: #f8f8f8;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-family: monospace;
font-size: 12px;
margin: 20px 0;
}
.system-info {
background: #f0f7ff;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
}
.system-info p {
margin: 5px 0;
}
.footer {
background: #f8f8f8;
padding: 20px;
text-align: center;
color: #666;
font-size: 12px;
border-top: 1px solid #e0e0e0;
}
.badge {
display: inline-block;
background: #f44336;
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
}
.severity-high {
background: #f44336;
}
.severity-medium {
background: #ff9800;
}
.severity-low {
background: #4caf50;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>⚠️ 系统异常告警</h1>
<div class="time">
<span th:text="${timestamp}"></span>
</div>
</div>
<div class="content">
<div class="exception-info">
<div class="exception-name">
<span class="badge" th:classappend="'severity-' + ${severity}">异常</span>
<span th:text="${exceptionName}"></span>
</div>
<div class="exception-message" th:text="${message}"></div>
</div>
<div class="system-info">
<h3>📊 系统信息</h3>
<p><strong>应用名称:</strong> <span th:text="${applicationName}"></span></p>
<p><strong>服务器IP:</strong> <span th:text="${serverIp}"></span></p>
<p><strong>请求URL:</strong> <span th:text="${requestUrl}"></span></p>
<p><strong>请求方法:</strong> <span th:text="${requestMethod}"></span></p>
<p><strong>请求参数:</strong> <span th:text="${requestParams}"></span></p>
<p><strong>用户IP:</strong> <span th:text="${clientIp}"></span></p>
<p><strong>用户代理:</strong> <span th:text="${userAgent}"></span></p>
</div>
<div th:if="${stackTrace != null}">
<h3>🔍 异常堆栈</h3>
<div class="stack-trace">
<pre th:text="${stackTrace}"></pre>
</div>
</div>
<div th:if="${suggestion != null}">
<h3>💡 处理建议</h3>
<div class="exception-info" style="background: #e8f5e9; border-left-color: #4caf50;">
<div th:text="${suggestion}"></div>
</div>
</div>
</div>
<div class="footer">
<p>此为系统自动发送的异常告警邮件,请勿直接回复。</p>
<p>如需帮助,请联系系统管理员。</p>
</div>
</div>
</body>
</html>
5. 异常通知系统
5.1 异常信息封装类
java
import lombok.Builder;
import lombok.Data;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
@Data
@Builder
public class ExceptionAlert {
private String applicationName; // 应用名称
private String serverIp; // 服务器IP
private String exceptionName; // 异常名称
private String message; // 异常消息
private String stackTrace; // 异常堆栈
private String severity; // 严重级别 (high/medium/low)
private String requestUrl; // 请求URL
private String requestMethod; // 请求方法
private Map<String, String[]> requestParams; // 请求参数
private String clientIp; // 客户端IP
private String userAgent; // 用户代理
private String timestamp; // 发生时间
private String suggestion; // 处理建议
/**
* 从异常构建告警信息
*/
public static ExceptionAlert fromException(Exception e, String applicationName, String serverIp) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
return ExceptionAlert.builder()
.applicationName(applicationName)
.serverIp(serverIp)
.exceptionName(e.getClass().getSimpleName())
.message(e.getMessage())
.stackTrace(sw.toString())
.severity(determineSeverity(e))
.timestamp(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
.suggestion(getSuggestion(e))
.build();
}
/**
* 根据异常类型判断严重级别
*/
private static String determineSeverity(Exception e) {
if (e instanceof NullPointerException || e instanceof IllegalArgumentException) {
return "medium";
} else if (e instanceof RuntimeException) {
return "high";
} else if (e instanceof Exception) {
return "low";
}
return "medium";
}
/**
* 获取处理建议
*/
private static String getSuggestion(Exception e) {
if (e instanceof NullPointerException) {
return "请检查相关对象是否为 null,确保在使用前进行判空处理。";
} else if (e instanceof IllegalArgumentException) {
return "请检查传入的参数是否符合要求,确保参数格式和范围正确。";
} else if (e instanceof RuntimeException) {
return "请查看详细堆栈信息,定位具体问题代码位置。";
}
return "请查看详细错误信息,并根据堆栈定位问题。";
}
}
5.2 异常通知服务
java
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class ExceptionAlertService {
private final MailService mailService;
@Value("${mail.alert-recipients}")
private String alertRecipients;
@Value("${mail.alert-enabled:true}")
private boolean alertEnabled;
@Value("${spring.application.name:未知应用}")
private String applicationName;
/**
* 发送异常告警邮件
*/
public void sendExceptionAlert(Exception e) {
if (!alertEnabled) {
log.debug("邮件告警已禁用,不发送异常通知");
return;
}
try {
// 获取当前请求信息
HttpServletRequest request = getCurrentRequest();
String serverIp = getServerIp();
// 构建告警信息
ExceptionAlert alert = ExceptionAlert.fromException(e, applicationName, serverIp);
// 填充请求相关信息
if (request != null) {
alert.setRequestUrl(request.getRequestURL().toString());
alert.setRequestMethod(request.getMethod());
alert.setRequestParams(request.getParameterMap());
alert.setClientIp(getClientIp(request));
alert.setUserAgent(request.getHeader("User-Agent"));
}
// 发送邮件
sendAlertEmail(alert);
} catch (Exception ex) {
log.error("发送异常告警邮件失败", ex);
}
}
/**
* 发送自定义告警
*/
public void sendCustomAlert(String title, String content, String severity) {
if (!alertEnabled) {
return;
}
try {
Map<String, Object> variables = new HashMap<>();
variables.put("title", title);
variables.put("content", content);
variables.put("severity", severity);
variables.put("timestamp", java.time.LocalDateTime.now()
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
variables.put("applicationName", applicationName);
variables.put("serverIp", getServerIp());
mailService.sendTemplateMail(alertRecipients, "【系统告警】" + title,
"custom-alert", variables);
log.info("自定义告警邮件发送成功: title={}", title);
} catch (Exception e) {
log.error("发送自定义告警失败", e);
}
}
/**
* 发送告警邮件
*/
private void sendAlertEmail(ExceptionAlert alert) {
Map<String, Object> variables = new HashMap<>();
variables.put("exceptionName", alert.getExceptionName());
variables.put("message", alert.getMessage());
variables.put("stackTrace", alert.getStackTrace());
variables.put("severity", alert.getSeverity());
variables.put("timestamp", alert.getTimestamp());
variables.put("applicationName", alert.getApplicationName());
variables.put("serverIp", alert.getServerIp());
variables.put("requestUrl", alert.getRequestUrl());
variables.put("requestMethod", alert.getRequestMethod());
variables.put("clientIp", alert.getClientIp());
variables.put("userAgent", alert.getUserAgent());
variables.put("suggestion", alert.getSuggestion());
// 格式化请求参数
if (alert.getRequestParams() != null && !alert.getRequestParams().isEmpty()) {
StringBuilder params = new StringBuilder();
alert.getRequestParams().forEach((key, values) -> {
params.append(key).append("=");
if (values != null && values.length > 0) {
params.append(String.join(",", values));
}
params.append("; ");
});
variables.put("requestParams", params.toString());
} else {
variables.put("requestParams", "无");
}
String subject = String.format("【%s】%s - %s",
alert.getSeverity().toUpperCase(),
alert.getExceptionName(),
alert.getApplicationName());
mailService.sendTemplateMail(alertRecipients, subject, "exception-alert", variables);
}
/**
* 获取当前请求
*/
private HttpServletRequest getCurrentRequest() {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attributes != null ? attributes.getRequest() : null;
}
/**
* 获取客户端真实IP
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个代理的情况,取第一个IP
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
/**
* 获取服务器IP
*/
private String getServerIp() {
try {
return InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
return "unknown";
}
}
}
5.3 全局异常处理器
java
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {
private final ExceptionAlertService alertService;
/**
* 处理所有未捕获的异常
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, Object> handleException(Exception e) {
log.error("系统异常", e);
// 发送异常告警邮件
alertService.sendExceptionAlert(e);
Map<String, Object> response = new HashMap<>();
response.put("code", 500);
response.put("message", "系统内部错误,请稍后再试");
response.put("error", e.getMessage());
return response;
}
/**
* 处理自定义业务异常
*/
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, Object> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
Map<String, Object> response = new HashMap<>();
response.put("code", e.getCode());
response.put("message", e.getMessage());
return response;
}
/**
* 处理参数校验异常
*/
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, Object> handleIllegalArgumentException(IllegalArgumentException e) {
log.warn("参数异常: {}", e.getMessage());
Map<String, Object> response = new HashMap<>();
response.put("code", 400);
response.put("message", e.getMessage());
return response;
}
}
5.4 业务异常类
java
import lombok.Getter;
@Getter
public class BusinessException extends RuntimeException {
private final Integer code;
public BusinessException(String message) {
super(message);
this.code = 500;
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
this.code = 500;
}
}
6. 高级功能
6.1 邮件发送队列(防止阻塞)
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class MailQueueService {
private final MailService mailService;
private final ThreadPoolExecutor executor;
@Autowired
public MailQueueService(MailService mailService) {
this.mailService = mailService;
this.executor = new ThreadPoolExecutor(
2, 5, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
/**
* 异步发送邮件
*/
@Async
public void sendAsync(Runnable mailTask) {
executor.execute(() -> {
try {
mailTask.run();
} catch (Exception e) {
log.error("异步发送邮件失败", e);
}
});
}
/**
* 发送简单邮件(异步)
*/
public void sendSimpleMailAsync(String to, String subject, String content) {
sendAsync(() -> mailService.sendSimpleMail(to, subject, content));
}
/**
* 发送模板邮件(异步)
*/
public void sendTemplateMailAsync(String to, String subject,
String template, Map<String, Object> variables) {
sendAsync(() -> mailService.sendTemplateMail(to, subject, template, variables));
}
}
6.2 邮件发送频率限制
java
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Component
public class MailRateLimiter {
// 限制每个接收者每小时最多接收10封邮件
private static final int MAX_EMAILS_PER_HOUR = 10;
private final Cache<String, AtomicInteger> cache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
/**
* 检查是否允许发送邮件
*/
public boolean allowSend(String recipient) {
try {
AtomicInteger count = cache.get(recipient, AtomicInteger::new);
if (count.get() >= MAX_EMAILS_PER_HOUR) {
return false;
}
count.incrementAndGet();
return true;
} catch (Exception e) {
return true; // 缓存异常时允许发送
}
}
/**
* 获取当前小时已发送数量
*/
public int getCurrentCount(String recipient) {
AtomicInteger count = cache.getIfPresent(recipient);
return count != null ? count.get() : 0;
}
}
6.3 健康检查与重试机制
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class MailHealthChecker {
@Autowired
private MailService mailService;
@Autowired
private ApplicationEventPublisher eventPublisher;
private boolean mailAvailable = true;
/**
* 每5分钟检查一次邮件服务状态
*/
@Scheduled(fixedDelay = 300000)
public void checkMailHealth() {
try {
// 发送测试邮件到系统邮箱
mailService.sendSimpleMail("admin@localhost", "Health Check", "Mail service is working");
if (!mailAvailable) {
log.info("邮件服务已恢复");
mailAvailable = true;
// 发布服务恢复事件
AvailabilityChangeEvent.publish(eventPublisher, this, ReadinessState.ACCEPTING_TRAFFIC);
}
} catch (Exception e) {
if (mailAvailable) {
log.error("邮件服务异常", e);
mailAvailable = false;
// 发布服务不可用事件
AvailabilityChangeEvent.publish(eventPublisher, this, ReadinessState.REFUSING_TRAFFIC);
}
}
}
public boolean isMailAvailable() {
return mailAvailable;
}
}
7. 测试用例
7.1 单元测试
java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.HashMap;
import java.util.Map;
@SpringBootTest
class MailServiceTest {
@Autowired
private MailService mailService;
@Test
void testSendSimpleMail() {
mailService.sendSimpleMail("test@example.com", "测试邮件", "这是一封测试邮件");
}
@Test
void testSendHtmlMail() {
String html = "<h1>测试邮件</h1><p>这是一封HTML邮件</p>";
mailService.sendHtmlMail("test@example.com", "HTML测试", html);
}
@Test
void testSendTemplateMail() {
Map<String, Object> variables = new HashMap<>();
variables.put("username", "张三");
variables.put("content", "您的订单已发货");
mailService.sendTemplateMail("test@example.com", "订单通知",
"order-notification", variables);
}
}
7.2 异常告警测试
java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class ExceptionAlertTest {
@Autowired
private ExceptionAlertService alertService;
@Test
void testExceptionAlert() {
try {
// 模拟空指针异常
String str = null;
str.length();
} catch (Exception e) {
alertService.sendExceptionAlert(e);
}
}
@Test
void testCustomAlert() {
alertService.sendCustomAlert("数据库连接失败",
"数据库连接池已耗尽,请检查数据库状态",
"high");
}
}
8. 常见问题与解决方案
8.1 邮件发送超时
问题:发送邮件时出现超时异常
解决:增加超时配置
yaml
spring:
mail:
properties:
mail:
smtp:
connectiontimeout: 5000 # 连接超时(毫秒)
timeout: 5000 # 读取超时
writetimeout: 5000 # 写入超时
8.2 授权码错误
错误信息:535 Error: authentication failed
原因:使用了 QQ 密码而非授权码
解决:在 QQ 邮箱设置中获取正确的 16 位授权码
8.3 SSL 连接失败
错误信息:javax.net.ssl.SSLHandshakeException
解决:添加 SSL 配置或使用 587 端口
yaml
spring:
mail:
port: 587
properties:
mail:
smtp:
starttls:
enable: true
required: true
8.4 邮件被识别为垃圾邮件
解决建议:
-
使用企业邮箱域名
-
设置正确的邮件主题和内容格式
-
添加退订链接
-
控制发送频率
8.5 生产环境安全配置
yaml
# 使用环境变量存储敏感信息
spring:
mail:
username: ${QQ_MAIL_USERNAME}
password: ${QQ_MAIL_PASSWORD}
# 生产环境关闭调试
debug: false
9. 总结
9.1 核心功能
✅ QQ 邮箱 SMTP 配置
✅ 多种邮件类型支持(文本、HTML、附件、模板)
✅ 完善的异常通知系统
✅ 全局异常捕获与邮件告警
✅ 邮件发送队列与异步处理
✅ 频率限制与健康检查
9.2 最佳实践
-
敏感信息保护:授权码使用环境变量,不硬编码
-
异步发送:避免邮件发送阻塞主业务
-
频率限制:防止邮件轰炸
-
降级策略:邮件服务不可用时不影响主业务
-
模板管理:使用 Thymeleaf 管理邮件模板,便于维护
-
监控告警:邮件服务本身也需要监控
更多推荐
所有评论(0)