发送简单文本邮件、HTML邮件(包括模板HTML邮件)、附件邮件、静态资源邮件。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<artifactId>spring-boot-demo-emailartifactId>
<version>1.0.0-SNAPSHOTversion>
<packaging>jarpackaging>
<parent>
<groupId>com.xkcodinggroupId>
<artifactId>spring-boot-demoartifactId>
<version>1.0.0-SNAPSHOTversion>
parent>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
<jasypt.version>2.1.1jasypt.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
dependency>
<dependency>
<groupId>com.github.ulisesbocchiogroupId>
<artifactId>jasypt-spring-boot-starterartifactId>
<version>${jasypt.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
dependencies>
<build>
<finalName>spring-boot-demo-emailfinalName>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
spring:
mail:
host: smtp.mxhichina.com
port: 465
username: spring-boot-[email protected]
# 使用 jasypt 加密密码,使用com.xkcoding.email.PasswordTest.testGeneratePassword 生成加密密码,替换 ENC(加密密码)
password: ENC(OT0qGOpXrr1Iog1W+fjOiIDCJdBjHyhy)
protocol: smtp
test-connection: true
default-encoding: UTF-8
properties:
mail.smtp.auth: true
mail.smtp.starttls.enable: true
mail.smtp.starttls.required: true
mail.smtp.ssl.enable: true
mail.display.sendmail: spring-boot-demo
# 为 jasypt 配置解密秘钥
jasypt:
encryptor:
password: spring-boot-demo
public interface MailService {
/**
* 发送文本邮件
*
* @param to 收件人地址
* @param subject 邮件主题
* @param content 邮件内容
* @param cc 抄送地址
*/
void sendSimpleMail(String to, String subject, String content, String... cc);
/**
* 发送HTML邮件
*
* @param to 收件人地址
* @param subject 邮件主题
* @param content 邮件内容
* @param cc 抄送地址
* @throws MessagingException 邮件发送异常
*/
void sendHtmlMail(String to, String subject, String content, String... cc) throws MessagingException;
/**
* 发送带附件的邮件
*
* @param to 收件人地址
* @param subject 邮件主题
* @param content 邮件内容
* @param filePath 附件地址
* @param cc 抄送地址
* @throws MessagingException 邮件发送异常
*/
void sendAttachmentsMail(String to, String subject, String content, String filePath, String... cc) throws MessagingException;
/**
* 发送正文中有静态资源的邮件
*
* @param to 收件人地址
* @param subject 邮件主题
* @param content 邮件内容
* @param rscPath 静态资源地址
* @param rscId 静态资源id
* @param cc 抄送地址
* @throws MessagingException 邮件发送异常
*/
void sendResourceMail(String to, String subject, String content, String rscPath, String rscId, String... cc) throws MessagingException;
}
@Service
public class MailServiceImpl implements MailService {
@Autowired
private JavaMailSender mailSender;
@Value("${spring.mail.username}")
private String from;
/**
* 发送文本邮件
*
* @param to 收件人地址
* @param subject 邮件主题
* @param content 邮件内容
* @param cc 抄送地址
*/
@Override
public void sendSimpleMail(String to, String subject, String content, String... cc) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(content);
if (ArrayUtil.isNotEmpty(cc)) {
message.setCc(cc);
}
mailSender.send(message);
}
/**
* 发送HTML邮件
*
* @param to 收件人地址
* @param subject 邮件主题
* @param content 邮件内容
* @param cc 抄送地址
* @throws MessagingException 邮件发送异常
*/
@Override
public void sendHtmlMail(String to, String subject, String content, String... cc) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
if (ArrayUtil.isNotEmpty(cc)) {
helper.setCc(cc);
}
mailSender.send(message);
}
/**
* 发送带附件的邮件
*
* @param to 收件人地址
* @param subject 邮件主题
* @param content 邮件内容
* @param filePath 附件地址
* @param cc 抄送地址
* @throws MessagingException 邮件发送异常
*/
@Override
public void sendAttachmentsMail(String to, String subject, String content, String filePath, String... cc) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
if (ArrayUtil.isNotEmpty(cc)) {
helper.setCc(cc);
}
FileSystemResource file = new FileSystemResource(new File(filePath));
String fileName = filePath.substring(filePath.lastIndexOf(File.separator));
helper.addAttachment(fileName, file);
mailSender.send(message);
}
/**
* 发送正文中有静态资源的邮件
*
* @param to 收件人地址
* @param subject 邮件主题
* @param content 邮件内容
* @param rscPath 静态资源地址
* @param rscId 静态资源id
* @param cc 抄送地址
* @throws MessagingException 邮件发送异常
*/
@Override
public void sendResourceMail(String to, String subject, String content, String rscPath, String rscId, String... cc) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
if (ArrayUtil.isNotEmpty(cc)) {
helper.setCc(cc);
}
FileSystemResource res = new FileSystemResource(new File(rscPath));
helper.addInline(rscId, res);
mailSender.send(message);
}
}
package com.xkcoding.email;
import org.jasypt.encryption.StringEncryptor;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
/**
*
* 数据库密码测试
*
*
* @author yangkai.shen
* @date Created in 2019-08-27 16:15
*/
public class PasswordTest extends SpringBootDemoEmailApplicationTests {
@Autowired
private StringEncryptor encryptor;
/**
* 生成加密密码
*/
@Test
public void testGeneratePassword() {
// 你的邮箱密码
String password = null;
// 加密后的密码(注意:配置上去的时候需要加 ENC(加密密码))
String encryptPassword = encryptor.encrypt(password);
String decryptPassword = encryptor.decrypt(encryptPassword);
System.out.println("password = " + password);
System.out.println("encryptPassword = " + encryptPassword);
System.out.println("decryptPassword = " + decryptPassword);
}
}
/**
*
* 邮件测试
*
*
* @author yangkai.shen
* @date Created in 2018-11-21 13:49
*/
public class MailServiceTest extends SpringBootDemoEmailApplicationTests {
@Autowired
private MailService mailService;
@Autowired
private TemplateEngine templateEngine;
@Autowired
private ApplicationContext context;
/**
* 测试简单邮件
*/
@Test
public void sendSimpleMail() {
mailService.sendSimpleMail("[email protected]", "这是一封简单邮件", "这是一封普通的SpringBoot测试邮件");
}
/**
* 测试HTML邮件
*
* @throws MessagingException 邮件异常
*/
@Test
public void sendHtmlMail() throws MessagingException {
Context context = new Context();
context.setVariable("project", "Spring Boot Demo");
context.setVariable("author", "Yangkai.Shen");
context.setVariable("url", "https://github.com/xkcoding/spring-boot-demo");
String emailTemplate = templateEngine.process("welcome", context);
mailService.sendHtmlMail("[email protected]", "这是一封模板HTML邮件", emailTemplate);
}
/**
* 测试HTML邮件,自定义模板目录
*
* @throws MessagingException 邮件异常
*/
@Test
public void sendHtmlMail2() throws MessagingException {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setApplicationContext(context);
templateResolver.setCacheable(false);
templateResolver.setPrefix("classpath:/email/");
templateResolver.setSuffix(".html");
templateEngine.setTemplateResolver(templateResolver);
Context context = new Context();
context.setVariable("project", "Spring Boot Demo");
context.setVariable("author", "Yangkai.Shen");
context.setVariable("url", "https://github.com/xkcoding/spring-boot-demo");
String emailTemplate = templateEngine.process("test", context);
mailService.sendHtmlMail("[email protected]", "这是一封模板HTML邮件", emailTemplate);
}
/**
* 测试附件邮件
*
* @throws MessagingException 邮件异常
*/
@Test
public void sendAttachmentsMail() throws MessagingException {
URL resource = ResourceUtil.getResource("static/xkcoding.png");
mailService.sendAttachmentsMail("[email protected]", "这是一封带附件的邮件", "邮件中有附件,请注意查收!", resource.getPath());
}
/**
* 测试静态资源邮件
*
* @throws MessagingException 邮件异常
*/
@Test
public void sendResourceMail() throws MessagingException {
String rscId = "xkcoding";
String content = "这是带静态资源的邮件
+ rscId + "\' >";
URL resource = ResourceUtil.getResource("static/xkcoding.png");
mailService.sendResourceMail("[email protected]", "这是一封带静态资源的邮件", content, resource.getPath(), rscId);
}
}
此文件为邮件模板,位于 resources/templates 目录下
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>SpringBootDemo(入门SpringBoot的首选Demo)title>
<style>
body {
text-align: center;
margin-left: auto;
margin-right: auto;
}
#welcome {
text-align: center;
}
style>
head>
<body>
<div id="welcome">
<h3>欢迎使用 <span th:text="${project}">span> - Powered By <span th:text=" ${author}">span>h3>
<span th:text="${url}">span>
<div style="text-align: center; padding: 10px">
<a style="text-decoration: none;" href="#" th:href="@{${url}}" target="_bank">
<strong>spring-boot-demo,入门Spring Boot的首选Demo!:)strong>
a>
div>
<div style="text-align: center; padding: 4px">
如果对你有帮助,请任意打赏
div>
<div style="width: 100%;height: 100%;text-align: center;display: flex">
<div style="flex: 1;">div>
<div style="display: flex;width: 400px;">
<div style="flex: 1;text-align: center;">
<div>
<img width="180px" height="180px" src="http://xkcoding.com/resources/wechat-reward-image.png">
div>
<div>微信打赏div>
div>
<div style="flex: 1;text-align: center;">
<div><img width="180px" height="180px" src="http://xkcoding.com/resources/alipay-reward-image.png">
div>
<div>支付宝打赏div>
div>
div>
<div style="flex: 1;">div>
div>
div>
body>
html>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<artifactId>spring-boot-demo-taskartifactId>
<version>1.0.0-SNAPSHOTversion>
<packaging>jarpackaging>
<parent>
<groupId>com.xkcodinggroupId>
<artifactId>spring-boot-demoartifactId>
<version>1.0.0-SNAPSHOTversion>
parent>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<finalName>spring-boot-demo-taskfinalName>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
此处等同于在配置文件配置
spring.task.scheduling.pool.size=20 spring.task.scheduling.thread-name-prefix=Job-Thread-
@Configuration
@EnableScheduling
@ComponentScan(basePackages = {"com.xkcoding.task.job"})
public class TaskConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
/**
* 这里等同于配置文件配置
* {@code spring.task.scheduling.pool.size=20} - Maximum allowed number of threads.
* {@code spring.task.scheduling.thread-name-prefix=Job-Thread- } - Prefix to use for the names of newly created threads.
* {@link org.springframework.boot.autoconfigure.task.TaskSchedulingProperties}
*/
@Bean
public Executor taskExecutor() {
return new ScheduledThreadPoolExecutor(20, new BasicThreadFactory.Builder().namingPattern("Job-Thread-%d").build());
}
}
@Component
@Slf4j
public class TaskJob {
/**
* 按照标准时间来算,每隔 10s 执行一次
*/
@Scheduled(cron = "0/10 * * * * ?")
public void job1() {
log.info("【job1】开始执行:{}", DateUtil.formatDateTime(new Date()));
}
/**
* 从启动时间开始,间隔 2s 执行
* 固定间隔时间
*/
@Scheduled(fixedRate = 2000)
public void job2() {
log.info("【job2】开始执行:{}", DateUtil.formatDateTime(new Date()));
}
/**
* 从启动时间开始,延迟 5s 后间隔 4s 执行
* 固定等待时间
*/
@Scheduled(fixedDelay = 4000, initialDelay = 5000)
public void job3() {
log.info("【job3】开始执行:{}", DateUtil.formatDateTime(new Date()));
}
}
server:
port: 8080
servlet:
context-path: /demo
# 下面的配置等同于 TaskConfig
#spring:
# task:
# scheduling:
# pool:
# size: 20
# thread-name-prefix: Job-Thread-
对定时任务的管理,包括新增定时任务,删除定时任务,暂停定时任务,恢复定时任务,修改定时任务启动时间,以及定时任务列表查询。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<artifactId>spring-boot-demo-task-quartzartifactId>
<version>1.0.0-SNAPSHOTversion>
<packaging>jarpackaging>
<parent>
<groupId>com.xkcodinggroupId>
<artifactId>spring-boot-demoartifactId>
<version>1.0.0-SNAPSHOTversion>
parent>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
<mybatis.mapper.version>2.1.0mybatis.mapper.version>
<mybatis.pagehelper.version>1.2.10mybatis.pagehelper.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-quartzartifactId>
dependency>
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapper-spring-boot-starterartifactId>
<version>${mybatis.mapper.version}version>
dependency>
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelper-spring-boot-starterartifactId>
<version>${mybatis.pagehelper.version}version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
dependency>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
<build>
<finalName>spring-boot-demo-task-quartzfinalName>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
server:
port: 8080
servlet:
context-path: /demo
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/spring-boot-demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimum-idle: 5
connection-test-query: SELECT 1 FROM DUAL
maximum-pool-size: 20
auto-commit: true
idle-timeout: 30000
pool-name: SpringBootDemoHikariCP
max-lifetime: 60000
connection-timeout: 30000
quartz:
# 参见 org.springframework.boot.autoconfigure.quartz.QuartzProperties
job-store-type: jdbc
wait-for-jobs-to-complete-on-shutdown: true
scheduler-name: SpringBootDemoScheduler
properties:
org.quartz.threadPool.threadCount: 5
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
org.quartz.jobStore.misfireThreshold: 5000
org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
# 在调度流程的第一步,也就是拉取待即将触发的triggers时,是上锁的状态,即不会同时存在多个线程拉取到相同的trigger的情况,也就避免的重复调度的危险。参考:https://segmentfault.com/a/1190000015492260
org.quartz.jobStore.acquireTriggersWithinLock: true
logging:
level:
com.xkcoding: debug
com.xkcoding.task.quartz.mapper: trace
mybatis:
configuration:
# 下划线转驼峰
map-underscore-to-camel-case: true
mapper-locations: classpath:mappers/*.xml
type-aliases-package: com.xkcoding.task.quartz.entity
mapper:
mappers:
- tk.mybatis.mapper.common.Mapper
not-empty: true
style: camelhump
wrap-keyword: "`{0}`"
safe-delete: true
safe-update: true
identity: MYSQL
pagehelper:
auto-dialect: true
helper-dialect: mysql
reasonable: true
params: count=countSql
@Data
@Accessors(chain = true)
public class JobForm {
/**
* 定时任务全类名
*/
@NotBlank(message = "类名不能为空")
private String jobClassName;
/**
* 任务组名
*/
@NotBlank(message = "任务组名不能为空")
private String jobGroupName;
/**
* 定时任务cron表达式
*/
@NotBlank(message = "cron表达式不能为空")
private String cronExpression;
}
@Service
@Slf4j
public class JobServiceImpl implements JobService {
private final Scheduler scheduler;
private final JobMapper jobMapper;
@Autowired
public JobServiceImpl(Scheduler scheduler, JobMapper jobMapper) {
this.scheduler = scheduler;
this.jobMapper = jobMapper;
}
/**
* 添加并启动定时任务
*
* @param form 表单参数 {@link JobForm}
* @return {@link JobDetail}
* @throws Exception 异常
*/
@Override
public void addJob(JobForm form) throws Exception {
// 启动调度器
scheduler.start();
// 构建Job信息
JobDetail jobDetail = JobBuilder.newJob(JobUtil.getClass(form.getJobClassName()).getClass())
.withIdentity(form.getJobClassName(), form.getJobGroupName())
.build();
// Cron表达式调度构建器(即任务执行的时间)
CronScheduleBuilder cron = CronScheduleBuilder.cronSchedule(form.getCronExpression());
//根据Cron表达式构建一个Trigger
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity(form.getJobClassName(), form.getJobGroupName())
.withSchedule(cron)
.build();
try {
scheduler.scheduleJob(jobDetail, trigger);
} catch (SchedulerException e) {
log.error("【定时任务】创建失败!", e);
throw new Exception("【定时任务】创建失败!");
}
}
/**
* 删除定时任务
*
* @param form 表单参数 {@link JobForm}
* @throws SchedulerException 异常
*/
@Override
public void deleteJob(JobForm form) throws SchedulerException {
scheduler.pauseTrigger(TriggerKey.triggerKey(form.getJobClassName(), form.getJobGroupName()));
scheduler.unscheduleJob(TriggerKey.triggerKey(form.getJobClassName(), form.getJobGroupName()));
scheduler.deleteJob(JobKey.jobKey(form.getJobClassName(), form.getJobGroupName()));
}
/**
* 暂停定时任务
*
* @param form 表单参数 {@link JobForm}
* @throws SchedulerException 异常
*/
@Override
public void pauseJob(JobForm form) throws SchedulerException {
scheduler.pauseJob(JobKey.jobKey(form.getJobClassName(), form.getJobGroupName()));
}
/**
* 恢复定时任务
*
* @param form 表单参数 {@link JobForm}
* @throws SchedulerException 异常
*/
@Override
public void resumeJob(JobForm form) throws SchedulerException {
scheduler.resumeJob(JobKey.jobKey(form.getJobClassName(), form.getJobGroupName()));
}
/**
* 重新配置定时任务
*
* @param form 表单参数 {@link JobForm}
* @throws Exception 异常
*/
@Override
public void cronJob(JobForm form) throws Exception {
try {
TriggerKey triggerKey = TriggerKey.triggerKey(form.getJobClassName(), form.getJobGroupName());
// 表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(form.getCronExpression());
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
// 根据Cron表达式构建一个Trigger
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
// 按新的trigger重新设置job执行
scheduler.rescheduleJob(triggerKey, trigger);
} catch (SchedulerException e) {
log.error("【定时任务】更新失败!", e);
throw new Exception("【定时任务】创建失败!");
}
}
/**
* 查询定时任务列表
*
* @param currentPage 当前页
* @param pageSize 每页条数
* @return 定时任务列表
*/
@Override
public PageInfo<JobAndTrigger> list(Integer currentPage, Integer pageSize) {
PageHelper.startPage(currentPage, pageSize);
List<JobAndTrigger> list = jobMapper.list();
return new PageInfo<>(list);
}
}
@RestController
@RequestMapping("/job")
@Slf4j
public class JobController {
private final JobService jobService;
@Autowired
public JobController(JobService jobService) {
this.jobService = jobService;
}
/**
* 保存定时任务
*/
@PostMapping
public ResponseEntity<ApiResponse> addJob(@Valid JobForm form) {
try {
jobService.addJob(form);
} catch (Exception e) {
return new ResponseEntity<>(ApiResponse.msg(e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
}
return new ResponseEntity<>(ApiResponse.msg("操作成功"), HttpStatus.CREATED);
}
/**
* 删除定时任务
*/
@DeleteMapping
public ResponseEntity<ApiResponse> deleteJob(JobForm form) throws SchedulerException {
if (StrUtil.hasBlank(form.getJobGroupName(), form.getJobClassName())) {
return new ResponseEntity<>(ApiResponse.msg("参数不能为空"), HttpStatus.BAD_REQUEST);
}
jobService.deleteJob(form);
return new ResponseEntity<>(ApiResponse.msg("删除成功"), HttpStatus.OK);
}
/**
* 暂停定时任务
*/
@PutMapping(params = "pause")
public ResponseEntity<ApiResponse> pauseJob(JobForm form) throws SchedulerException {
if (StrUtil.hasBlank(form.getJobGroupName(), form.getJobClassName())) {
return new ResponseEntity<>(ApiResponse.msg("参数不能为空"), HttpStatus.BAD_REQUEST);
}
jobService.pauseJob(form);
return new ResponseEntity<>(ApiResponse.msg("暂停成功"), HttpStatus.OK);
}
/**
* 恢复定时任务
*/
@PutMapping(params = "resume")
public ResponseEntity<ApiResponse> resumeJob(JobForm form) throws SchedulerException {
if (StrUtil.hasBlank(form.getJobGroupName(), form.getJobClassName())) {
return new ResponseEntity<>(ApiResponse.msg("参数不能为空"), HttpStatus.BAD_REQUEST);
}
jobService.resumeJob(form);
return new ResponseEntity<>(ApiResponse.msg("恢复成功"), HttpStatus.OK);
}
/**
* 修改定时任务,定时时间
*/
@PutMapping(params = "cron")
public ResponseEntity<ApiResponse> cronJob(@Valid JobForm form) {
try {
jobService.cronJob(form);
} catch (Exception e) {
return new ResponseEntity<>(ApiResponse.msg(e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
}
return new ResponseEntity<>(ApiResponse.msg("修改成功"), HttpStatus.OK);
}
@GetMapping
public ResponseEntity<ApiResponse> jobList(Integer currentPage, Integer pageSize) {
if (ObjectUtil.isNull(currentPage)) {
currentPage = 1;
}
if (ObjectUtil.isNull(pageSize)) {
pageSize = 10;
}
PageInfo<JobAndTrigger> all = jobService.list(currentPage, pageSize);
return ResponseEntity.ok(ApiResponse.ok(Dict.create().set("total", all.getTotal()).set("data", all.getList())));
}
}
QRTZ_TRIGGERS
表中的 TRIGGER_STATE
字段)集成 XXL-JOB 实现分布式定时任务,并提供绕过
xxl-job-admin
对定时任务的管理的方法,
包括定时任务列表,触发器列表,新增定时任务,删除定时任务,停止定时任务,启动定时任务,修改定时任务,
手动触发定时任务。
https://github.com/xuxueli/xxl-job.git
$ git clone https://github.com/xuxueli/xxl-job.git
数据库脚本地址:/xxl-job/doc/db/tables_xxl_job.sql
server.port=18080
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?Unicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<artifactId>spring-boot-demo-task-xxl-jobartifactId>
<version>1.0.0-SNAPSHOTversion>
<packaging>jarpackaging>
<parent>
<groupId>com.xkcodinggroupId>
<artifactId>spring-boot-demoartifactId>
<version>1.0.0-SNAPSHOTversion>
parent>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
<xxl-job.version>2.1.0xxl-job.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.xuxueligroupId>
<artifactId>xxl-job-coreartifactId>
<version>${xxl-job.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
dependency>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
<build>
<finalName>spring-boot-demo-task-xxl-jobfinalName>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>```
### 2.2. 编写 配置类 XxlJobProps.java
```java
@Data
@ConfigurationProperties(prefix = "xxl.job")
public class XxlJobProps {
/**
* 调度中心配置
*/
private XxlJobAdminProps admin;
/**
* 执行器配置
*/
private XxlJobExecutorProps executor;
/**
* 与调度中心交互的accessToken
*/
private String accessToken;
@Data
public static class XxlJobAdminProps {
/**
* 调度中心地址
*/
private String address;
}
@Data
public static class XxlJobExecutorProps {
/**
* 执行器名称
*/
private String appName;
/**
* 执行器 IP
*/
private String ip;
/**
* 执行器端口
*/
private int port;
/**
* 执行器日志
*/
private String logPath;
/**
* 执行器日志保留天数,-1
*/
private int logRetentionDays;
}
}
server:
port: 8080
servlet:
context-path: /demo
xxl:
job:
# 执行器通讯TOKEN [选填]:非空时启用;
access-token:
admin:
# 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
address: http://localhost:18080/xxl-job-admin
executor:
# 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
app-name: spring-boot-demo-task-xxl-job-executor
# 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
ip:
# 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
port: 9999
# 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
log-path: logs/spring-boot-demo-task-xxl-job/task-log
# 执行器日志保存天数 [选填] :值大于3时生效,启用执行器Log文件定期清理功能,否则不生效;
log-retention-days: -1
@Slf4j
@Configuration
@EnableConfigurationProperties(XxlJobProps.class)
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class XxlJobConfig {
private final XxlJobProps xxlJobProps;
@Bean(initMethod = "start", destroyMethod = "destroy")
public XxlJobSpringExecutor xxlJobExecutor() {
log.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(xxlJobProps.getAdmin().getAddress());
xxlJobSpringExecutor.setAccessToken(xxlJobProps.getAccessToken());
xxlJobSpringExecutor.setAppName(xxlJobProps.getExecutor().getAppName());
xxlJobSpringExecutor.setIp(xxlJobProps.getExecutor().getIp());
xxlJobSpringExecutor.setPort(xxlJobProps.getExecutor().getPort());
xxlJobSpringExecutor.setLogPath(xxlJobProps.getExecutor().getLogPath());
xxlJobSpringExecutor.setLogRetentionDays(xxlJobProps.getExecutor().getLogRetentionDays());
return xxlJobSpringExecutor;
}
}
@Slf4j
@Component
@JobHandler("demoTask")
public class DemoTask extends IJobHandler {
/**
* execute handler, invoked when executor receives a scheduling request
*
* @param param 定时任务参数
* @return 执行状态
* @throws Exception 任务异常
*/
@Override
public ReturnT<String> execute(String param) throws Exception {
// 可以动态获取传递过来的参数,根据参数不同,当前调度的任务不同
log.info("【param】= {}", param);
XxlJobLogger.log("demo task run at : {}", DateUtil.now());
return RandomUtil.randomInt(1, 11) % 2 == 0 ? SUCCESS : FAIL;
}
}
Run SpringBootDemoTaskXxlJobApplication
任务列表的操作列,拥有以下操作:执行、启动/停止、日志、编辑、删除
执行:单次触发任务,不影响定时逻辑
启动:启动定时任务
停止:停止定时任务
日志:查看当前任务执行日志
编辑:更新定时任务
删除:删除定时任务
实际场景中,如果添加定时任务都需要手动在 xxl-job-admin 去操作,这样可能比较麻烦,用户更希望在自己的页面,添加定时任务参数、定时调度表达式,然后通过 API 的方式添加定时任务
// 添加执行器列表
@RequestMapping("/list")
@ResponseBody
// 去除权限校验
@PermissionLimit(limit = false)
public ReturnT> list(){
return new ReturnT<>(xxlJobGroupDao.findAll());
}
...
// 分别在 pageList、add、update、remove、pause、start、triggerJob 方法上添加注解,去除权限校验
@PermissionLimit(limit = false)
@Slf4j
@RestController
@RequestMapping("/xxl-job")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class ManualOperateController {
private final static String baseUri = "http://127.0.0.1:18080/xxl-job-admin";
private final static String JOB_INFO_URI = "/jobinfo";
private final static String JOB_GROUP_URI = "/jobgroup";
/**
* 任务组列表,xxl-job叫做触发器列表
*/
@GetMapping("/group")
public String xxlJobGroup() {
HttpResponse execute = HttpUtil.createGet(baseUri + JOB_GROUP_URI + "/list").execute();
log.info("【execute】= {}", execute);
return execute.body();
}
/**
* 分页任务列表
*
* @param page 当前页,第一页 -> 0
* @param size 每页条数,默认10
* @return 分页任务列表
*/
@GetMapping("/list")
public String xxlJobList(Integer page, Integer size) {
Map<String, Object> jobInfo = Maps.newHashMap();
jobInfo.put("start", page != null ? page : 0);
jobInfo.put("length", size != null ? size : 10);
jobInfo.put("jobGroup", 2);
jobInfo.put("triggerStatus", -1);
HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/pageList").form(jobInfo).execute();
log.info("【execute】= {}", execute);
return execute.body();
}
/**
* 测试手动保存任务
*/
@GetMapping("/add")
public String xxlJobAdd() {
Map<String, Object> jobInfo = Maps.newHashMap();
jobInfo.put("jobGroup", 2);
jobInfo.put("jobCron", "0 0/1 * * * ? *");
jobInfo.put("jobDesc", "手动添加的任务");
jobInfo.put("author", "admin");
jobInfo.put("executorRouteStrategy", "ROUND");
jobInfo.put("executorHandler", "demoTask");
jobInfo.put("executorParam", "手动添加的任务的参数");
jobInfo.put("executorBlockStrategy", ExecutorBlockStrategyEnum.SERIAL_EXECUTION);
jobInfo.put("glueType", GlueTypeEnum.BEAN);
HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/add").form(jobInfo).execute();
log.info("【execute】= {}", execute);
return execute.body();
}
/**
* 测试手动触发一次任务
*/
@GetMapping("/trigger")
public String xxlJobTrigger() {
Map<String, Object> jobInfo = Maps.newHashMap();
jobInfo.put("id", 4);
jobInfo.put("executorParam", JSONUtil.toJsonStr(jobInfo));
HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/trigger").form(jobInfo).execute();
log.info("【execute】= {}", execute);
return execute.body();
}
/**
* 测试手动删除任务
*/
@GetMapping("/remove")
public String xxlJobRemove() {
Map<String, Object> jobInfo = Maps.newHashMap();
jobInfo.put("id", 4);
HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/remove").form(jobInfo).execute();
log.info("【execute】= {}", execute);
return execute.body();
}
/**
* 测试手动停止任务
*/
@GetMapping("/stop")
public String xxlJobStop() {
Map<String, Object> jobInfo = Maps.newHashMap();
jobInfo.put("id", 4);
HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/stop").form(jobInfo).execute();
log.info("【execute】= {}", execute);
return execute.body();
}
/**
* 测试手动启动任务
*/
@GetMapping("/start")
public String xxlJobStart() {
Map<String, Object> jobInfo = Maps.newHashMap();
jobInfo.put("id", 4);
HttpResponse execute = HttpUtil.createGet(baseUri + JOB_INFO_URI + "/start").form(jobInfo).execute();
log.info("【execute】= {}", execute);
return execute.body();
}
}