> 文章列表 > email,task,task-quartz,xxl-job

email,task,task-quartz,xxl-job

email,task,task-quartz,xxl-job

spring-boot-demo-email

发送简单文本邮件、HTML邮件(包括模板HTML邮件)、附件邮件、静态资源邮件。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion><artifactId>spring-boot-demo-email</artifactId><version>1.0.0-SNAPSHOT</version><packaging>jar</packaging><parent><groupId>com.xkcoding</groupId><artifactId>spring-boot-demo</artifactId><version>1.0.0-SNAPSHOT</version></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version><jasypt.version>2.1.1</jasypt.version></properties><dependencies><!-- Spring Boot 邮件依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId></dependency><!--jasypt配置文件加解密--><dependency><groupId>com.github.ulisesbocchio</groupId><artifactId>jasypt-spring-boot-starter</artifactId><version>${jasypt.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId></dependency><!-- Spring Boot 模板依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency></dependencies><build><finalName>spring-boot-demo-email</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

application.yml

spring:mail:host: smtp.mxhichina.comport: 465username: spring-boot-demo@xkcoding.com# 使用 jasypt 加密密码,使用com.xkcoding.email.PasswordTest.testGeneratePassword 生成加密密码,替换 ENC(加密密码)password: ENC(OT0qGOpXrr1Iog1W+fjOiIDCJdBjHyhy)protocol: smtptest-connection: truedefault-encoding: UTF-8properties:mail.smtp.auth: truemail.smtp.starttls.enable: truemail.smtp.starttls.required: truemail.smtp.ssl.enable: truemail.display.sendmail: spring-boot-demo
# 为 jasypt 配置解密秘钥
jasypt:encryptor:password: spring-boot-demo

MailService.java

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;}

MailServiceImpl.java

@Service
public class MailServiceImpl implements MailService {@Autowiredprivate JavaMailSender mailSender;@Value("${spring.mail.username}")private String from;/*** 发送文本邮件** @param to      收件人地址* @param subject 邮件主题* @param content 邮件内容* @param cc      抄送地址*/@Overridepublic 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 邮件发送异常*/@Overridepublic 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 邮件发送异常*/@Overridepublic 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 邮件发送异常*/@Overridepublic 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;/*** <p>* 数据库密码测试* </p>** @author yangkai.shen* @date Created in 2019-08-27 16:15*/
public class PasswordTest extends SpringBootDemoEmailApplicationTests {@Autowiredprivate StringEncryptor encryptor;/*** 生成加密密码*/@Testpublic 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);}
}

MailServiceTest.java

/*** <p>* 邮件测试* </p>** @author yangkai.shen* @date Created in 2018-11-21 13:49*/
public class MailServiceTest extends SpringBootDemoEmailApplicationTests {@Autowiredprivate MailService mailService;@Autowiredprivate TemplateEngine templateEngine;@Autowiredprivate ApplicationContext context;/*** 测试简单邮件*/@Testpublic void sendSimpleMail() {mailService.sendSimpleMail("237497819@qq.com", "这是一封简单邮件", "这是一封普通的SpringBoot测试邮件");}/*** 测试HTML邮件** @throws MessagingException 邮件异常*/@Testpublic 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("237497819@qq.com", "这是一封模板HTML邮件", emailTemplate);}/*** 测试HTML邮件,自定义模板目录** @throws MessagingException 邮件异常*/@Testpublic 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("237497819@qq.com", "这是一封模板HTML邮件", emailTemplate);}/*** 测试附件邮件** @throws MessagingException 邮件异常*/@Testpublic void sendAttachmentsMail() throws MessagingException {URL resource = ResourceUtil.getResource("static/xkcoding.png");mailService.sendAttachmentsMail("237497819@qq.com", "这是一封带附件的邮件", "邮件中有附件,请注意查收!", resource.getPath());}/*** 测试静态资源邮件** @throws MessagingException 邮件异常*/@Testpublic void sendResourceMail() throws MessagingException {String rscId = "xkcoding";String content = "<html><body>这是带静态资源的邮件<br/><img src=\\'cid:" + rscId + "\\' ></body></html>";URL resource = ResourceUtil.getResource("static/xkcoding.png");mailService.sendResourceMail("237497819@qq.com", "这是一封带静态资源的邮件", content, resource.getPath(), rscId);}
}

welcome.html

此文件为邮件模板,位于 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>

参考

  • Spring Boot 官方文档:https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/#boot-features-email
  • Spring Boot 官方文档:https://docs.spring.io/spring/docs/5.1.2.RELEASE/spring-framework-reference/integration.html#mail

spring-boot-demo-task

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion><artifactId>spring-boot-demo-task</artifactId><version>1.0.0-SNAPSHOT</version><packaging>jar</packaging><parent><groupId>com.xkcoding</groupId><artifactId>spring-boot-demo</artifactId><version>1.0.0-SNAPSHOT</version></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><finalName>spring-boot-demo-task</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

TaskConfig.java

此处等同于在配置文件配置

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 {@Overridepublic 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}*/@Beanpublic Executor taskExecutor() {return new ScheduledThreadPoolExecutor(20, new BasicThreadFactory.Builder().namingPattern("Job-Thread-%d").build());}
}

TaskJob.java

@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()));}
}

application.yml

server:port: 8080servlet:context-path: /demo
# 下面的配置等同于 TaskConfig
#spring:
#  task:
#    scheduling:
#      pool:
#        size: 20
#      thread-name-prefix: Job-Thread-

参考

  • Spring Boot官方文档:https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/#boot-features-task-execution-scheduling

spring-boot-demo-task-quartz

对定时任务的管理,包括新增定时任务,删除定时任务,暂停定时任务,恢复定时任务,修改定时任务启动时间,以及定时任务列表查询。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion><artifactId>spring-boot-demo-task-quartz</artifactId><version>1.0.0-SNAPSHOT</version><packaging>jar</packaging><parent><groupId>com.xkcoding</groupId><artifactId>spring-boot-demo</artifactId><version>1.0.0-SNAPSHOT</version></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version><mybatis.mapper.version>2.1.0</mybatis.mapper.version><mybatis.pagehelper.version>1.2.10</mybatis.pagehelper.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId></dependency><dependency><groupId>tk.mybatis</groupId><artifactId>mapper-spring-boot-starter</artifactId><version>${mybatis.mapper.version}</version></dependency><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>${mybatis.pagehelper.version}</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies><build><finalName>spring-boot-demo-task-quartz</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

application.yml

server:port: 8080servlet: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%2B8username: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Drivertype: com.zaxxer.hikari.HikariDataSourcehikari:minimum-idle: 5connection-test-query: SELECT 1 FROM DUALmaximum-pool-size: 20auto-commit: trueidle-timeout: 30000pool-name: SpringBootDemoHikariCPmax-lifetime: 60000connection-timeout: 30000quartz:# 参见 org.springframework.boot.autoconfigure.quartz.QuartzPropertiesjob-store-type: jdbcwait-for-jobs-to-complete-on-shutdown: truescheduler-name: SpringBootDemoSchedulerproperties:org.quartz.threadPool.threadCount: 5org.quartz.threadPool.threadPriority: 5org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: trueorg.quartz.jobStore.misfireThreshold: 5000org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTXorg.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate# 在调度流程的第一步,也就是拉取待即将触发的triggers时,是上锁的状态,即不会同时存在多个线程拉取到相同的trigger的情况,也就避免的重复调度的危险。参考:https://segmentfault.com/a/1190000015492260org.quartz.jobStore.acquireTriggersWithinLock: true
logging:level:com.xkcoding: debugcom.xkcoding.task.quartz.mapper: trace
mybatis:configuration:# 下划线转驼峰map-underscore-to-camel-case: truemapper-locations: classpath:mappers/*.xmltype-aliases-package: com.xkcoding.task.quartz.entity
mapper:mappers:- tk.mybatis.mapper.common.Mappernot-empty: truestyle: camelhumpwrap-keyword: "`{0}`"safe-delete: truesafe-update: trueidentity: MYSQL
pagehelper:auto-dialect: truehelper-dialect: mysqlreasonable: trueparams: count=countSql

JobForm.java

@Data
@Accessors(chain = true)
public class JobForm {/*** 定时任务全类名*/@NotBlank(message = "类名不能为空")private String jobClassName;/*** 任务组名*/@NotBlank(message = "任务组名不能为空")private String jobGroupName;/*** 定时任务cron表达式*/@NotBlank(message = "cron表达式不能为空")private String cronExpression;
}

JobServiceImpl.java

@Service
@Slf4j
public class JobServiceImpl implements JobService {private final Scheduler scheduler;private final JobMapper jobMapper;@Autowiredpublic JobServiceImpl(Scheduler scheduler, JobMapper jobMapper) {this.scheduler = scheduler;this.jobMapper = jobMapper;}/*** 添加并启动定时任务** @param form 表单参数 {@link JobForm}* @return {@link JobDetail}* @throws Exception 异常*/@Overridepublic 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表达式构建一个TriggerCronTrigger 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 异常*/@Overridepublic 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 异常*/@Overridepublic void pauseJob(JobForm form) throws SchedulerException {scheduler.pauseJob(JobKey.jobKey(form.getJobClassName(), form.getJobGroupName()));}/*** 恢复定时任务** @param form 表单参数 {@link JobForm}* @throws SchedulerException 异常*/@Overridepublic void resumeJob(JobForm form) throws SchedulerException {scheduler.resumeJob(JobKey.jobKey(form.getJobClassName(), form.getJobGroupName()));}/*** 重新配置定时任务** @param form 表单参数 {@link JobForm}* @throws Exception 异常*/@Overridepublic 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表达式构建一个Triggertrigger = 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 定时任务列表*/@Overridepublic PageInfo<JobAndTrigger> list(Integer currentPage, Integer pageSize) {PageHelper.startPage(currentPage, pageSize);List<JobAndTrigger> list = jobMapper.list();return new PageInfo<>(list);}
}

JobController.java

@RestController
@RequestMapping("/job")
@Slf4j
public class JobController {private final JobService jobService;@Autowiredpublic JobController(JobService jobService) {this.jobService = jobService;}/*** 保存定时任务*/@PostMappingpublic 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);}/*** 删除定时任务*/@DeleteMappingpublic 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);}@GetMappingpublic 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())));}
}

启动

image-20181126214007372
image-20181126214109926
image-20181126214212905
image-20181126214138641
image-20181126214250757

参考

  • Spring Boot 官方文档:https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/#boot-features-quartz
  • Quartz 官方文档:http://www.quartz-scheduler.org/documentation/quartz-2.2.x/quick-start.html
  • Quartz 重复调度问题:https://segmentfault.com/a/1190000015492260
  • 关于Quartz定时任务状态 (在 QRTZ_TRIGGERS 表中的 TRIGGER_STATE 字段)
    image-20181126171110378
  • Vue.js 官方文档:https://cn.vuejs.org/v2/guide/
  • Element-UI 官方文档:http://element-cn.eleme.io/#/zh-CN

spring-boot-demo-task-xxl-job

集成 XXL-JOB 实现分布式定时任务,并提供绕过 xxl-job-admin 对定时任务的管理的方法,
包括定时任务列表,触发器列表,新增定时任务,删除定时任务,停止定时任务,启动定时任务,修改定时任务,
手动触发定时任务。

1. xxl-job-admin调度中心

https://github.com/xuxueli/xxl-job.git

$ git clone https://github.com/xuxueli/xxl-job.git

1.1. 创建调度中心的表结构

数据库脚本地址:/xxl-job/doc/db/tables_xxl_job.sql

1.2. 修改 application.properties

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

默认用户名密码:admin/admin
email,task,task-quartz,xxl-job
email,task,task-quartz,xxl-job

2. 编写执行器项目

2.1. pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion><artifactId>spring-boot-demo-task-xxl-job</artifactId><version>1.0.0-SNAPSHOT</version><packaging>jar</packaging><parent><groupId>com.xkcoding</groupId><artifactId>spring-boot-demo</artifactId><version>1.0.0-SNAPSHOT</version></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version><xxl-job.version>2.1.0</xxl-job.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><!-- xxl-job-core --><dependency><groupId>com.xuxueli</groupId><artifactId>xxl-job-core</artifactId><version>${xxl-job.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies><build><finalName>spring-boot-demo-task-xxl-job</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></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;@Datapublic static class XxlJobAdminProps {/*** 调度中心地址*/private String address;}@Datapublic static class XxlJobExecutorProps {/*** 执行器名称*/private String appName;/*** 执行器 IP*/private String ip;/*** 执行器端口*/private int port;/*** 执行器日志*/private String logPath;/*** 执行器日志保留天数,-1*/private int logRetentionDays;}
}

2.3. 编写配置文件 application.yml

server:port: 8080servlet:context-path: /demo
xxl:job:# 执行器通讯TOKEN [选填]:非空时启用;access-token:admin:# 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;address: http://localhost:18080/xxl-job-adminexecutor:# 执行器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

2.4. 编写自动装配类 XxlConfig.java

@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;}
}

2.5. 编写具体的定时逻辑 DemoTask.java

@Slf4j
@Component
@JobHandler("demoTask")
public class DemoTask extends IJobHandler {/*** execute handler, invoked when executor receives a scheduling request** @param param 定时任务参数* @return 执行状态* @throws Exception 任务异常*/@Overridepublic 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;}
}

2.6. 启动执行器

Run SpringBootDemoTaskXxlJobApplication

3. 配置定时任务

3.1. 将启动的执行器添加到调度中心

执行器管理 - 新增执行器
email,task,task-quartz,xxl-job

3.2. 添加定时任务

任务管理 - 新增 - 保存
email,task,task-quartz,xxl-job

3.3. 启停定时任务

任务列表的操作列,拥有以下操作:执行、启动/停止、日志、编辑、删除
执行:单次触发任务,不影响定时逻辑
启动:启动定时任务
停止:停止定时任务
日志:查看当前任务执行日志
编辑:更新定时任务
删除:删除定时任务

4. 使用API添加定时任务

实际场景中,如果添加定时任务都需要手动在 xxl-job-admin 去操作,这样可能比较麻烦,用户更希望在自己的页面,添加定时任务参数、定时调度表达式,然后通过 API 的方式添加定时任务

4.1. 改造xxl-job-admin

4.1.1. 修改 JobGroupController.java

// 添加执行器列表
@RequestMapping("/list")
@ResponseBody
// 去除权限校验
@PermissionLimit(limit = false)
public ReturnT<List<XxlJobGroup>> list(){return  new ReturnT<>(xxlJobGroupDao.findAll());
}
...

4.1.2. 修改 JobInfoController.java

// 分别在 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();}}

参考

  • 《分布式任务调度平台xxl-job》