构建邮件发送服务与设计模式解耦

108 阅读5分钟

在本篇文章中,我将逐步介绍如何使用Spring BootThymeleaf构建一个邮件发送服务,其中包括验证码和通知邮件的发送功能。我们还将引入发布订阅设计模式来解耦不同类型邮件的逻辑,使代码更加清晰、模块化和可扩展。此外自定义线程池和使用@Async来将我们发送邮件异步化

一、准备工作

在开始之前,确保您的项目已经集成了Spring Boot,并具有所需的依赖项。您还需要一个可用的邮箱账号,以便发送实际邮件。这里以qq邮箱为例。

1.1qq邮箱授权获取 mail.qq.com/

首先进入qq邮箱官网,点击设置、选择账号找到 POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务

image.png

点击管理服务之后生成授权码

image.png

1.2 创建springboot工程

  1. 添加邮箱服务所需依赖
<!-- Spring Boot Starter Thymeleaf -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Spring Boot Starter Thymeleaf -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  1. application.properties 配置我们的邮件信息
spring.mail.host=smtp.qq.com
spring.mail.username=xxxxxxx@qq.com
spring.mail.password=刚刚申请的邮件授权码
spring.mail.default-encoding=utf-8

二、构建邮件发送服务

1.基础邮件功能

@Service
@Slf4j
public class EmailService {

    @Autowired
    private JavaMailSender mailSender;

    @Autowired
    private TemplateEngine templateEngine;

    @Async("emailExecutor")
    public void sendVerificationCode(String recipientEmail, String verificationCode) throws MessagingException {
        sendEmail(recipientEmail, "您的验证码", "verification-email", "verificationCode", verificationCode);
    }

    @Async("emailExecutor")
    public void sendNotification(String recipientEmail, String emailContent, String subject) throws MessagingException {
        sendEmail(recipientEmail, subject, "notification-email", "content", emailContent);
    }

    private void sendEmail(String recipientEmail, String subject, String templateName, String variableName, String variableValue) throws MessagingException {
        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, true);
        helper.setFrom(SEND_EMAIL_FROM);
        helper.setTo(recipientEmail);
        helper.setSubject(subject);
        Context context = new Context();
        context.setVariable(variableName, variableValue);
        String content = templateEngine.process(templateName, context);
        helper.setText(content, true);
        mailSender.send(message);
    }
}

这里使用自定义的 emailExecutor 线程池来异步的发送邮件。@Async 是 Spring 框架提供的一个注解,用于声明方法是异步执行的。通过使用 @Async 注解,您可以告诉 Spring 将标记的方法放在独立的线程中执行,从而实现异步执行的功能。

@EnableAsync
@Configuration
public class ThreadPoolConfig {

    @Bean("emailExecutor")
    public Executor asyncServiceExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(5);
        // 设置最大线程数
        executor.setMaxPoolSize(10);
        // 配置队列大小
        executor.setQueueCapacity(Integer.MAX_VALUE);
        // 设置线程存活时间
        executor.setKeepAliveSeconds(60);
        // 设置默认线程名称前缀
        executor.setThreadNamePrefix("send-email-thread");
        // 等待所有任务结束后在关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //执行初始化
        executor.initialize();
        return executor;
    }
}

请注意,合理配置这些参数需要考虑系统负载、任务类型和性能要求。如果系统负载较大,可以适当增加核心线程数和最大线程数,以提高并发处理能力。如果任务类型是I/O密集型,可以适当调整队列容量,以平衡任务等待和线程切换开销。 最终的配置取决于您的应用需求和硬件资源。您可以根据实际情况进行性能测试和调优,找到最佳的配置组合。想要深度了解线程池相关知识的小伙伴可以了解一下美团动态线程池框架 DynamicTp

2.使用Spring的发布订阅模式解耦邮件逻辑

  1. 首先创建一个邮件类型枚举来扩展邮件类型
public enum EmailType {
    VERIFICATION("验证码邮件", "verification-email"),
    NEWSLETTER("订阅邮件", "newsletter-email"),
    ORDER_CONFIRMATION("订单确认邮件", "order-confirmation-email"),
    NOTIFICATION("通知邮件", "notification-email");

    private final String description;
    private final String templateName;

    EmailType(String description, String templateName) {
        this.description = description;
        this.templateName = templateName;
    }

    public String getDescription() {
        return description;
    }

    public String getTemplateName() {
        return templateName;
    }
}
  1. 接下来创建发送邮件事件
@Getter
public class SendEmailEvent extends ApplicationEvent {
    private final String recipientEmail;
    private final String subject;
    private final String content;
    private final EmailType emailType;

    public SendEmailEvent(Object source, String recipientEmail, String subject, String content, EmailType emailType) {
        super(source);
        this.recipientEmail = recipientEmail;
        this.subject = subject;
        this.content = content;
        this.emailType = emailType;
    }
}
  1. 有了事件我们又该如何消费事件呢,这就要用到我们事件监听器了
@Component
public class SendEmailListener {

    @Resource
    private EmailService emailService;

    @EventListener
    public void sendEmail(SendEmailEvent event) throws MessagingException {
        String recipientEmail = event.getRecipientEmail();
        String content = event.getContent();
        String subject = event.getSubject();
        EmailType emailType = event.getEmailType();
        switch (emailType){
            case VERIFICATION:
                // Load and process verification-email template
                emailService.sendVerificationCode(recipientEmail,content);
                break;
            case NEWSLETTER:
                // Load and process newsletter-email template
                break;
            case ORDER_CONFIRMATION:
                // Load and process order-confirmation-email template
                break;
            case NOTIFICATION:
                // Load and process notification-email template
                emailService.sendNotification(recipientEmail,content,subject);
                break;
            default:
                // Handle unsupported email type
                return;
        }
    }
}
  1. 最终我们是不是要测试我们定义的发送邮件事件呢
@RestController
@Slf4j
public class EmailController {

    @Resource
    private ApplicationEventPublisher eventPublisher;

    @GetMapping("/send-verification")
    public String sendVerificationEmail2(){
        String subject = "登录验证码";
        String content = generateRandomCode();
        String email = "xxxxxxx@qq.com";
        codeCache.put(email, content);
        eventPublisher.publishEvent(new SendEmailEvent(this,email,subject,content, EmailType.VERIFICATION));

        return "Verification code email sent successfully!";
    }

    private static Cache<String, String> codeCache = Caffeine.newBuilder()
            .expireAfterWrite(CACHE_EXPIRATION_MINUTES, TimeUnit.MINUTES)
            .build();

    @GetMapping("/cache-code")
    public String getCacheCode(){
        String email = "xxxxxxx@qq.com";
        return codeCache.getIfPresent(email);
    }

    @GetMapping("/send-notification")
    public String sendNotification(){
        String subject = "note-chat系统通知";
        String content = "请您登录到 note-chat 平台,查看并回复此消息。";
        String email = "xxxxxxxx@qq.com";
        eventPublisher.publishEvent(new SendEmailEvent(this,email,subject,content, EmailType.NOTIFICATION));
        return "Notification-email sent successfully!";
    }
    
    private String generateRandomCode() {
        return RandomUtil.randomNumbers(CODE_LENGTH);
    } 

}

ApplicationEventPublisher 是 Spring 提供的一个接口,用于发布和传播应用程序事件。通过使用这个接口,您可以在应用程序中实现事件驱动的编程,实现不同组件之间的解耦和通信。 这里使用Caffeine简单的缓存一下我们验证码。

<!-- caffeine cache -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

最后为了使用户更直观查看我们邮件内容,使用 Spring Boot 的 Thymeleaf 来格式一下我们发送的邮件 ,这里以验证码邮件为例。此依赖已在springboot项目构建时提供了,调用此模版的代码请查看EmailService 类实现。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>验证码邮件</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f2f2f2;
        }
        .container {
            max-width: 600px;
            margin: 0 auto;
            padding: 20px;
            border-radius: 10px;
            background-color: #fff;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }
        h2 {
            color: #007bff;
            margin-bottom: 15px;
        }
        p {
            font-size: 16px;
            color: #333;
            line-height: 1.6;
            margin-bottom: 10px;
        }
        h3 {
            font-size: 24px;
            color: #007bff;
            font-weight: bold;
            margin-top: 20px;
        }
        .code {
            font-size: 36px;
            color: #007bff;
            font-weight: bold;
        }
        .note {
            font-size: 14px;
            color: #888;
            margin-top: 20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>欢迎使用 <span style="font-weight: bold;">note-chat</span></h2>
        <p>尊敬的用户,您好!</p>
        <p>为了保障您的账户安全,请在以下 <span style="font-weight: bold;">5 分钟</span> 内完成验证:</p>
        <h3>验证码:<span th:text="${verificationCode}" class="code"></span></h3>
        <p>此验证码仅用于本次操作的身份验证目的,请勿泄露给他人。</p>
        <p>感谢您的支持与配合,祝您一切顺利!</p>
        <p class="note"><em>note-chat 团队敬上</em></p>
    </div>
</body>
</html>

三、来看成品吧

1.验证码效果

image.png

2.普通通知邮件效果

image.png