本篇来学习在企业开发过程中经常会使用到的一些功能,如邮件发送、定时任务等功能。

邮件发送

邮件发送是一个非常常见的功能,注册时的身份认证、重要通知发送等都会使用到邮件来发送。Sun公司提供了JavaMail用来实现邮件发送,但是配置烦琐, Spring中提供了JavaMailSender用来简化邮件配置,而SpringBoot则提供了MailSenderAutoConfiguration对邮件的发送做了进一步简化。接来下就来看看如何在SpringBoot中发送邮件。

发送前的准备

注意本篇以QQ邮箱为例,向读者介绍邮件的发送过程。使用QQ邮件发送邮件,首先要申请开通POP3/SMTP服务或者IMAP/SMTP服务。可以看到无论是哪种都需要使用SMTP协议,SMTP全称Simple Mail Transfer Protocol,也就是简单邮件传输协议,它定义了邮件客户端软件与SMTP服务器之间,以及SMTP服务器与SMTP服务器之间的通信规则。举个例子,用户(test@qq.com)先将邮件投递到腾讯的SMTP服务器,这个过程就使用了SMTP协议,然后腾讯的SMTP服务器将邮件投递到网易的SMTP服务器,这个过程依然使用了SMTP协议,SMTP服务器就是用来接收邮件的。而POP3全称为Post Office Protocol3,也就是邮局协议,它定义了邮件客户端与POP3服务器之间的通信规则。该协议在什么场景下会用到呢?当邮件到达网易的SMTP服务器之后,用户(admin@163.com)需要登录服务器查看邮件,这个时候就用上该协议了:邮件服务商会为每一个用户提供专门的邮件存储空间, SMTP服务器收到邮件之后,将邮件保存到相应用户的邮件存储空间中,如果用户要读取邮件,就需要通过邮件服务商的POP3邮件服务器来完成。至于IMAP协议,则是对POP3协议的扩展,功能更强,作用类似。

下面介绍QQ邮箱开通POP3/SMTP服务或者IMAP/SMTP服务的步骤。

第一步,登录QQ邮箱,然后依次单击顶部的设置按钮和账户按钮,如下图所示:

第二步,在账户选项卡下方找到POP3/SMTP服务,单击后方的“开启”按钮,这里笔者已经全部打开了:

单击“开启”按钮后,按照引导步骤发送短信,操作成功后,会获取一个授权码,如下图所示,记得将授权码保存下来后面会使用到它。

这个授权码就是以后登录的密钥,以后登录不再需要密码了。

邮件发送

接下来将是通过构建一个SpringBoot项目来完成邮件发送的功能。

环境搭建

使用spring Initializr构建工具构建一个SpringBoot的Web应用,名称为mailspringboot,然后在pom.xml文件中添加如下依赖:

1
2
3
4
5
6
7
8
9
<!--添加mail依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

接着修改application.properties配置文件,在其中新增邮件基本信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 设置邮件服务器主机
spring.mail.host=smtp.qq.com
# 设置邮件服务器端口(可以是465或者587)
spring.mail.port=465
# 设置邮件服务器用户名
spring.mail.username=xxxx@qq.com
# 设置邮件服务器密码(注意必须填写授权码)
spring.mail.password=
# 设置邮件默认编码格式
spring.mail.default-encoding=UTF-8
# 设置邮件的SSL连接配置
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
# 开启debug,便于开发者查看邮件发送日志
spring.mail.properties.mail.debug=true

在上面笔者配置了邮件服务器的地址、端口(可以是465或者587)、用户的账号和密码以及默认编码、SSL连接配置等,最后开启debug,这样便于开发者查看邮件发送日志。注意SSL的配置可以在QQ邮箱帮助中心看到相关文档,如下图所示:

完成这些配置后,基本的邮件发送环境就搭建成功了,接下来就可以发送邮件了。邮件从简单到复杂有多种类型,下面分别予以学习。

发送简单邮件

新建一个component包,并在其中新建一个MailService类用来封装邮件的发送,相应的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class MailService {
@Autowired
JavaMailSender javaMailSender;
public void sendSimpleMail(String from,String to,String cc,String subject,String text){
SimpleMailMessage simpleMsg = new SimpleMailMessage();
simpleMsg.setFrom(from);
simpleMsg.setTo(to);
simpleMsg.setCc(cc);
simpleMsg.setSubject(subject);
simpleMsg.setText(text);
javaMailSender.send(simpleMsg);
}
}

解释一下上述代码的含义:

  • JavaMailSender是SpringBoot在MailSenderPropertiesConfiguration类中配置好的,该类在Mail自动配置类MailSenderAutoConfiguration中导入,因此这里可以直接注入JavaMailSender进行使用。
  • 注意这里定义的sendSimpleEmail方法的5个参数分别表示邮件发送者,收件人,抄送人,邮件主题以及邮件内容。cc和bcc的区别可以点击 这里 进行查看。
  • 简单邮件可以直接构建出一个SimpleMailMessage对象,然后设置它对应的属性,设置完成后即可通过 JavaMailSender对象的send方法将邮件发送出去。

接下来对这个MailService进行单元测试,相应的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RunWith(SpringRunner.class)
@SpringBootTest
public class MailServiceTest {
@Autowired
MailService mailService;
@Test
public void sendSimpleMailTest(){
mailService.sendSimpleMail(
"aaaaa@qq.com",
"bbbbb@qq.com",
"ccccc@qq.com",
"简单邮件发送测试主题",
"SimpleMailSender邮件测试内容"
);
}
}

之后运行该测试方法,可以看到邮件发送成功:

SpringBoot的控制台也有对应的发送日志信息:

发送带附件的邮件

要发送一个带附件的邮件也非常容易,通过调用Attachment方法即可添加附件,该方法调用多次即可添加多个附件。只需要在MailService类中新增如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**带有附件的邮件*/
public void sendAttachFileMail(String from, String to, String subject, String text, File file){
MimeMessage mimeMsg = javaMailSender.createMimeMessage();
try {
MimeMessageHelper helper = new MimeMessageHelper(mimeMsg,true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(text);
helper.addAttachment(file.getName(),file);
javaMailSender.send(mimeMsg);
} catch (MessagingException e) {
e.printStackTrace();
}
}

可以看到这里使用了MimeMessageHelper,它简化了邮件配置,它的构造方法的第二个参数true表示构造一个mutipart message类型的邮件,mutipart message类型的邮件包含多个正文、附件以及内嵌资源,邮件的表现形式更加丰富。最后通过addAttachment方法来添加附件。

接下来对这个sendAttachFileMail方法进行单元测试,相应的代码为:

1
2
3
4
5
6
7
8
9
@Test
public void sendAttachFileTest(){
mailService.sendAttachFileMail(
"aaaaa@qq.com",
"bbbbb@qq.com",
"带有附件形式的邮件发送测试主题",
"AttachFileSender邮件测试内容",
new File("C:\\Users\\Administrator\\Desktop\\avatar.jpg"));
}

之后运行该测试方法,可以看到邮件发送成功:

发送正文带图片的邮件

注意这里所指的“发送带图片资源的邮件”,其中的图片不是以附件的形式,而是在正文中插入的图片,开发者可以使用FileSystemResource来实现这一功能。在MailService类中新增如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**正文中带有图片的邮件*/
public void sendTextWithImage(String from, String to, String subject, String text, String[] srcPath,String[] resIds){
if(srcPath.length!=resIds.length){
System.out.println("对不起,发送失败");
return;
}
MimeMessage mimeMsg = javaMailSender.createMimeMessage();
try {
MimeMessageHelper helper = new MimeMessageHelper(mimeMsg,true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(text,true);
for(int i=0;i<srcPath.length;i++){
FileSystemResource resource = new FileSystemResource(new File(srcPath[i]));
helper.addInline(resIds[i],resource);
}
javaMailSender.send(mimeMsg);
} catch (MessagingException e) {
System.out.println("对不起,发送失败");
}
}

在发送邮件时,分别传入图片资源路径和资源id,通过FileSystemResource构造静态资源,然后调用addInline方法将资源添加到邮件对象中。注意在调用MimeMessageHelper对象的setText方法时,第二个参数true表示邮件的正文是HTML格式的,该参数不传默认为false。

接下来对这个sendTextWithImage方法进行单元测试,相应的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void sendTextWithImageTest(){
mailService.sendTextWithImage(
"aaaaa@qq.com",
"bbbbb@qq.com",
"正文带有图片的邮件发送测试主题(图片)",
"<div>你好,这是一封正文带有图片的邮件:"+
"这是图片1:<div><img src='cid:p01'/></div>"+
"这是图片2:<div><img src='cid:p02'/></div>"+
"</div>",
new String[]{"C:\\Users\\Administrator\\Desktop\\avatar.jpg","C:\\Users\\Administrator\\Desktop\\one.png"},
new String[]{"p01","p02"}
);
}

邮件的正文是一段HTML文本,用cid标注出两个静态资源,分别为p01和p02。

之后运行该测试方法,可以看到邮件发送成功:

使用FreeMarker构建邮件模板

其实自己观察就会发现前面正文中携带图片这是采用了字符串拼接而成的HTML,这种不但容易出错,且不易维护,使用HTML模板可以很好地解决这个问题。使用FreeMarker构建邮件模板,首先需要添加FreeMarker依赖:

1
2
3
4
5
<!--添加freemarker依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

然后在MailService类中新增如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**使用FreeMaker构建邮件模板的邮件*/
public void sendHtmlMail(String from,String to,String subject,String text){
MimeMessage mimeMsg = javaMailSender.createMimeMessage();
try {
MimeMessageHelper helper = new MimeMessageHelper(mimeMsg,true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(text,true);
javaMailSender.send(mimeMsg);
} catch (MessagingException e) {
System.out.println("对不起,发送失败");
}
}

接下来在resources目录下创建ftlh目录作为模板存放位置,在该目录下创建mailtemplate.ftlh作为邮件模板,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div>邮箱激活</div>
<div>您的注册信息为:
<table border="1px">
<tr>
<td>用户名</td>
<td>${username}</td>
</tr>
<tr>
<td>用户性别</td>
<td>${gender}</td>
</tr>
</table>
</div>
<div>
<a href="http://www.baidu.com">确认信息无误后,请点击本链接激活邮箱</a>
</div>

既然使用到了用户信息,那么需要创建一个User实体类,其中的代码为:

1
2
3
4
5
public class User {
private String username;
private String gender;
//getter、setter和toString方法
}

接下来对这个sendHtmlMail方法进行单元测试,相应的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void sendHtmlMailTest(){
try {
Configuration configuration = new Configuration(Configuration.VERSION_2_3_30);
ClassLoader loader = MailspringbootApplication.class.getClassLoader();
configuration.setClassLoaderForTemplateLoading(loader,"ftlh");
Template template = configuration.getTemplate("mailtemplate.ftlh");
StringWriter mail = new StringWriter();
User user = new User();
user.setUsername("余思");
user.setGender("男");
template.process(user,mail);
mailService.sendHtmlMail(
"aaaaa@qq.com",
"bbbbb@qq.com",
"使用FreeMaker构建邮件模板的邮件",
mail.toString()
);
} catch (Exception e) {
e.printStackTrace();
}
}

首先配置FreeMaker模板的位置,配置模板文件,然后结合User对象来渲染模板,将模板结果发送出去,之后运行该测试方法,可以看到邮件发送成功:

使用Thymeleaf构建邮件模板

既然可以使用FreeMaker构建邮件模板,那么自然也能使用Thymeleaf来构建邮件模板,使用Thymeleaf构建邮件模板相对来说更加的方便。使用Thymeleaf构建邮件模板,首先需要添加Thymeleaf依赖:

1
2
3
4
5
<!--添加thymeleaf依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

注意Thymeleaf的邮件模板默认位置在resources/templates目录下,创建对应的html目录,然后创建模板mailtemplate.html文件,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>邮件</title>
</head>
<body>
<div>邮箱激活</div>
<div>您的注册信息为:
<table border="1px">
<tr>
<td>用户名</td>
<td th:text="${username}"></td>
</tr>
<tr>
<td>用户性别</td>
<td th:text="${gender}"></td>
</tr>
</table>
</div>
<div>
<a href="http://www.baidu.com">确认信息无误后,请点击本链接激活邮箱</a>
</div>
</body>
</html>

接下来对之前那个sendHtmlMail方法进行单元测试,注意这里是使用Thymeleaf模板引擎,相应的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Autowired
TemplateEngine templateEngine;
@Test
public void sendHtmlMailByThymeleafTest(){
Context context = new Context();
context.setVariable("username","余思");
context.setVariable("gender","男");
String mail = templateEngine.process("/html/mailtemplate.html",context);
mailService.sendHtmlMail(
"2810706745@qq.com",
"2131247535@qq.com",
"使用FreeMaker构建邮件模板的邮件",
mail
);
}

不同于FreeMaker,Thymeleaf提供了TemplateEngine来对模板进行渲染,通过Context构造模板中变量需要的值,这种方式比FreeMaker构建模板更加方便。

之后运行该测试方法,可以看到邮件发送成功:

以上介绍的这几种不同的邮件发送方式基本上能满足大部分的业务需求,开发人员在实际过程中需要结合实际情况来选择合适的方式。

定时任务

定时任务是企业级开发中最常用的功能之一,如定时统计订单数、数据库备份、定时发送短信和邮件、定时统计博客访客等,简单的定时任务可以直接通过Spring中的@Scheduled注解来实现,复杂的定时任务则可以通过集成Quartz([kwɔːts])来实现,下面分别予以介绍。

@Scheduled

@Scheduled是由Spring提供的定时任务注解,使用方便,配置简单,可以解决工作中的大部分定时任务需求,使用方式如下:

第一步,创建工程并添加依赖。使用spring Initializr构建工具构建一个SpringBoot的Web应用,名称为quartzspringboot,然后添加如下依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

第二步,开启定时任务。在项目启动类上添加@EnableScheduling注解,相应的代码为:

1
2
3
4
5
6
7
@SpringBootApplication
@EnableScheduling
public class QuartzspringbootApplication {
public static void main(String[] args) {
SpringApplication.run(QuartzspringbootApplication.class, args);
}
}

第三步,配置定时任务。定时任务主要是通过@Scheduled注解来进行配置。新建一个component包,并在其中创建一个MySchedule类,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class MySchedule {
@Scheduled(fixedDelay = 1000)
public void fixedDelay(){
System.out.println("fixedDelay:"+new Date());
}
@Scheduled(fixedRate = 2000)
public void fixedRate(){
System.out.println("fixedRate:"+new Date());
}
@Scheduled(initialDelay = 1000,fixedRate = 2000)
public void initialDelay(){
System.out.println("initialDelay:"+new Date());
}
@Scheduled(cron = "0 * * * * ?")
public void cron(){
System.out.println("cron:"+new Date());
}
}

简单解释一下上述代码的含义:

  • 通过@Scheduled注解来标注一个定时任务,其中fixedDelay=1000表示在当前任务执行结束1秒后开启另一个任务,fixedRate=2000表示在当前任务执行2秒后开启另一个定时任务,initialDelay=1000则表示首次执行的延迟时间。
  • @Scheduled注解中也可以使用cron表达式,cron="0 * * * * ?"表示该定时任务每分钟执行一次。

配置完成后,接下来启动SpringBoot项目,定时任务部分打印日志如下所示:

Quartz

Quartz简介

Quartz是一个功能丰富的开源作业调度库,它由Java写成,可以集成在任何Java应用程序中,包括JavaSE和JavaEE等。使用Quartz可以创建简单或者复杂的执行计划,它支持数据库、集群、插件以及邮件,且支持cron表达式,具有极高的灵活性。SpringBoot中集成Quartz和Spring中集成Quartz非常相似,主要提供了三个Bean:JobDetail、Trigger以及SchedulerFactory。

整合SpringBoot

在前面的项目中添加quartz依赖:

1
2
3
4
5
<!--添加quartz依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

接着新建两个Job,其中MyFirstJob是一个普通的JavaBean,因此可以先添加@Component注解将其注册到Spring容器中:

1
2
3
4
5
6
@Component
public class MyFirstJob {
public void sayHello(){
System.out.println("MyFirstJob--->sayHello:"+new Date());
}
}

另一个Job为MySecondJob,它继承抽象类QuartzJobBean,且需要实现该类中的executeInternal方法,该方法在任务被调用的时候使用,相应的代码为:

1
2
3
4
5
6
7
8
9
10
public class MySecondJob extends QuartzJobBean {
private String name;
public void setName(String name){
this.name=name;
}
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("hello---->name:"+new Date());
}
}

接下来新建一个config包,并在其中创建QuartzConfig类用于对JobDetailTrigger进行配置,相应的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Configuration
public class QuartzConfig {
@Bean
MethodInvokingJobDetailFactoryBean jobDetailOne(){
MethodInvokingJobDetailFactoryBean bean = new MethodInvokingJobDetailFactoryBean();
bean.setTargetBeanName("myFirstJob");
bean.setTargetMethod("sayHello");
return bean;
}
@Bean
JobDetailFactoryBean jobDetailTwo(){
JobDetailFactoryBean bean = new JobDetailFactoryBean();
bean.setJobClass(MySecondJob.class);
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("name","envy");
bean.setJobDataMap(jobDataMap);
bean.setDurability(true);
return bean;
}
@Bean
SimpleTriggerFactoryBean simpleTrigger(){
SimpleTriggerFactoryBean bean = new SimpleTriggerFactoryBean();
bean.setJobDetail(jobDetailOne().getObject());
bean.setRepeatCount(3);
bean.setStartDelay(1000);
bean.setRepeatInterval(2000);
return bean;
}
@Bean
CronTriggerFactoryBean cronTrigger(){
CronTriggerFactoryBean bean = new CronTriggerFactoryBean();
bean.setJobDetail(jobDetailTwo().getObject());
bean.setCronExpression("* * * * * ?");
return bean;
}
@Bean
SchedulerFactoryBean schedulerFactory(){
SchedulerFactoryBean bean = new SchedulerFactoryBean();
SimpleTrigger simpleTrigger = simpleTrigger().getObject();
CronTrigger cronTrigger = cronTrigger().getObject();
bean.setTriggers(simpleTrigger,cronTrigger);
return bean;
}
}

解释一下上述代码的含义:

  • JobDetail的配置方式方式:第一种方式通过MethodInvokingJobDetailFactoryBean类配置 JobDetail,只需要指定Job的实例名称和要调用的方法即可,注册这种方式无法在创建 JobDetail时传递参数;第二种方式是通过 JobDetailFactoryBean来实现的,这种方式只需要指定 JobClass即可,当然可以通过 JobDataMap传递参数到Job中,Job中只需要提供属性名,并且提供一个相应的set方法即可接收到参数。
  • Trigger有多种不同实现,这里展示两种最常使用的TriggerSimpleTriggerCronTrigger,这两种Trigger分别使用SimpleTriggerFactoryBeanCronTriggerFactoryBean进行创建。在SimpleTriggerFactoryBean对象中,首先设置JobDetail,然后通过setRepeatCount配置任务循环次数,setStartDelay配置任务启动延迟时间,setRepeatInterval配置任务的时间间隔。在CronTriggerFactoryBean对象中,则主要配置JobDetail和Cron表达式。
  • 最后通过SchedulerFactoryBean创建SchedulerFactory对象,然后配置Trigger即可。

经过这几步的配置,定时任务就配置成功了。接下来启动SpringBoot项目,可以看到控制台输出一些信息:

可以看到MyFirstJob在第一次运行以后,每间隔2秒执行了3次后就不再执行了,MySecondJob则每秒执行一次,一直执行下去。