email,task,task-quartz,xxl-job

spring-boot-demo-email

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

pom.xml


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

application.yml

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

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

MailServiceTest.java

/**
 * 

* 邮件测试 *

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

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


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

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

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: 8080
  servlet:
    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


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

application.yml

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

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;

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

JobController.java

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

启动

email,task,task-quartz,xxl-job_第1张图片
email,task,task-quartz,xxl-job_第2张图片
email,task,task-quartz,xxl-job_第3张图片
email,task,task-quartz,xxl-job_第4张图片
email,task,task-quartz,xxl-job_第5张图片

参考

  • 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 字段)
    email,task,task-quartz,xxl-job_第6张图片
  • 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_第7张图片
email,task,task-quartz,xxl-job_第8张图片

2. 编写执行器项目

2.1. pom.xml


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

2.3. 编写配置文件 application.yml

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

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 任务异常
     */
    @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;
    }
}

2.6. 启动执行器

Run SpringBootDemoTaskXxlJobApplication

3. 配置定时任务

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

执行器管理 - 新增执行器
email,task,task-quartz,xxl-job_第9张图片

3.2. 添加定时任务

任务管理 - 新增 - 保存
email,task,task-quartz,xxl-job_第10张图片

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(){
        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》

你可能感兴趣的:(java,spring,boot,spring)