1. 为什么选择 QQ 邮箱?

1.1 QQ 邮箱的优势

  • 免费:个人用户免费使用

  • 稳定:腾讯企业级服务,高可用性

  • 简单:SMTP/POP3 配置简单

  • 安全:支持 SSL/TLS 加密

  • 普及率高:国内用户覆盖面广

1.2 应用场景

  • 系统异常告警

  • 用户注册/登录通知

  • 密码找回

  • 业务操作提醒

  • 定时报表发送


2. QQ 邮箱准备工作

2.1 开启 SMTP 服务

  1. 登录 QQ 邮箱mail.qq.com

  2. 进入设置:点击左上角"设置" → 选择"账户"

  3. 开启服务

    • 找到 "POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务"

    • 开启 "IMAP/SMTP服务" 或 "POP3/SMTP服务"

    • 点击"开启"

  4. 获取授权码

    • 按照提示发送短信验证

    • 获取 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 最佳实践

  1. 敏感信息保护:授权码使用环境变量,不硬编码

  2. 异步发送:避免邮件发送阻塞主业务

  3. 频率限制:防止邮件轰炸

  4. 降级策略:邮件服务不可用时不影响主业务

  5. 模板管理:使用 Thymeleaf 管理邮件模板,便于维护

  6. 监控告警:邮件服务本身也需要监控

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐