【外观模式】SpringBoot集成mail发送邮件

news2024/11/22 22:40:36

前言

发送邮件功能,借鉴 刚果商城,根据文档及项目代码实现。整理总结便有了此文,文章有不对的点,请联系博主指出,请多多点赞收藏,您的支持是我最大的动力~

发送邮件功能主要借助 mail、freemarker以及rocketmq实现。

刚果商城是个分布式项目,近看发送消息模块即可。

image-20231205211959763

标准的DDD分层架构。

RocketMQ部署

方便起见,使用docker部署环境

RocketMQ 4.5.1 安装部署

安装 NameServer

docker run -d -p 9876:9876 --name rmqnamesrv foxiswho/rocketmq:server-4.5.1

安装 Brocker

1)新建配置目录

mkdir -p /mydata/rocketmq/conf

2)新建配置文件 broker.conf

brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
# 此处为本地ip, 如果部署服务器, 需要填写服务器外网ip
brokerIP1 = xx.xx.xx.xx

3)创建容器

docker run -d \
-p 10911:10911 \
-p 10909:10909 \
--name rmqbroker \
--link rmqnamesrv:namesrv \
-v /mydata/rocketmq/conf/broker.conf:/etc/rocketmq/broker.conf \
-e "NAMESRV_ADDR=namesrv:9876" \
-e "JAVA_OPTS=-Duser.home=/opt" \
-e "JAVA_OPT_EXT=-server -Xms512m -Xmx512m" \
foxiswho/rocketmq:broker-4.5.1

安装 rocketmq 控制台

docker pull pangliang/rocketmq-console-ng
docker run -d \
--link rmqnamesrv:namesrv \
-e "JAVA_OPTS=-Drocketmq.config.namesrvAddr=namesrv:9876 -Drocketmq.config.isVIPChannel=false" \
--name rmqconsole \
-p 8088:8080 \
-t pangliang/rocketmq-console-ng

运行成功,稍等几秒启动时间,浏览器输入 ip:8088 查看。

记得放行上述所有端口,最终结果如下:

image-20231205220952290

RocketMQ安装成功~

引入主要依赖

        <!-- 发送邮件主要依赖 -->
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        
		<!-- 模板引擎 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        
		<!-- 消息队列 实现解耦 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
        </dependency>

配置文件

image-20231205212430700

主要看application.yaml 和 application-dev.yaml

application.yaml

server:
  port: 8001

spring:
  profiles:
    active: dev
  application:
    name: message-service
    stream:
      bindings:
        # 主要是如下两个通道的配置 (消费者通道)
        mailSend:
          consumer:
            concurrency: 4
            max-attempts: 1
          content-type: application/json
          destination: message-center_topic
          group: message-center_mail-send_cg
        # 生产者通道   
        messageOutput:
          content-type: application/json
          destination: message-center_topic
          group: message-center_general-send_pg
      rocketmq:
        bindings:
          mailSend:
            consumer:
              delay-level-when-next-consume: -1
              tags: common_message-center_mail-send_tag
      # ...        

application-dev.yaml

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    stream:
      rocketmq:
        binder:
          name-server: 127.0.0.1:9876 # rocketmq服务
  mail:
    default-encoding: UTF-8
    host: smtp.163.com
    password: xxx
    port: 25
    protocol: smtp
    username: xxx@163.com

重要的是mail中参数(用的是网易邮箱),username:网易邮箱账号,password:登录 SMTP server 的密码

登录 SMTP server 密码

password获取步骤如下

一、登录网页版邮箱(https://email.163.com/),进入邮箱首页。

二、点击上方设置,选择POP/SMTP/IMAP选项。

image-20231205213826857

三、在客户端协议界面,选择开启对应的协议,IMAP或者POP3分别为不同的收信协议,选择只开启需要的收信协议,比如IMAP,推荐使用IMAP协议来收发邮件,它可以和网页版完全同步。

image-20231205214131071

四、点击开启,继续开启,手机扫码发送短信后,得到的一串密码即为登录密码

image-20231205214306587

image-20231205214336717

image-20231205214600255

真正代码实现

interfaces层

image-20231205214957319

用户接口层,入参为CQRS风格,参数都在application层

发送邮件入参:

@Data
@ApiModel("邮箱发送")
public class MailSendCommand {
    
    @ApiModelProperty(value = "标题", example = "刚果商城邮箱验证码提醒")
    @NotBlank(message = "邮箱标题不能为空")
    private String title;
    
    @Email
    @ApiModelProperty(value = "发送者", example = "congomall@163.com")
    @NotBlank(message = "邮箱发送者不能为空")
    private String sender;
    
    @Email
    @ApiModelProperty(value = "接收者", example = "7798432@163.com", notes = "实际发送时更改为自己邮箱")
    @NotBlank(message = "邮箱接收者不能为空")
    private String receiver;
    
    @Email
    @ApiModelProperty("抄送者")
    private String cc;
    
    @ApiModelProperty(value = "消息参数")
    private List<String> paramList;
    
    // 与数据库对应
    @ApiModelProperty(value = "模板ID", example = "userRegisterVerification")
    @NotBlank(message = "邮箱模板ID不能为空")
    private String templateId;
}

application层

image-20231205215452481

直接调用到application层Service实现类方法,该层封装好参数直接调用基础层中消息生产者。

domain层

image-20231205215755887

领域层里面主要是一些常量、实体类,接口以及仓储接口具体实现在基础层。

infrastructure层 ☆

image-20231205220340090

image-20231205221308019

消息通道配置

source -> sink

public interface MessageSource {
    
    String OUTPUT = "messageOutput";
    
    @Output(MessageSource.OUTPUT)
    MessageChannel messageOutput();
}
public interface MessageSink {
    
    String MAIL_SEND = "mailSend";
    
    @Input(MessageSink.MAIL_SEND)
    SubscribableChannel mailSend();
}

常量与配置文件中通道名称保持一致

消息生产者

@Slf4j
@Component
@AllArgsConstructor
public class MessageSendProduce {
    
    // 属性名与配置文件中通道名保持一致
    private final MessageChannel messageOutput;
    
    /**
     * 邮箱消息发送
     */
    public void mailMessageSend(MailMessageSendEvent mailMessageSendEvent) {
        String keys = UUID.randomUUID().toString();
        Message<?> message = MessageBuilder
                .withPayload(JSON.toJSONString(mailMessageSendEvent))
                .setHeader(MessageConst.PROPERTY_KEYS, keys)
                .setHeader(MessageConst.PROPERTY_TAGS, MessageRocketMQConstants.MESSAGE_MAIL_SEND_TAG)
                .build();
        long startTime = SystemClock.now();
        boolean sendResult = false;
        try {
            // 发送消息给mq
            sendResult = messageOutput.send(message, 2000L);
        } finally {
            log.info("邮箱消息发送,发送状态: {}, Keys: {}, 执行时间: {} ms, 消息内容: {}", sendResult, keys, SystemClock.now() - startTime, JSON.toJSONString(mailMessageSendEvent));
        }
    }
}

消息消费者

@Slf4j
@Component
@RequiredArgsConstructor
public class MailMessageSendConsume {
    
    private final MessageSendFacade messageSendFacade;
    
    // 幂等性注解,还没研究
    @Idempotent(
            uniqueKeyPrefix = "mail_message_send:",
            key = "#event.messageSendId+'_'+#event.hashCode()",
            type = IdempotentTypeEnum.SPEL,
            scene = IdempotentSceneEnum.MQ,
            keyTimeout = 600L
    )
    @StreamListener(MessageSink.MAIL_SEND)
    public void mailMessageSend(@Payload MailMessageSendEvent event, @Headers Map headers) {
        long startTime = System.currentTimeMillis();
        try {
            MessageSend messageSend = BeanUtil.toBean(event, MessageSend.class);
            // 【外观模式】: 抽象消息发送、消息存储以及失败回调业务方等逻辑
            messageSendFacade.mailMessageSend(messageSend);
        } finally {
            log.info("Keys: {}, Msg id: {}, Execute time: {} ms, Message: {}", headers.get("rocketmq_KEYS"), headers.get("rocketmq_MESSAGE_ID"), System.currentTimeMillis() - startTime,
                    JSON.toJSONString(event));
        }
    }
}

【外观模式】

直接与外观类交互,外观类封装了做某件事的所有操作,无需与一个个子操作一一交互,降低了复杂性,提高了可维护性

以消息发送为例,将发送邮箱以及消息存储和失败回调业务封装为一个方法降低调用处理复杂度。

image-20231205221631822

发送邮箱核心实现类

@Slf4j
@Component
@AllArgsConstructor
public class MailMessageProduceImpl implements ApplicationListener<ApplicationInitializingEvent>, MailMessageProduce {
    
    private final MailTemplateMapper mailTemplateMapper;
    
    private final JavaMailSender javaMailSender;
    
    private final Configuration configuration;
    
    @SneakyThrows
    @Override
    public boolean send(MessageSend messageSend) {
        try {
            // 根据模板id查询模板 模板id:userRegisterVerification
            MailTemplateDO mailTemplateDO = mailTemplateMapper.selectOne(
                    Wrappers.lambdaQuery(MailTemplateDO.class)
                            .eq(MailTemplateDO::getTemplateId, messageSend.getTemplateId()));
            MimeMessage mimeMessage = javaMailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
            helper.setFrom(messageSend.getSender());
            helper.setSubject(messageSend.getTitle());
            if (StrUtil.isNotBlank(messageSend.getCc())) {
                helper.setCc(messageSend.getCc().split(","));
            }
            if (StrUtil.isNotBlank(messageSend.getReceiver())) {
                helper.setTo(messageSend.getReceiver().split(","));
            }
            Map<String, Object> model = Maps.newHashMap();
            // 模板参数名称与下面freemarker模板中参数一一对应
            String[] templateParams = mailTemplateDO.getTemplateParam().split(",");
            if (ArrayUtil.isNotEmpty(templateParams)) {
                for (int i = 0; i < templateParams.length; i++) {
                    model.put(templateParams[i], messageSend.getParamList().get(i));
                }
            }
            // 模板id就是模板名
            String templateKey = messageSend.getTemplateId() + ".ftl";
            // 从单例对象容器获取模板
            Template template = Singleton.get(templateKey, () -> {
                try {
                    return configuration.getTemplate(templateKey);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
            helper.setText(html, true);
            // freemarker填充参数,发送邮箱
            javaMailSender.send(mimeMessage);
        } catch (Throwable ex) {
            log.error("邮件发送失败,Request: {}", JSONUtil.toJsonStr(messageSend), ex);
            return false;
        }
        return true;
    }
    
    /**
     * 初始化邮箱模板 【率先先将所有模板初始化到单例对象容器中】
     */
    @SneakyThrows
    @Override
    public void onApplicationEvent(ApplicationInitializingEvent event) {
        Resource[] resources = new PathMatchingResourcePatternResolver().getResources(ResourceUtils.CLASSPATH_URL_PREFIX + "templates/*.ftl");
        for (Resource resource : resources) {
            String templateName = resource.getFilename();
            Singleton.put(templateName, configuration.getTemplate(templateName));
        }
    }
}

image-20231205222229490

模板具体内容:

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
<div>
    <p style="caret-color: rgb(86, 90, 92); color: rgb(86, 90, 92); font-family: Helvetica, 微软雅黑, 宋体; text-size-adjust: auto; font-size: 20px;">
        亲爱的用户:</p>
    <p style="caret-color: rgb(86, 90, 92); color: rgb(86, 90, 92); font-family: Helvetica, 微软雅黑, 宋体; font-size: 18px; text-size-adjust: auto;">
        您好!感谢您的使用,您本次的验证码为:<span class="Apple-converted-space">&nbsp;</span></p><b
            style="font-family: Helvetica, 微软雅黑, 宋体; text-size-adjust: auto; font-size: 32px; color: rgb(45, 123, 255);">${validCode}</b>
    <p style="caret-color: rgb(86, 90, 92); color: rgb(86, 90, 92); font-family: Helvetica, 微软雅黑, 宋体; text-size-adjust: auto; font-size: 20px;">
        安全提示:</p>
    <p style="caret-color: rgb(86, 90, 92); color: rgb(86, 90, 92); font-family: Helvetica, 微软雅黑, 宋体; font-size: 18px; text-size-adjust: auto;">
        为保障您的帐户安全,请在 5 分钟内完成验证,否则验证码将自动失效。<span class="Apple-converted-space">&nbsp;</span></p>
</div>
<div>
    <includetail><!--<![endif]--></includetail>
</div>
</body>
</html>

image-20231205222432490

最终实现效果

测试结果如下:

收件为QQ邮箱:

image-20231205210059105

收件为谷歌邮箱:

image-20231205225618465

经我测试发现,配置的是网易邮箱,发送者就只能是网易邮箱,接收者可以是任意邮箱

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1297599.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

包装类, 泛型---java

目录 一. 包装类 1.1 基本数据类型和对应的包装类 1.2 装箱和拆箱 二. 泛型 2.1什么是泛型 2.2泛型的引入 2.3 泛型类语法 2.4 泛型类的使用 2.5 裸类型(Raw Type)(了解) 2.6 泛型是如何编译的 2.7 泛型的上界 2.8 泛型方法 一. 包装类 在 Java 中&#xff0c;由于基本…

爱智EdgerOS之深入解析AI图像引擎如何实现AI视觉开发

一、前言 AI 视觉是为了让计算机利用摄像机来替代人眼对目标进行识别&#xff0c;跟踪并进一步完成一些更加复杂的图像处理。这一领域的学术研究已经存在了很长时间&#xff0c;但直到 20 世纪 70 年代后期&#xff0c;当计算机的性能提高到足以处理图片这样大规模的数据时&am…

DSP处理器及其体系结构特点(您都用过哪些DSP?)

DSP处理器概述 数字信号处理器&#xff08;Digital Signal Processor&#xff0c;DSP&#xff09;是一种专门设计用于执行数字信号处理任务的微处理器类型。与通用微处理器&#xff08;如CPU&#xff09;相比&#xff0c;DSP处理器在处理数字信号时具有更高的性能和效率。 用途…

JAVA程序如何打jar和war问题解决

背景: 近期研究一个代码审计工具 需要jar包 jar太多了 可以将jar 打成war包 首先看下程序目录结构 pom.xml文件内容 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"ht…

数据库后门是什么?我们要如何预防它的危害

数据库后门是黑客在数据库中安装的一种特殊程序或代码&#xff0c;可以绕过正常的认证和授权机制&#xff0c;从而获取数据库的敏感信息或者控制整个数据库。黑客可以通过各种方式安装后门&#xff0c;比如利用漏洞、钓鱼、社会工程学等。 数据库后门的危害主要体现在以下几个方…

GPTs应用:创新无限,生态扩容

今天分享的GPTs系列深度研究报告&#xff1a;《GPTs应用&#xff1a;创新无限&#xff0c;生态扩容》。 &#xff08;报告出品方&#xff1a;华泰证券&#xff09; 报告共计&#xff1a;20页 GPTs 发展现状&#xff1a;从 AI 工具到开发平台&#xff0c;掀起全民开发浪潮 11…

YOLOv5独家原创改进:SPPF自研创新 | 可变形大核注意力(D-LKA Attention),大卷积核提升不同特征感受野的注意力机制

💡💡💡本文自研创新改进: 可变形大核注意力(D-LKA Attention)高效结合SPPF进行二次创新,大卷积核提升不同特征感受野的注意力机制。 收录 YOLOv5原创自研 https://blog.csdn.net/m0_63774211/category_12511931.html 💡💡💡全网独家首发创新(原创),适合p…

Docker三 | 数据卷

目录 Docker数据卷简介 添加数据卷的命令 容器数据卷的继承 Docker数据卷简介 Docker容器产生的数据&#xff0c;如果不备份&#xff0c;当容器实例删除后&#xff0c;容器中的数据也会消失&#xff0c;为了保存数据可以在Docker中使用数据卷。Docker数据卷是宿主机的一个可以…

【LeetCode】2703. 返回传递的参数的长度

返回传递的参数的长度 题目题解 题目 请你编写一个函数 argumentsLength&#xff0c;返回传递给该函数的参数数量。 示例 1&#xff1a; 输入&#xff1a;args [5] 输出&#xff1a;1 解释&#xff1a; argumentsLength(5); // 1只传递了一个值给函数&#xff0c;因此它应返…

搞懂内存函数

引言 本文介绍memcpy的使用和模拟实现、memmove的使用和模拟实现、memcmp使用、memset使用 ✨ 猪巴戒&#xff1a;个人主页✨ 所属专栏&#xff1a;《C语言进阶》 &#x1f388;跟着猪巴戒&#xff0c;一起学习C语言&#x1f388; 目录 引言 memcpy memcpy的使用 memcpy的…

PyTorch: 基于VGG16处理MNIST数据集的图像分类任务

引言 在本博客中&#xff0c;小编将向大家介绍如何使用VGG16处理MNIST数据集的图像分类任务。MNIST数据集是一个常用的手写数字分类数据集&#xff0c;包含60,000个训练样本和10,000个测试样本。我们将使用Python编程语言和PyTorch深度学习框架来实现这个任务。 在Conda虚拟环…

鸿蒙开发组件之Image

Image组件加载图片方式有三种&#xff1a; 1、网络地址加载 直接Image(xxxx),添加上图片的网络地址就可以了。注意&#xff1a;真机、模拟题调试需要申请"ohos.permission.INTERNET"权限 Image(https://xxxxxxx) 2、PixelMap格式加载像素图 Image(PixelMapObjec…

根据年份和第几周来获取,那一个周的周天日期

在工作中遇到这个问题&#xff0c;仓库有物料录入&#xff0c;告诉了年份和这个年的第几周&#xff0c;要求把时间转换为XXXX-XX-XX的格式。日期为那个周的最后一天&#xff08;周天&#xff09; 在Java中想要获取特定年份和周数的周天日期&#xff0c;可以使用LocalDate类 pu…

【SpringBoot】响应

controller方法中的return的结果&#xff0c;使用ResponseBody注解&#xff08;方法注解或类注解&#xff09;响应给服务器。 RestController Controller ResponseBody 类上有RestController注解或ResponseBody注解时&#xff1a;表示当前类下所有的方法返回值做为响应数据…

四招打造完美分层自动化测试框架,让测试更高效!

写在前面 我们刚开始做自动化测试&#xff0c;可能写的代码都是基于原生写的代码&#xff0c;看起来特别不美观&#xff0c;而且感觉特别生硬。 来看下面一段代码&#xff1a; 具体表现如下&#xff1a; driver对象在测试类中显示 定位元素的value值在测试类中显示 定位元素…

数据表排序

指针用的有点少了&#xff0c;有点不适应 用的冒泡排序 代码如下&#xff1a; #include<stdio.h> int num[100][100]; int * p[100], jud[100]; int judge(int i, int j, int rank); int m, n, k;int main(void) {scanf("%d%d%d", &m, &n, &k);f…

字符串函数strtok

1.调用格式&#xff1a; 2.调用形式&#xff1a;char*strtok(char*p1,const char*p2),其中第二个是由分隔符组成的字符串&#xff0c;第一个为需要分隔的字符串 3.调用目的&#xff1a;将分隔符之间的字符串取出 4.调用时一般将源字符串拷贝后调用&#xff0c;因为此函数会将…

C++11原子操作atomic

文章目录 原子操作atomic原子操作的相关函数原子操作的特点“平凡的”与“合格的” 原子操作atomic 前面我们介绍了互斥锁等一系列多线程相关操作&#xff0c;这里我们来说下原子操作atomic。 可以理解为原子变量就是将上面的操作进行了整合的一个全新变量&#xff0c;但是实际…

sensitive word 敏感词(脏词) 如何忽略无意义的字符?达到更好的过滤效果?

忽略字符 说明 我们的敏感词一般都是比较连续的&#xff0c;比如 傻帽 那就有大聪明发现&#xff0c;可以在中间加一些字符&#xff0c;比如【傻!#$帽】跳过检测&#xff0c;但是骂人等攻击力不减。 那么&#xff0c;如何应对这些类似的场景呢&#xff1f; 我们可以指定特…

带大家做一个,易上手的家常可乐鸡翅

将鸡翅从冰箱中拿出 泡水解冻 这里 我用的二十个 将葱切段 切一些蒜片 有姜也可以切一些小片下来 这里 家里没姜了 六根干辣椒 一把花椒 等鸡翅化开之后 清洗干净 然后 如下图 中间位置切两刀 方便入味 起锅烧油 然后 下鸡翅 干辣椒 花椒 先翻炒一下 这里不需要放水 鸡翅会…