如何在springboot项目中定时发送飞书邮件
项目中总是会接到发送邮件的需求,但是在邮件中完成条件判断和循环数据列表时,如果手动拼写,则会显得非常麻烦。本文主要采用第三方工具pebble模板引擎来渲染我们的html邮件内容。
需求
每天定时早上8.25发送邮件到指定的邮箱中。邮件内容是统计昨天的数据,并计算相应的百分比。
工具
pebble | 模板引擎,渲染html |
---|---|
hutool | 邮件发送工具 |
开发
定时任务
@Scheduled(cron = "0 25 8 * * ?")
public void sendTicketDataAnalyseEmail() throws PebbleException, IOException {
LocalDate yesterday = LocalDate.now().minusDays(1L);
String yesterdayFormat = yesterday.format(dateTimeFormatter);
sendTicketDataAnalyseEmail(yesterdayFormat);
}
利用spring非常简单的实现一个定时发送程序。
模板解析伪代码
// 获取文件路径
ClassPathResource classPathResource = new ClassPathResource("/script/mail.html");
// 初始化引擎
PebbleEngine engine = new PebbleEngine.Builder().autoEscaping(false).build();
// 加载模板
PebbleTemplate pebbleTemplate = engine.getTemplate(classPathResource.getPath());
// 查询模板的数据
List<TicketDataDayAnalyseDto> ticketDataDayAnalyseDtoList = ticketService.dataAnalyse(ticketDataQueryDto);
// 加载模板数据,并执行
Map<String, Object> context = new HashMap<>();
context.put("dataList", ticketDataDayAnalyseDtoList);
context.put("format", yesterdayFormat);
Writer writer = new StringWriter();
pebbleTemplate.evaluate(writer, context);
String output = writer.toString();
// 将渲染成功的结果数据直接发送
MailUtil.send("yudongbin@wuhanpe.com", yesterdayFormat + "-刷脸乘车统计", output, true);
log.info("--------------邮件发送成功--------------");
邮件模板html
这里涉及到pebble中的for标志位使用。使用非常简单,只需要简单的配置即可实现我们需要的功能。
邮件效果
如果用户需要自定一些好看的邮件模板,可以使用163邮箱中的源码模式,先编辑好内容,在转换成源码模式,直接生成我们需要的html。我们在嵌入pebble模板的代码即可。
hutool邮件配置
这个也是非常简单,在resources目录下放置mail.setting即可。内容根据需要自行定义即可。
# 邮件服务器的SMTP地址
host = smtp.feishu.cn
# 邮件服务器的SMTP端口
port = 465
# 发件人(必须正确,否则发送失败)
from = yudongbin@wuhanpe.com
# 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
user = yudongbin@wuhanpe.com
# 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
pass= password
#使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
startttlsEnable = true
# 使用SSL安全连接
sslEnable = true
# 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
socketFactoryClass = javax.net.ssl.SSLSocketFactory
# 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
socketFactoryFallback = true
# 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口456
socketFactoryPort = 465
# SMTP超时时长,单位毫秒,缺省值不超时
#timeout = 0
# Socket连接超时值,单位毫秒,缺省值不超时
#connectionTimeout = 0
飞书邮件密码设置
设置后生成对应的配置地址和密码,贴到我们的mail.setting文件中即可。
这个时候就想到一个问题,现在渲染html和发送邮件是使用两个单独的工具jar包来完成的,其实可以把这两个合二为一。使用我们常用的hutool工具,直接集成pebble来进行使用即可。这样就免去了单独初始化配置的麻烦。
hutool进阶开发
通过查阅官方文档后发现支持如下几款模板引擎。
我们可以通过仿照类似的方式进行集成即可。
下载源码
可以选择下载gitee上面的源码,网速会相对快一些。
编写对应代码
其实hutool框架已经封装的相对完善的代码,我们只需要参照之前已经实现的代码类即可。
模板类
package cn.hutool.extra.template.engine.pebble;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.extra.template.AbstractTemplate;
import cn.hutool.extra.template.TemplateException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Map;
/**
* @author:zooooooooy
*/
public class PebbleTemplate extends AbstractTemplate {
private final com.mitchellbosecke.pebble.template.PebbleTemplate template;
public PebbleTemplate(com.mitchellbosecke.pebble.template.PebbleTemplate template) {
this.template = template;
}
/**
* 渲染对象
* @param bindingMap 绑定的参数,此Map中的参数会替换模板中的变量
* @param writer 输出
*/
@Override
public void render(Map<?, ?> bindingMap, Writer writer) {
final Map<String, Object> map = Convert.convert(new TypeReference<Map<String, Object>>() {}, bindingMap);
try {
this.template.evaluate(writer, map);
} catch (Exception e) {
throw new TemplateException("pebble template parse failed, cause by: ", e);
}
}
/**
* 渲染对象
* @param bindingMap 绑定的参数,此Map中的参数会替换模板中的变量
* @param out 输出
*/
@Override
public void render(Map<?, ?> bindingMap, OutputStream out) {
final Map<String, Object> map = Convert.convert(new TypeReference<Map<String, Object>>() {}, bindingMap);
try {
this.template.evaluate(new OutputStreamWriter(out), map);
} catch (Exception e) {
throw new TemplateException("pebble template parse failed, cause by: ", e);
}
}
}
该类的主要功能就是使用模板渲染相应的模板文件,得到我们想要的结果。
模板引擎类
package cn.hutool.extra.template.engine.pebble;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.template.Template;
import cn.hutool.extra.template.TemplateConfig;
import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.TemplateException;
import com.mitchellbosecke.pebble.PebbleEngine;
import com.mitchellbosecke.pebble.error.PebbleException;
import com.mitchellbosecke.pebble.loader.ClasspathLoader;
import com.mitchellbosecke.pebble.loader.FileLoader;
import com.mitchellbosecke.pebble.loader.StringLoader;
import org.beetl.core.GroupTemplate;
/**
* @author:zooooooooy
*/
public class PebbleTemplateEngine implements TemplateEngine {
private PebbleEngine engine;
public PebbleTemplateEngine() {
}
public PebbleTemplateEngine(TemplateConfig config) {
init(config);
}
@Override
public TemplateEngine init(TemplateConfig config) {
init(createEngine(config));
return this;
}
/**
* 初始化引擎
* @param engine 引擎
*/
private void init(PebbleEngine engine){
this.engine = engine;
}
/**
* 创建引擎
*
* @param config 模板配置
* @return {@link GroupTemplate}
*/
private static PebbleEngine createEngine(TemplateConfig config) {
if (null == config) {
config = TemplateConfig.DEFAULT;
}
PebbleEngine pebbleEngine;
switch (config.getResourceMode()) {
case CLASSPATH:
ClasspathLoader classpathLoader = new ClasspathLoader();
classpathLoader.setPrefix(StrUtil.addSuffixIfNot(config.getPath(), "/"));
pebbleEngine = new PebbleEngine.Builder()
.loader(classpathLoader)
.autoEscaping(false)
.build();
break;
case FILE:
FileLoader fileLoader = new FileLoader();
fileLoader.setPrefix(StrUtil.addSuffixIfNot(config.getPath(), "/"));
pebbleEngine = new PebbleEngine.Builder()
.loader(fileLoader)
.autoEscaping(false)
.build();
break;
case WEB_ROOT:
fileLoader = new FileLoader();
fileLoader.setPrefix(StrUtil.addSuffixIfNot(FileUtil.getAbsolutePath(FileUtil.file(FileUtil.getWebRoot(), config.getPath())), "/"));
pebbleEngine = new PebbleEngine.Builder()
.loader(fileLoader)
.autoEscaping(false)
.build();
break;
case STRING:
StringLoader stringLoader = new StringLoader();
stringLoader.setPrefix(StrUtil.addSuffixIfNot(config.getPath(), "/"));
pebbleEngine = new PebbleEngine.Builder()
.loader(stringLoader)
.autoEscaping(false)
.build();
break;
default:
classpathLoader = new ClasspathLoader();
classpathLoader.setPrefix(StrUtil.addSuffixIfNot(config.getPath(), "/"));
pebbleEngine = new PebbleEngine.Builder()
.loader(classpathLoader)
.autoEscaping(false)
.build();
break;
}
return pebbleEngine;
}
@Override
public Template getTemplate(String resource) {
try {
return new PebbleTemplate(engine.getTemplate(resource));
} catch (PebbleException e) {
throw new TemplateException("pebble template get template from resource failed, cause by: ", e);
}
}
}
该类主要是通过初始化的路径来选择不同的模板引擎,进而找到对应的文件进行渲染。
编写测试类
@Test
public void pebbleEngineTest() {
// 字符串模板
TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("templates").setCustomEngine(PebbleTemplateEngine.class));
Template template = engine.getTemplate("<h3>{{ message }}</h3>");
String result = template.render(Dict.create().set("message", "Hutool"));
Assert.assertEquals("<h3>Hutool</h3>", result);
//ClassPath模板
engine = TemplateUtil.createEngine(new TemplateConfig("templates", ResourceMode.CLASSPATH).setCustomEngine(PebbleTemplateEngine.class));
template = engine.getTemplate("pebble_test.peb");
result = template.render(Dict.create().set("name", "Hutool"));
Assert.assertEquals("hello, Hutool", result);
}
pom引入
<dependency>
<groupId>com.mitchellbosecke</groupId>
<artifactId>pebble</artifactId>
<version>2.3.0</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
这里我们就完成了一个主要功能的编写。进行打包丢入到项目中就可以直接在hutool中使用到自己创建的类了。
同时有一个思考,我可不可以提一个pr到hutool官方中去,有可能会被官方收录到hutool-extra包中。
PR
我们可以按照pr的要求,先fork项目到自己仓库,在制定的分支上修改,并提交。编写测试用例,并完善注释。同时要需要遵守hutool官方对于pr的一些要求规范。
已成功被作者合并到v6版本中。
总结
自此一个完整的邮件发送完结,后续也可以参照上述方式进行复杂的邮件内容发送。
转载自:https://juejin.cn/post/7216299104405471287