文章目录
SpringBoot使用Velocity发送电子邮件
简介
Velocity能用来干什么?
我认为大家熟知的什么用Velocity页面当做MVC的页面来呈现这种方式,但在当今前后端分离的时代,我觉得很大一部分公司前端页面都是使用的React,Vue等纯前端框架。使用各大模板引擎当做页面应该很少
除了这个,官方文档提到了一个很重要的一个应用场景,就是发送电子邮件。当应用需要给用户发送电子邮件时,并且有统一的邮件模板,只需要针对不同用户展现不同的内容。这个时候,页面需要同Java代码分离,并且页面的模板是不需要经常改变的,这就用到了Velocity
入门
Velocity工作原理简单概括为,在页面中使用Velocity的语法设置变量,取值。而Java代码负责设值、同时渲染模板
页面
页面主要分为以下两大类
引用
看引用相关文档即可知道,这里只做简单记录
-
命名规则
引用由$+变量名/方法名组成。必须以字母开头,后面的部分可以是字母、数字、下划线_
-
访问属性和方法
通过命名+ 点的方式
## 访问order的customerName $order.customerName
关于是访问属性还是访问方法,参考查找规则
-
引用的正式写法
当变量后面跟着其他单词时,Velocity可能会把后面的单词也当做变量去查找,为了防止这种情况发生,需要使用正式写法
Jack is a ${vice}maniac
这里只会去查找vice
指令
指令以#开头,常用的主要有以下几个
-
set,设值
#set($temList = $order.items)
将items设值给定义的变量temList
-
If / ElseIf / Else,判断
#if( $foo < 10 ) Go North #elseif( $foo == 10 ) Go East #elseif( $bar == 6 ) Go South #else Go West #end
foo可以是对象、集合、数组等,判断条件为
-
foreah,循环
#set($temList = $order.items) #foreach($item in $temList) $item #end
-
include和parse,重用其他页面
<html> ## 这里要写resources下的全路径 #parse("templates/emails/header.vm") <body> </body> </html>
关于其他使用,如转义、数学计算等,移步相关文档
Java代码
页面写完之后,我们需要给页面中的变量设值,开发文档
新建一个springboot应用
可以使用Velocity和VelocityEngine,看个人喜好,主要使用Context进行变量的设置
示例页面
<html>
#parse("templates/emails/header.vm")
<body>
Hi, $order.customerName<br>
## 使用正式语法
您在${order.paymentTime}时完成的订单详情如下
<table border="1">
<tr>
<th>宝贝</th>
</tr>
#set($temList = $order.items)
#foreach($item in $temList)
<tr>
<td>$item</td>
</tr>
#end
</table>
<br>
合计金额为:$order.paymentAmount<br>
配送方式为:$order.deliveryMethod
</body>
</html>
给上述页面设值
@Service
@Log
public class SendEmailService {
private static final VelocityEngine ve = new VelocityEngine();
/**
* https://velocity.apache.org/engine/2.3/configuration.html#resource-management
*
* https://velocity.apache.org/engine/2.3/configuration.html#configuration-examples
* 注意最后一段话
* Node that the three names 'file', 'class', and 'jar' are merely for your convenience and sanity.
* They can be anything you want - they are just used to associate a set of properties together.
* However, it is recommended that you use names that give some hint of the function
*
* 说明可以将file,class,jar的位置替换成其他,所以下面的写法是resource.loader.class.class,这里使用class
*/
@PostConstruct
public void initVelocity() {
ve.setProperty(RuntimeConstants.RESOURCE_LOADERS, "class");
ve.setProperty("resource.loader.class.class", ClasspathResourceLoader.class.getName());
ve.setProperty("resource.loader.class.cache", true);
ve.init();
}
public boolean sendOrderDetailEmail() {
Order order = new Order();
order.setCustomerName("jack");
List<String> items = Arrays.asList("猪肉", "牛肉", "鱼肉");
order.setItems(items);
order.setPaymentAmount(BigDecimal.valueOf(78.365));
order.setPaymentTime(LocalDateTime.now());
order.setDeliveryMethod("顺丰");
// 设值
VelocityContext context = new VelocityContext();
context.put("order", order);
context.put("header", "OrderDetail");
// 获取模板
Template template = ve.getTemplate(SendEmailUtil.obtainTemplateRealPath("orderDetail"));
StringWriter writer = new StringWriter();
// 将模板和变量值进行渲染
template.merge(context, writer);
return true;
}
}
注意 :这里主要注意Velocity时读取模板位置的设置,ResourceLoader文档
什么时候需要设置ResourceLoader?
- 通过velocityEngine.mergeTemplate或者template.merge 的方式,需要在初始化的时候配置ResourceLoader.
- 如果.vm文件中含有#include或#parse指令,也需要配置ResourceLoader
/**
* 两种方式:https://velocity.apache.org/engine/devel/developer-guide.html#using-velocity
* 一种是直接使用字符串 + velocityEngine.evaluate
* 另外一种是使用模板文件 + velocityEngine.mergeTemplate, 这种方式需要在初始化的时候配置resourceLoader
*
* velocity配置: https://velocity.apache.org/engine/devel/configuration.html#configuring-velocity
* 默认配置文件位置:org/apache/velocity/runtime/defaults/velocity.properties
* Any values specified before init() time will replace the default values.
* Therefore, you only have to configure velocity with the values for the keys that you need to change,
* and not worry about the rest
*/
@Log
@Component
public class VelocityUsageService {
/**
* 字符串中含有#parse或#include指令,需要配置resourceLoader
* @return
*/
public String velocityEvaluateHasParseOrIncludeDirective() {
StringWriter writer = new StringWriter();
String template = "<html>\n" + "## 这里要写resources下的全路径\n" + " #parse(\"templates/emails/header.vm\")\n" + "<body>\n" + "Hi, $order.customerName<br>\n" + "## 使用正式语法\n" +
"您在${order.paymentTime}时完成的订单详情如下\n" + "<table border=\"1\">\n" + " <tr>\n" + " <th>宝贝</th>\n" + " </tr>\n" + " #set($temList = $order.items)\n" +
" #foreach($item in $temList)\n" + " <tr>\n" + " <td>$item</td>\n" + " </tr>\n" + " #end\n" + "</table>\n" + "<br>\n" + "合计金额为:$order.paymentAmount<br>\n" +
"配送方式为:$order.deliveryMethod\n" + "</body>\n" + "</html>";
VelocityEngine velocityEngine = new VelocityEngine();
velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADERS, "class");
velocityEngine.setProperty("resource.loader.class.class", ClasspathResourceLoader.class.getName());
velocityEngine.setProperty("resource.loader.class.cache", true);
VelocityContext velocityContext = new VelocityContext();
Order order = new Order();
order.setCustomerName("jack");
List<String> items = Arrays.asList("猪肉", "牛肉", "鱼肉");
order.setItems(items);
order.setPaymentAmount(BigDecimal.valueOf(78.365));
order.setPaymentTime(LocalDateTime.now());
order.setDeliveryMethod("顺丰");
velocityContext.put("order", order);
velocityContext.put("header", "OrderDetail");
// velocityContext.put("name", "sparrow");
velocityEngine.evaluate(velocityContext, writer, UUID.randomUUID().toString(), template);
log.info(writer.toString());
return writer.toString();
}
public String velocityEvaluateNoParseAndIncludeDirective() {
StringWriter writer = new StringWriter();
String template = "hello $name";
VelocityEngine velocityEngine = new VelocityEngine();
VelocityContext velocityContext = new VelocityContext();
velocityContext.put("name", "sparrow");
velocityEngine.evaluate(velocityContext, writer, UUID.randomUUID().toString(), template);
log.info(writer.toString());
return writer.toString();
}
/**
* https://velocity.apache.org/engine/devel/developer-guide.html#resource-loaders
* 通过velocityEngine.mergeTemplate或者template.merge 的方式,需要在初始化的时候配置ResourceLoader.
* 如果.vm文件中含有#include或#parse指令,也需要配置ResourceLoader
*
* because the resource management system will also handle non-template reasources, specifically things that are loaded via the #include() directive
*/
public String velocityMergeTemplate() {
StringWriter writer = new StringWriter();
VelocityEngine velocityEngine = new VelocityEngine();
velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADERS, "class");
velocityEngine.setProperty("resource.loader.class.class", ClasspathResourceLoader.class.getName());
velocityEngine.setProperty("resource.loader.class.cache", true);
VelocityContext velocityContext = new VelocityContext();
velocityContext.put("name", "sparrow");
// 方式一
// Template template = velocityEngine.getTemplate(SendEmailUtil.obtainTemplateRealPath("registerSuccess"));
// template.merge(velocityContext, writer);
// 方式二
velocityEngine.mergeTemplate(SendEmailUtil.obtainTemplateRealPath("registerSuccess"), Charset.defaultCharset().name(), velocityContext, writer);
log.info(writer.toString());
return writer.toString();
}
}
集成发送电子邮件
这里使用163邮箱发送电子邮件
关于收发邮件所用的协议,基本是两个
- SMTP协议用来发邮件,全称是Simple Mail Transfer Protocol,即简单邮件传输协议。它是用于在计算机网络中传输电子邮件的标准协议。
- IMAP协议用来收邮件,全称Internet Message Access Protocol,即互联网邮件访问协议。它是一种用于接收和管理电子邮件的协议。IMAP允许用户通过电子邮件客户端(如Outlook、Thunderbird等)连接到邮件服务器,查看、下载、组织和管理邮件
使用springboot封装好的mail工具-JavaMailSender,相关文档1和相关文档2
前提
在163邮箱上将POP3/SMTP/IMAP开启,并拿到授权码
编码
配置文件
server:
port: 18080
servlet:
context-path: /v
spring:
mail:
host: smtp.163.com
username: 自己的邮箱
# 163邮箱授权码
password: 填写你自己的
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
# https://velocity.apache.org/engine/devel/developer-guide.html#logging
logging:
level:
org.apache.velocity: trace
发送邮件
@Service
@Log
public class SendEmailService {
private static final VelocityEngine ve = new VelocityEngine();
@Autowired
private JavaMailSender javaMailSender;
@PostConstruct
public void initVelocity() {
ve.setProperty(RuntimeConstants.RESOURCE_LOADERS, "class");
ve.setProperty("resource.loader.class.class", ClasspathResourceLoader.class.getName());
ve.setProperty("resource.loader.class.cache", true);
ve.init();
}
public boolean sendOrderDetailEmail() {
VelocityContext context = new VelocityContext();
Order order = new Order();
order.setCustomerName("jack");
List<String> items = Arrays.asList("猪肉", "牛肉", "鱼肉");
order.setItems(items);
order.setPaymentAmount(BigDecimal.valueOf(78.365));
order.setPaymentTime(LocalDateTime.now());
order.setDeliveryMethod("顺丰");
context.put("order", order);
context.put("header", "OrderDetail");
Template template = ve.getTemplate(SendEmailUtil.obtainTemplateRealPath("orderDetail"));
StringWriter writer = new StringWriter();
template.merge(context, writer);
javaMailSender.send(buildMessage("OrderDetail Email", writer.toString()));
return true;
}
@SneakyThrows
private MimeMessage buildMessage(String subject, String emailContent) {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setFrom("发送方邮箱地址");
helper.setTo("接收方邮箱地址");
helper.setSubject(subject);
message.setText(emailContent, Charset.defaultCharset().name(), "html");
return message;
}
}
发送结果
官方文档
官方文档的阅读主要分为两大部分
- 如果需要写页面,则参考User’s Guide
- 如果需要写Java代码,则参考Developer’s Guide
示例代码
- 完整的示例代码仓库, 更详细地使用,请注意代码中的注释