使用 FreeMarker 模板引擎实现动态邮件发送(Java 技术栈)

一、核心内容框架

1. 背景与动机

  • 为什么需要模板引擎?
    • 静态邮件内容的局限性(如手动拼接字符串易错)
    • 动态数据绑定需求(用户个性化内容、变量替换)
    • 模板复用性(统一邮件样式,分离内容与逻辑)
  • FreeMarker 的优势
    • 纯 Java 实现,与 Spring 生态无缝集成
    • 强大的模板语法(条件判断、循环、宏定义)
    • 支持 HTML/XML/TEXT 等多种格式输2

2. 技术实现步骤

2.1 核心maven依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.4.5</version>
</parent>
<dependencies>
    <!-- web -->
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- mail -->
    <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>
    <!-- JavaMail SMTP -->
    <dependency>
        <groupId>com.sun.mail</groupId>
        <artifactId>javax.mail</artifactId>
        <version>1.6.2</version>
    </dependency>
   <!-- Redis(用于缓存验证码) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>

2.2 application.yml配置

Spring Boot 邮件服务配置详解(以 QQ 邮箱为例)

spring:
  freemarker:
    # 是否允许请求参数覆盖模板模型中的属性(默认 false 更安全)
    allow-request-override: false
    # 是否允许 Session 属性覆盖模板模型中的属性(默认 false 更安全)
    allow-session-override: false
    # 是否启用模板缓存(生产环境建议 true 提升性能)
    cache: true
    # 设置模板输出使用的字符编码(推荐 UTF-8 避免乱码)
    charset: UTF-8
    # 是否检查模板路径是否存在(true 表示路径不存在会报错)
    check-template-location: true
    # 设置响应内容类型(通常为 text/html)
    content-type: text/html
    # 是否启用 Freemarker 模板引擎(默认 true)
    enabled: true
    # 是否将 HTTP 请求属性暴露给模板(默认 false 更安全)
    expose-request-attributes: false
    # 是否将 Session 属性暴露给模板(默认 false 更安全)
    expose-session-attributes: false
    # 是否暴露 Spring 的宏助手(macro helpers),用于模板中使用 Spring 标签库
    expose-spring-macro-helpers: true
    # 是否优先从文件系统加载模板(开发时设为 true 可动态修改模板)
    prefer-file-system-access: true
    # 设置模板文件的后缀名(Freemarker 默认是 .ftl)
    suffix: .ftl
    # 设置模板文件的存放路径(默认放在 src/main/resources/templates 下)
    template-loader-path: classpath:/templates/
  mail:
    host: smtp.qq.com
    port: 465
    username: 填写发送邮件的邮箱
    password: 填写授权码   # 授权码,不是QQ密码!
    protocol: smtp
    properties:
      mail:
        smtp:
          auth: true
          ssl:
            enable: true
server:
  port: 1481

授权码获取 :需在 QQ 邮箱 → 设置 → 账户 → "开启 POP3/SMTP 服务" 处生成 

多邮箱服务商适配指南

服务商stmp主机端口(SSL/TLS)安全配置要求

QQ 邮箱

smtp.qq.com

465 / 587

必须使用授权码

163 邮箱

smtp.163.com

465 / 994

需开通 IMAP/SMTP 服务

Gmail

smtp.gmail.com

465 / 587

需启用两步验证并生成应用专用密码

企业邮箱

mail.yourdomain.com

25/465/587

根据服务商文档配置

    2.3 .ftl模板编写

    跨平台响应式邮件模板设计(PC & 移动端通用)

    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>ConnectSphere 邮件通知</title>
        <style>
            body, table, td, a {
                -webkit-text-size-adjust: 100%;
                -ms-text-size-adjust: 100%;
                margin: 0;
                padding: 0;
                font-size: 100%;
                font-family: Arial, sans-serif;
                vertical-align: baseline;
            }
    
            table {
                border-collapse: collapse;
                mso-table-lspace: 0pt;
                mso-table-rspace: 0pt;
            }
    
            img {
                border: 0;
                height: auto;
                line-height: 100%;
                outline: none;
                text-decoration: none;
            }
    
            .container {
                max-width: 600px;
                margin: 0 auto;
                background-color: #ffffff;
                border-radius: 8px;
                box-shadow: 0 2px 8px rgba(0,0,0,0.05);
                padding: 30px;
                font-family: Arial, sans-serif;
                color: #333333;
            }
    
            .header {
                font-size: 24px;
                font-weight: bold;
                color: #2d3748;
                padding-bottom: 10px;
                border-bottom: 2px solid #e2e8f0;
            }
    
            .info {
                margin: 18px 0;
            }
    
            .label {
                font-weight: bold;
                color: #4a5568;
            }
    
            .content-box {
                margin-top: 10px;
                padding: 15px;
                background-color: #f1f5f9;
                border-left: 4px solid #4f46e5;
                border-radius: 4px;
            }
    
            .footer {
                margin-top: 20px;
                font-size: 14px;
                color: #718096;
            }
    
            a {
                color: #3182ce;
                text-decoration: underline;
            }
    
            .signature {
                margin-top: 25px;
                font-style: italic;
                color: #4a5568;
            }
    
            .security-warning {
                margin-top: 20px;
                padding: 15px;
                background-color: #fef9c3;
                border-left: 4px solid #facc15;
                border-radius: 4px;
                font-size: 14px;
                color: #854d0e;
            }
    
            .disclaimer {
                margin-top: 30px;
                padding-top: 20px;
                font-size: 12px;
                color: #888888;
                border-top: 1px solid #e2e8f0;
            }
    
            @media only screen and (max-width: 600px) {
                .container {
                    width: 100% !important;
                    padding: 20px;
                }
            }
        </style>
    </head>
    <body style="margin:0; padding:0; background-color:#f7f9fc;">
    <center>
        <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color:#f7f9fc;">
            <tr>
                <td align="center" valign="top" style="padding: 20px 10px;">
                    <table class="container" align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="background-color:#ffffff; border-radius:8px; box-shadow:0 2px 8px rgba(0,0,0,0.05);">
                        <tr>
                            <td style="padding: 30px;">
                                <!-- Header -->
                                <div class="header">尊敬的用户:</div>
    
                                <!-- Body -->
                                <p style="margin-top: 15px;">您好!</p>
                                <p>感谢您使用 <strong>ConnectSphere</strong>。我们收到了您的操作请求,以下是相关信息说明:</p>
    
                                <div class="info"><span class="label">【邮件类型】:</span> ${type}</div>
                                <div class="info"><span class="label">【操作时间】:</span> ${time}</div>
                                <div class="info"><span class="label">【关联账号】:</span> ${email}</div>
                                <div class="content-box">
                                    【操作内容】:${content}
                                </div>
    
                                <p class="footer">
                                    如该操作非您本人所为,请立即通过以下方式联系我们:<br>
                                    📞 客服邮箱:<a href="mailto:support@connectsphere.com">support@connectsphere.com</a><br>
                                    🌐 官方网站:<a href="https://www.connectsphere.com " target="_blank">https://www.connectsphere.com </a>
                                </p>
    
                                <div class="security-warning">
                                    ⚠️ 温馨提示:为了保障账户安全,请勿将此邮件内容透露给他人。建议定期修改密码,并开启双重验证功能。
                                </div>
    
                                <p class="signature">
                                    此邮件由系统自动发送,请勿回复。<br>
                                    ConnectSphere 团队 祝您使用愉快!
                                </p>
    
                                <div class="disclaimer">
                                    <p><strong>免责声明:</strong>本邮件由 ConnectSphere 系统自动发送,仅供指定收件人参考。我们不对因使用或依赖本邮件内容而产生的任何损失或损害承担责任。</p>
                                    <p><strong>版权声明:</strong>© ${year} ConnectSphere. 保留所有权利。未经授权禁止转载、复制或用于其他商业用途。</p>
                                </div>
                            </td>
                        </tr>
                    </table>
                </td>
            </tr>
        </table>
    </center>
    </body>
    </html>

    模板创建完毕,将其放在src/main/resources/templates目录下(我这里是为了区别模板配置,放在了email)

    2.4 代码编写

    以邮件发送验证码为例

    2.4.1 EmailController编写
    package com.demo.controller;
    
    import com.demo.service.CodeUtil;
    import com.demo.service.EmailService;
    import com.demo.service.VerificationCodeCache;
    import lombok.RequiredArgsConstructor;
    import org.springframework.web.bind.annotation.*;
    
    @RestController
    @RequestMapping("/api/email")
    @RequiredArgsConstructor
    public class EmailController {
    
        private final EmailService emailService;
        private final VerificationCodeCache codeCache;
    
    
    
        // 发送验证码
        @GetMapping("/send-code")
        public String sendVerificationCode(@RequestParam String email) {
            String code = CodeUtil.generateCode();
            emailService.sendVerificationCode(email, code);
            codeCache.saveCode(email, code);
            return "验证码已发送,请查收邮箱";
        }
    
        // 校验验证码
        @PostMapping("/verify-code")
        public boolean verifyCode(@RequestParam String email, @RequestParam String inputCode){
            String storedCode = codeCache.getCode(email);
            if (storedCode == null) {
                return false; // 验证码不存在或已过期
            }
            boolean result = storedCode.equals(inputCode);
            if (result) {
                codeCache.removeCode(email); // 验证成功后删除验证码
            }
            return result;
        }
    
    }
    2.4.2 EmailService编写

    这里主要是学习,没有按规范编写架构。

    package com.demo.service;
    
    
    import jakarta.mail.internet.MimeMessage;
    import lombok.RequiredArgsConstructor;
    import lombok.SneakyThrows;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.mail.javamail.JavaMailSender;
    import org.springframework.mail.javamail.MimeMessageHelper;
    import org.springframework.stereotype.Service;
    
    import java.text.SimpleDateFormat;
    import java.time.LocalDateTime;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    @Service
    @RequiredArgsConstructor
    public class EmailService {
        private final JavaMailSender javaMailSender;
        private final EmailTemplateService emailTemplateService;
        @Value("${spring.mail.username}")
        private String from; // 发件人邮箱
    
        @SneakyThrows
        public void sendVerificationCode(String to, String code) {
            //TODO 通过文本发送邮件
    //        SimpleMailMessage message = new SimpleMailMessage();
    //        message.setFrom(from);
    //        message.setTo(to);
    //        message.setSubject("ConnectSphere");
    //        Map<String,Object> map = new HashMap<>();
    //        map.put("content", code);
    //        map.put("email", to);
    //        map.put("type", "验证码");
    //        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    //        map.put("time", sf.format(new Date()));
    //        map.put("year", LocalDateTime.now().getYear());
    //        String text = emailTemplateService.getRenderedTemplate("email/email.ftl", map);
    //        message.setText(text);
    //        javaMailSender.send(message);
            //TODO 通过HTML发送邮件
            MimeMessage message = javaMailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
            helper.setFrom(from);
            helper.setTo(to);
            helper.setSubject("ConnectSphere");
            Map<String,Object> map = new HashMap<>();
            map.put("content", code + " 该验证码五分钟内有效");
            map.put("email", to);
            map.put("type", "验证码");
            SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            map.put("time", sf.format(new Date()));
            map.put("year", LocalDateTime.now().getYear());
            String text = emailTemplateService.getRenderedTemplate("email/email.ftl", map);
            helper.setText(text,true);
            javaMailSender.send(message);
        }
    }

    注意:这里的map的key值要与模板文件中的EL表达式对应上

    String text = emailTemplateService.getRenderedTemplate("email/email.ftl", map);

    该行代码通过 FreeMarker 模板引擎,将 map 中的数据动态填充到 email/email.ftl 模板文件中,最终生成一个完整的 HTML 或文本字符串内容,常用于构建个性化邮件正文。

    2.4.3 EmailTemplateService
    package com.demo.service;
    
    import freemarker.template.Configuration;
    import freemarker.template.Template;
    import freemarker.template.TemplateException;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Service;
    
    import java.io.IOException;
    import java.io.StringWriter;
    import java.util.Map;
    
    @Service
    @RequiredArgsConstructor
    public class EmailTemplateService {
    
        private final Configuration freemarkerConfig;
    
    
        public String getRenderedTemplate(String templateName, Map<String, Object> model) throws IOException, TemplateException {
            Template template = freemarkerConfig.getTemplate(templateName);
            StringWriter writer = new StringWriter();
            template.process(model, writer);
            return writer.toString();
        }
    }

    该方法用于将指定名称的 FreeMarker 模板进行渲染处理。通过传入包含动态数据的 Map 对象(即数据模型),替换模板中的表达式(如 ${variable}),最终生成完整的 HTML 或文本内容并以字符串形式返回。

    2.4.4 VerificationCodeCache
    package com.demo.service;
    import lombok.RequiredArgsConstructor;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Service;
    
    import java.util.concurrent.TimeUnit;
    
    @Service
    @RequiredArgsConstructor
    public class VerificationCodeCache {
    
        private final StringRedisTemplate redisTemplate;
    
    
        // 存储验证码(key: email,value: code)
        public void saveCode(String email, String code) {
            redisTemplate.opsForValue().set(email, code, 5, TimeUnit.MINUTES);
        }
    
        // 获取验证码
        public String getCode(String email) {
            return redisTemplate.opsForValue().get(email);
        }
    
        // 删除验证码
        public void removeCode(String email) {
            redisTemplate.delete(email);
        }
    }
    2.4.5 CodeUtil工具类
    package com.demo.service;
    import java.util.Random;
    
    public class CodeUtil {
        // 生成6位随机数字验证码
        public static String generateCode() {
            return String.format("%06d", new Random().nextInt(999999));
        }
    }
    2.4.6 启动类编写
    package com.demo;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class DemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    
    }
    

    3. 通过ApiFox测试调用

    http://localhost:1481/api/email/send-code?email=3477********@qq.com

    将路径中的邮件改为您要发送的邮箱

    查看收件箱,可以看到邮件已经成功发送了。

    PC端

    手机端

    二、结语

    作为一名刚入行的程序员,这是我第一次完整地记录并分享一个技术功能的实现过程。通过这篇博客,我对 FreeMarker 模板引擎和 SMTP 邮件发送有了更深入的理解。

    技术学习的路上,输出是最好的输入。希望这篇博客能对你有所帮助,也欢迎留言交流,一起成长!

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值