JavaMail结合Freemarker生成邮件HTML模板

一、前言
在HTML模板方面,当时在考虑Thymeleaf和Freemarker选用两个组件之一,最后还是选用freemarker;并不是说thymeleaf差,而是就个人而言觉得freemarker上手还是比较快,而且官方文档资料齐全;同样也考虑可能模板后续的灵活变动,才选用freemarker

本次分享针对springboot下结合email和HTML模板实现

二、实现步骤

  1. 首先在springboot工程pom.xml文件下引入依赖组件
//email组件启动器
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>
//freemarker组件启动器
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
  1. application.yml进行邮件配置,关于邮件开启SMTP服务的资料网上很多,可按照步骤开启即可
spring:
  mail:
    host: smtp.qq.com
    username: xxxxxx@qq.com
    password: cjmoshsbxmntcchi  //邮件授权码
    properties.mail.smtp.starttls.enable: true
    properties.mail.smtp.starttls.required: true
    properties.mail.smtp.ssl.enable: true
    from: xxxx@qq.com  //邮件发送人
  1. 然后先着手邮件功能封装MailService接口及MailServiceImpl接口实现(当然你也可以先进行模板部分编写,个人习惯)
    在这里插入图片描述
  2. MailService代码部分
public interface MailService {
    /**
     * 发送普通文本邮件
     * @param to 收件人
     * @param subject   主题
     * @param content   内容
     */
    void sendSimpleMail(String to, String subject, String content);

    /**
     * 发送HTML邮件
     * @param to    收件人
     * @param subject   主题
     * @param content   内容
     */
    void sendHtmlMail(List<String> to, String subject, String content);

    /**
     * 发送带附件的邮件
     * @param to    收件人
     * @param subject   主题
     * @param filePath  内容
     */
    void sendAttachmentsMail(String to, String subject,String content, String filePath);

}
  1. MailServiceImpl代码部分
@Service
public class MailServiceImpl implements MailService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private JavaMailSender mailSender; //发送邮件功能

    @Value("${spring.mail.from}")
    private String from;

    /**
     * 发送普通文本邮件
     *
     * @param to      收件人
     * @param subject 主题
     * @param content 内容
     */
    public void sendSimpleMail(String to, String subject, String content) {
        SimpleMailMessage message = new SimpleMailMessage();

        //邮件发送人
        message.setFrom(from);
        //邮件接收人
        message.setTo(to);
        //邮件主题
        message.setSubject(subject);
        //邮件内容
        message.setText(content);
        //发送邮件
        mailSender.send(message);
    }

    /**
     * 发送HTML邮件
     *
     * @param to      收件人
     * @param subject 主题
     * @param content 内容
     */
    public void sendHtmlMail(List<String> to, String subject, String content) {
        //获取MimeMessage对象
        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper messageHelper;
        try {
            messageHelper = new MimeMessageHelper(message, true);
            //邮件发送人
            messageHelper.setFrom(from);
            //邮件接收人多人
            String[] obj =  to.toArray(new String[to.size()]);
            messageHelper.setTo(obj);
            //邮件主题
            messageHelper.setSubject(subject);
            //邮件内容
            messageHelper.setText(content, true);
            //发送邮件
            mailSender.send(message);
            //日志输出
            logger.info("邮件发送成功");
        } catch (MessagingException e) {
            logger.error("邮件发送异常!", e);
        }
    }

    /**
     * 发送带附件的邮件
     *
     * @param to       收件人
     * @param subject  主题
     * @param filePath 内容
     */
    public void sendAttachmentsMail(String to, String subject, String content, String filePath) {
        MimeMessage message = mailSender.createMimeMessage();
        try {
            MimeMessageHelper helper = new MimeMessageHelper(message, true);
            helper.setFrom(from);
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(content, true);

            FileSystemResource file = new FileSystemResource(new File(filePath));
            String fileName = filePath.substring(filePath.lastIndexOf(File.separator));
            helper.addAttachment(fileName, file);
            mailSender.send(message);
            //日志信息
            logger.info("邮件已经发送。");
        } catch (MessagingException e) {
            logger.error("发送邮件时发生异常!", e);
        }
    }
}
  1. 现在邮件部分已经基本完成可以着手freemarker模板部分了,首先对于没有freemarker经验的人员建议参考官方中文文档(非常实用,能够快速入手)

地址:http://freemarker.foofun.cn/

  1. application.yml配置freemarker
#配置freemarker
spring:
  freemarker:
    suffix: .ftl   //模板文件后缀
    content-type: text/html
    charset: utf-8
    cache: false
    template-loader-path: classpath:/templates/freemarker/     //模板存放处
    check-template-location: true
  1. 接下来开始定义HTML模板,freemarker模板统一用ftl文件后缀命名
    在这里插入图片描述

由于邮件客户端处于安全机制的考虑是禁止在邮件HTML中嵌入JS以及部分CSS功能的,因为防止一些前端JS恶意XSS攻击,对于邮件客户端也不太好维护

  1. index.ftl内容,建议用HTML表格定义的标签来书写内容,对于大多数邮件客户端都支持且没有问题
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
      xmlns:o="urn:schemas-microsoft-com:office:office">

<head>
    <!-- NAME: NEW COLLECTION -->
    <!--[if gte mso 15]>
    <xml>
        <o:OfficeDocumentSettings>
            <o:AllowPNG/>
            <o:PixelsPerInch>96</o:PixelsPerInch>
        </o:OfficeDocumentSettings>
    </xml>
    <![endif]-->
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>*|MC:SUBJECT|*</title>

    <style type="text/css">
        p {
            margin: 10px 0;
            padding: 0;
        }

        table {
            border-collapse: collapse;
            border: 1px solid #fffef9;
        }

        th {
            font-weight: normal;
        }


        #bodyTable {
            /*@editable*/
            background-color: #E6A56A;
            /*@editable*/
            background-image: url(http://101.200.152.183:8080/static/bg.jpg);
            /*@editable*/
            background-repeat: no-repeat;
            /*@editable*/
            background-position: center;
            /*@editable*/
            background-size: cover;
            /*@editable*/
            border-top: 0;
            /*@editable*/
            border-bottom: 0;
        }
    </style>
</head>

<body>
<h3>这是${taskName},对应的自动化平台测试数据分析结果</h3>
<table align="center" border="0" cellpadding="0" height="100%" width="100%" id="bodyTable" style="color: #fff;">
    <tbody>
    <tr align="center" height="50px" width="100%" style="color: #fff;font-size: 30px">
        <td>
            <a
                    style="font-weight: bold;letter-spacing: normal;line-height: 100%;text-align: center;text-decoration: none;color: #FFFFFF;font-size: 30px;">自动化测试平台
            </a>
        </td>
    </tr>
    <tr style="font-size: 18px;">
        <td>
            <a
                    style="font-weight: bold;letter-spacing: normal;line-height: 100%;text-align: center;text-decoration: none;color: #fdb933;font-size: 18px;margin-left: 70px;">总计分析:
            </a>
        </td>
    </tr>
    <tr height="8px">
    </tr>
    <tr>
        <td>
            <table align="center" border="1px" cellpadding="1" height="90%" width="90%" style="color: #fff;">
                <tr align="center">
                    <th>序号</th>
                    <th>总用例数</th>
                    <th>通过数</th>
                    <th>失败数</th>
                    <th>跳过数</th>
                    <th>通过率</th>
                    <th>失败率</th>
                    <th>跳过率</th>
                    <th>总和</th>
                </tr>
                <tr align="center">
                    <td>1</td>
                    <td>${globalObj.caseSummaryNum}</td>
                    <td>${globalObj.passNum}</td>
                    <td>${globalObj.failNum}</td>
                    <td>${globalObj.blockNum}</td>
                    <td>${globalObj.passRatio}</td>
                    <td>${globalObj.failRatio}</td>
                    <td>${globalObj.blockRatio}</td>
                    <td>100%</td>
                </tr>

            </table>
        </td>
    </tr>

    <tr height="30px">
        <td></td>
    </tr>
    <tr style="font-size: 18px">
        <td>
            <a
                    style="font-weight: bold;letter-spacing: normal;line-height: 100%;text-align: center;text-decoration: none;color: #fdb933;font-size: 18px;margin-left: 70px;">黄金流分析:
            </a>
        </td>
    </tr>
    <tr height="8px">
    </tr>
    <tr>
        <td>
            <table align="center" border="1" cellpadding="0" height="90%" width="90%" style="color: #fff;">
                <tr align="center">
                    <th>序号</th>
                    <th>总用例数</th>
                    <th>通过数</th>
                    <th>失败数</th>
                    <th>跳过数</th>
                    <th>通过率</th>
                    <th>失败率</th>
                    <th>跳过滤</th>
                    <th>总和</th>
                </tr>
                <tr align="center">
                    <td>1</td>
                    <td>${goldObj.caseSummaryNum}</td>
                    <td>${goldObj.passNum}</td>
                    <td>${goldObj.failNum}</td>
                    <td>${goldObj.blockNum}</td>
                    <td>${goldObj.passRatio}</td>
                    <td>${goldObj.failRatio}</td>
                    <td>${goldObj.blockRatio}</td>
                    <td>100%</td>
                </tr>

            </table>
        </td>
    </tr>
    <tr height="30px">
        <td></td>
    </tr>
    <tr style="font-size: 18px;font-weight: bold;font-family: Arial;">
        <td>
            <a
                    style="font-weight: bold;letter-spacing: normal;line-height: 100%;text-align: center;text-decoration: none;color: #fdb933;font-size: 18px;margin-left: 70px;">支付分析:
            </a>
        </td>
    </tr>
    <tr height="8px">
    </tr>
    <tr>
        <td>
            <table align="center" border="1" cellpadding="0" height="90%" width="90%" style="color: #fff;">
                <tr align="center">
                    <th>序号</th>
                    <th>总用例数</th>
                    <th>通过数</th>
                    <th>失败数</th>
                    <th>跳过数</th>
                    <th>通过率</th>
                    <th>失败率</th>
                    <th>跳过滤</th>
                    <th>总和</th>
                </tr>
                <tr align="center">
                    <td>1</td>
                    <td>${payObj.caseSummaryNum}</td>
                    <td>${payObj.passNum}</td>
                    <td>${payObj.failNum}</td>
                    <td>${payObj.blockNum}</td>
                    <td>${payObj.passRatio}</td>
                    <td>${payObj.failRatio}</td>
                    <td>${payObj.blockRatio}</td>
                    <td>100%</td>
                </tr>
            </table>
        </td>
    </tr>
    <tr height="10px">
    </tr>
    <tr align="center" style="font-size: 20px;">
        <td align="center" valign="middle" style="font-family: Arial; font-size: 18px; padding: 20px">
            <table border="0" cellpadding="0" cellspacing="0" class="mcnButtonContentContainer"
                   style="border-collapse: separate !important;border-radius: 3px;background-color: #ED5A2E;">
                <tbody>
                <tr>
                    <td align="center" valign="middle" style="font-family: Arial; font-size: 16px; padding: 10px;">
                        <a class="mcnButton " title="点击进入看板" href="#" target="_blank"
                           style="font-weight: bold;letter-spacing: normal;line-height: 100%;text-align: center;text-decoration: none;color: #FFFFFF;">进入看板</a>
                    </td>
                </tr>
                </tbody>
            </table>
        </td>
    </tr>
    <tr height="10px">
    </tbody>

</table>
</body>

</html>
  1. 要让freemarker模板定义的变量实现参数化效果,可以根据Javabean对象,bean对象必须要含有属性的get方法,或者通过map变量一个一个复制,当然map也可以传递对象,详情参考freemarker官方文档
  2. 接下来对freemarker变量数据及使用基础配置进行了相关封装参考下面FreemarkerConfigUtil 、FreemarkerDataModelUtil工具类
public class FreemarkerDataModelUtil {

    private static final String SUMMARY = "caseSummaryNum";

    private static final String PASS_NUM = "passNum";

    private static final String FAIL_NUM = "failNum";

    private static final String BLOCK_NUM = "blockNum";

    public static Map<String, Object> getDataModels(CaseExcuteSummary dataModels) {
        Map<String, Object> datas = new HashMap<>();
        //初始化数据
        datas.put(SUMMARY, dataModels.getCaseSummary());
        datas.put(PASS_NUM, dataModels.getPassNum());
        datas.put(FAIL_NUM, dataModels.getFailNum());
        datas.put(BLOCK_NUM, dataModels.getBlockNum());

        //pass、fail、block百分率转换
        Integer passNum = dataModels.getPassNum();
        Integer failNum = dataModels.getFailNum();
        Integer summary = dataModels.getCaseSummary();
        DecimalFormat df = new DecimalFormat("0.00");
        String passRatio = df.format((float) passNum / summary * 100) + "%";
        String failRatio = df.format((float) failNum / summary * 100) + "%";
        String p = passRatio.replace("%", "");
        String f = failRatio.replace("%", "");
        String blockRatio = df.format(100 - Float.valueOf(p) - Float.valueOf(f)) + "%";
        //百分率设值
        datas.put("passRatio", passRatio);
        datas.put("failRatio", failRatio);
        datas.put("blockRatio", blockRatio);

        return datas;
    }

}

public class FreemarkerConfigUtil {

    /**
     * 获取HTML模板
     * @param maps
     * @return
     */
    public static String getTemplate(Map<String,Object> maps){
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
        cfg.setClassForTemplateLoading(FreemarkerConfigUtil.class,"/templates/freemarker");
        try {
            Template template = cfg.getTemplate("index.ftl");
            return FreeMarkerTemplateUtils.processTemplateIntoString(template, maps);
        }catch (Exception e){
            e.printStackTrace();
        }
        throw new BizException(ResultCodeEnums.MAIL_SEND_FAIL);
    }

}
  1. 由于freemarker模板数据来源于dao数据库中,所以在此定义下数模及演示数据部分避免读者产生困惑
<!--dao部分选用的是mybatisplus组件-->
 <dependency>
   <groupId>com.baomidou</groupId>
   <artifactId>mybatis-plus-boot-starter</artifactId>
   <version>3.3.1.tmp</version>
  </dependency>
#数模部分
DROP TABLE IF EXISTS `case_excute_summary`;
CREATE TABLE `case_excute_summary` (
  `id` bigint(20) NOT NULL COMMENT 'id',
  `task_id` bigint(20) NOT NULL COMMENT '任务id',
  `task_name` varchar(20) NOT NULL COMMENT '任务名称',
  `case_summary` int(6) NOT NULL COMMENT '总用例数',
  `pass_num` int(6) NOT NULL COMMENT '通过数',
  `fail_num` int(6) NOT NULL COMMENT '失败数',
  `block_num` int(6) NOT NULL COMMENT '跳过数',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `task_id` (`task_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用例执行汇总表';

-- ----------------------------
-- Records of case_excute_summary
-- ----------------------------
INSERT INTO `case_excute_summary` VALUES ('1244796019770351618', '1244796019770351618', '黄金流测试任务', '280', '260', '18', '2', '2020-04-14 19:26:34', '2020-04-14 19:26:37');
INSERT INTO `case_excute_summary` VALUES ('1244796019770351619', '1244796019770351619', '支付测试任务', '270', '260', '5', '5', '2020-04-14 19:26:34', '2020-04-14 19:26:37');
  1. 根据mybatisplus框架定义entity与数据表进行关系映射,然后在定义mapper进行CRUD
@Data
@TableName("case_excute_summary")
public class CaseExcuteSummary implements Serializable {

    private static final long serialVersionUID = 4915869244050353417L;
    /**
     * 汇总表id
     */
    @TableId(value = "id",type = IdType.ID_WORKER)
    private Long id;
    /**
     * 对应任务
     */
    @TableField("task_id")
    private Long taskId;
    /**
     * 任务名称
     */
    @TableField("task_name")
    private String taskName;
    /**
     * 用例总数
     */
    @TableField("case_summary")
    private Integer caseSummary;
    /**
     * 通过数
     */
    @TableField("pass_num")
    private Integer passNum;
    /**
     * 失败数
     */
    @TableField("fail_num")
    private Integer failNum;
    /**
     * 跳过数
     */
    @TableField("block_num")
    private Integer blockNum;
    /**
     * 创建时间
     */
    @TableField("create_time")
    private Date createTime;
    /**
     * 更新时间
     */
    @TableField("update_time")
    private Date updateTime;

}
public interface CaseExcuteSummaryMapper extends BaseMapper<CaseExcuteSummary> {
    /**
     * 用例汇总数据
     * @return
     */
    CaseExcuteSummary caseSummaryDetail();

}
  1. 开始编写测试demo了,在springboot对应的test包下运行测试用例
@SpringBootTest
class DemoApplicationTests {

    @Autowired
    private CaseExcuteSummaryMapper caseExcuteSummaryMapper;

    @Autowired
    private MailService mailService;

    @Test
    void contextLoads() {

        List<String> to = new ArrayList<>();
        to.add("xxxxx@qq.com");
        to.add("xxxx@qq.com");
        CaseExcuteSummary glod = new CaseExcuteSummary();
        CaseExcuteSummary pay = new CaseExcuteSummary();
        HashMap<String, Object> maps = new HashMap<>();
        glod.setId(1244796019770351618L);
        pay.setId(1244796019770351619L);
        CaseExcuteSummary glods = caseExcuteSummaryMapper.selectById(glod);
        CaseExcuteSummary pays = caseExcuteSummaryMapper.selectById(pay);
        CaseExcuteSummary summary = caseExcuteSummaryMapper.caseSummaryDetail();

        Map<String, Object> glodObj = FreemarkerDataModelUtil.getDataModels(glods);
        Map<String, Object> payObj = FreemarkerDataModelUtil.getDataModels(pays);
        Map<String, Object> summaryObj = FreemarkerDataModelUtil.getDataModels(summary);

        maps.put("payObj",payObj);
        maps.put("globalObj",summaryObj);
        maps.put("goldObj",glodObj);
        maps.put("taskName",glods.getTaskName());
        String template = FreemarkerConfigUtil.getTemplate(maps);

        mailService.sendHtmlMail(to,"自动化测试数据分析报告",template);
    }

}

  1. 登录个人QQ邮箱查看接收到的效果展示
    在这里插入图片描述

三、总结
在后端标准化的结合Javamail、freemarker可能还会涉及到其它框架组件,向本文中提到的mybatisplus、若只是简单的练习demo可以忽略这一块;数据部分自己通过map集合去解决就OK

文章仅个人总结不喜勿喷;存在不足之处请指正!感谢阅读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值