草庐IT

SpringBoot结合Quartz实现定时任务

hresh 2023-04-13 原文

《从零打造项目》系列文章

工具

ORM框架选型

数据库变更管理

定时任务框架

缓存

  • 待更新

安全框架

  • 待更新

开发规范

  • 待更新

前言

需求

假设我们有这样两个需求:

1、用户注册1分钟后给用户发送欢迎通知。

2、每天8点钟给用户发送当天温度通知。

接下来我们就准备实现上述两个需求,关于通知发送就只是简单地控制台输出,没有真正实现该功能。

关于定时任务框架的选择,本文将选用 Quartz 来实现上述需求,下面简单介绍一下 Quartz。

Quartz介绍

Quartz 作为一个优秀的开源调度框架,Quartz 具有以下特点:

  1. 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
  2. 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
  3. 分布式和集群能力,Terracotta 收购后在原来功能基础上作了进一步提升。

另外,作为 Spring 默认的调度框架,Quartz 很容易与 Spring 集成实现灵活可配置的调度功能。

在 Quartz 体系结构中,有三个组件非常重要:

  • Scheduler :调度器。Scheduler启动Trigger去执行Job。
  • Trigger :触发器。用来定义 Job(任务)触发条件、触发时间,触发间隔,终止时间等。四大类型:SimpleTrigger(简单的触发器)、CornTrigger(Cron表达式触发器)、DateIntervalTrigger(日期触发器)、CalendarIntervalTrigger(日历触发器)。
  • Job :任务。具体要执行的业务逻辑,比如:发送短信、发送邮件、访问数据库、同步数据等。

Quartz集群

Quartz 的存储方式有两种:RAMJobStoreJDBCJobStore。从名字就能看出,存在内存中和存在数据库当中。在默认情况下Quartz将任务调度的运行信息保存在内存中,这种方法提供了最佳的性能,因为内存中数据访问最快。不足之处是缺乏数据的持久性,当程序路途停止或系统崩溃时,所有运行的信息都会丢失。

两者之间的区别如下图所示:

JDBCJobStore 存储可以实现 Quartz 集群模式,实际场景下,我们必然需要考虑定时任务的高可用,即选用集群模式。

Quartz 集群架构如下,集群中的每个节点是一个独立的 Quartz 应用,且独立的 Quartz 节点并不与另一节点通信,而是通过相同的数据库表来感知另一 Quartz 应用。简而言之,Quartz 应用、数据库支撑、多节点部署即可搭建起Quartz的应用集群。

**Quartz 集群共用同一个数据库,由数据库中的数据来确定任务是否正在执行,如果该任务正在执行,则其他服务器就不能去执行该调度任务。**Quartz集群的特点如下:

1、持久化

Quartz 可以将调度器 scheduler、触发器 trigger 以及任务 Job 的运行时信息存储至数据库中,采用 JDBCJobStore,如果服务器异常时,可以基于数据库中的存储信息进行任务恢复。

2、高可用性

如果相关服务器节点挂掉的话,集群的其他节点则会继续执行相关任务。

3、伸缩性

如果集群中的节点数过少,导致相关任务无法及时执行,可以增加额外的服务器节点,只需将其他节点上的脚本及配置信息拷贝至新部署的节点上运行即可。

4、负载均衡

Quartz 使用随机的负载均衡算法,任务 job 是以随机的方式由不同的节点上 Scheduler 实例来执行。但当前不存在一个方法指派一个Job到集群中的特定节点。

下面我们就使用 Quartz 来实现定时任务推送。

项目实践

创建一个 Maven 项目,名为 quartz-task。

环境配置

1、在 pom.xml 文件中,引入相关依赖。

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.6.3</version>
  <relativePath/>
</parent>

<properties>
  <java.version>1.8</java.version>
  <fastjson.version>1.2.73</fastjson.version>
  <hutool.version>5.5.1</hutool.version>
  <mysql.version>8.0.19</mysql.version>
  <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
  <org.projectlombok.version>1.18.20</org.projectlombok.version>
  <druid.version>1.1.18</druid.version>
  <springdoc.version>1.6.9</springdoc.version>
  <liquibase.version>4.16.1</liquibase.version>
</properties>

<dependencies>
  <!-- 实现对 Spring MVC 的自动化配置 -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
  </dependency>
  <!-- 实现对 Quartz 的自动化配置 -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
  </dependency>
  <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
  </dependency>
  <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus</artifactId>
    <version>3.5.1</version>
  </dependency>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>${mysql.version}</version>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>${druid.version}</version>
  </dependency>
  <dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
    <version>4.16.1</version>
  </dependency>

  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
  </dependency>
  <dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.12</version>
  </dependency>
  <dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>${org.mapstruct.version}</version>
  </dependency>
  <dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>${org.mapstruct.version}</version>
  </dependency>
  <dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>${hutool.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>${springdoc.version}</version>
  </dependency>

</dependencies>

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    <plugin>
      <groupId>org.liquibase</groupId>
      <artifactId>liquibase-maven-plugin</artifactId>
      <version>4.16.1</version>
      <configuration>
        <!--properties文件路径,该文件记录了数据库连接信息等-->
        <propertyFile>src/main/resources/application.yml</propertyFile>
        <propertyFileWillOverride>true</propertyFileWillOverride>
      </configuration>
    </plugin>
    <plugin>
      <groupId>com.msdn.hresh</groupId>
      <artifactId>liquibase-changelog-generate</artifactId>
      <version>1.0-SNAPSHOT</version>
      <configuration>
        <sourceFolderPath>src/main/resources/liquibase/changelogs/
        </sourceFolderPath><!-- 当前应用根目录 -->
      </configuration>
    </plugin>
  </plugins>
</build>

2、添加 application.yml

server:
  port: 8080

spring:
  application:
    name: quartz-task
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/quartz_test_db?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false
    username: root
    password: root
  liquibase:
    enabled: true
    change-log: classpath:liquibase/master.xml
    # 记录版本日志表
    database-change-log-table: databasechangelog
    # 记录版本改变lock表
    database-change-log-lock-table: databasechangeloglock
  quartz:
    # 程序结束时会等待quartz相关的内容结束
    wait-for-jobs-to-complete-on-shutdown: true
    # 将任务等保存化到数据库
    job-store-type: jdbc
    # QuartzScheduler启动时覆盖己存在的Job
    overwrite-existing-jobs: false

mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    lazy-loading-enabled: true

changeLogFile: src/main/resources/liquibase/master.xml

3、关于 Quartz 的配置,可以一并写在 application.yml 中,类似于这样:

spring:
  datasource:
    user:
      url: jdbc:mysql://127.0.0.1:3306/quartz_test_db?useSSL=false&useUnicode=true&characterEncoding=UTF-8
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password:
    quartz:
      url: jdbc:mysql://127.0.0.1:3306/quartz_test_db?useSSL=false&useUnicode=true&characterEncoding=UTF-8
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password:

  # Quartz 的配置,对应 QuartzProperties 配置类
  quartz:
    scheduler-name: clusteredScheduler # Scheduler 名字。默认为 schedulerName
    job-store-type: jdbc # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。
    wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true
    overwrite-existing-jobs: false # 是否覆盖已有 Job 的配置
    properties: # 添加 Quartz Scheduler 附加属性,更多可以看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html 文档
      org:
        quartz:
          # JobStore 相关配置
          jobStore:
            # 数据源名称
            dataSource: quartzDataSource # 使用的数据源
            class: org.quartz.impl.jdbcjobstore.JobStoreTX # JobStore 实现类
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_ # Quartz 表前缀
            isClustered: true # 是集群模式
            clusterCheckinInterval: 1000
            useProperties: false
          # 线程池相关配置
          threadPool:
            threadCount: 25 # 线程池大小。默认为 10 。
            threadPriority: 5 # 线程优先级
            class: org.quartz.simpl.SimpleThreadPool # 线程池类型

不过因为 Quartz 配置内容过多,所以单独新建了 quartz.properties。

org.quartz.jobStore.useProperties=true

#在集群中每个实例都必须有一个唯一的instanceId,但是应该有一个相同的instanceName【默认“QuartzScheduler”】【非必须】
org.quartz.scheduler.instanceName=quartzScheduler
# Scheduler实例ID,全局唯一
org.quartz.scheduler.instanceId=AUTO
# 指定scheduler的主线程是否为后台线程,【默认false】【非必须】
org.quartz.scheduler.makeSchedulerThreadDaemon=true
# 触发job时是否需要拥有锁
org.quartz.jobStore.acquireTriggersWithinLock = true

#线程池配置
#线程池类型
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
#线程池大小
org.quartz.threadPool.threadCount=10
#线程优先级
org.quartz.threadPool.threadPriority=5

#============================================================================
# Configure JobStore
#============================================================================
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.tablePrefix=qrtz_
# 最大能忍受的触发超时时间(触发器被认定为“misfired”之前),如果超过则认为“失误”【默认60秒】
org.quartz.jobStore.misfireThreshold = 60000
# 配置数据源的名称,在后面配置数据源的时候要用到,
# 例如org.quartz.dataSource.myDS.driver=com.mysql.cj.jdbc.Driver
org.quartz.jobStore.dataSource = myDS

# 集群配置
org.quartz.jobStore.isClustered = true
# 检入到数据库中的频率(毫秒)。检查是否其他的实例到了应当检入的时候未检入这能指出一个失败的实例,
# 且当前Scheduler会以此来接管执行失败并可恢复的Job通过检入操作,Scheduler也会更新自身的状态记录
org.quartz.jobStore.clusterCheckinInterval=5000
# jobStore处理未按时触发的Job的数量
org.quartz.jobStore.maxMisfiresToHandleAtATime=20

# datasource
org.quartz.dataSource.myDS.provider = hikaricp
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.dataSource.myDS.driver=com.mysql.cj.jdbc.Driver
org.quartz.dataSource.myDS.URL=jdbc:mysql://localhost:3306/quartz_test_db?characterEncoding=utf8
org.quartz.dataSource.myDS.user=root
org.quartz.dataSource.myDS.password=root
# 最大连接数
org.quartz.dataSource.myDS.maxConnections = 10
# dataSource用于检测connection是否failed/corrupt的SQL语句
org.quartz.dataSource.myDS.validationQuery = select 1

关于 properties 文件中每个属性的含义,推荐阅读《Quartz配置文件详解&生产配置》。

4、手动在数据库中创建 Quartz 相关表,可以从 Quartz 发行版下载中找到tables_mysql.sql ,或直接从其源代码中找到 。因为我们使用 MySQL ,所以使用 tables_mysql_innodb.sql 脚本。

核心类

1、Quartz 配置类

@Configuration
public class SchedulerConfig {

  @Bean
  public SchedulerFactoryBean scheduler(DataSource dataSource) {
    SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
    schedulerFactory.setConfigLocation(new ClassPathResource("quartz.properties"));
    schedulerFactory.setDataSource(dataSource);
    schedulerFactory.setJobFactory(new SpringBeanJobFactory());
    schedulerFactory.setApplicationContextSchedulerContextKey("applicationContext");
    return schedulerFactory;
  }

}

2、Quartz 相关实体类

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ScheduleTask {

  // 任务名
  private String jobName;

  // 任务组
  private String groupName;

  // 任务数据
  private String jobData;

  // 任务执行处理类,小写字母开头
  private String jobHandlerClass;

  // 任务执行时间
  private Long jobTime;

  // 任务执行时间,cron时间表达式 (如:0/5 * * * * ? )
  private String jobCronTime;

  // 任务执行次数,(<0:表示不限次数)
  private int jobTimes;

}

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class JobResponse {

  // 任务名
  private String jobName;

  // 任务组
  private String groupName;

  // 任务数据
  private String jobData;

  private String triggerKey;

  private String jobStatus;

  // 任务执行时间,cron时间表达式 (如:0/5 * * * * ? )
  private String jobCronTime;

}

3、QuartzService定时器操作

@Service
@RequiredArgsConstructor
public class QuartzTaskService {

  public static final String JOB_DATA_KEY = "jobData";
  public static final String JOB_HANDLER_CLASS_KEY = "jobHandlerClass";
  private final Scheduler scheduler;

  public void createJob(ScheduleTask task) throws SchedulerException {
    JobDetail jobDetail = JobBuilder.newJob().ofType(MessageJob.class)
        .withIdentity(task.getJobName(), task.getGroupName())
        .usingJobData(JOB_DATA_KEY, task.getJobData())
        .usingJobData(JOB_HANDLER_CLASS_KEY, task.getJobHandlerClass())
        .build();
    Trigger trigger;
    if (StrUtil.isNotBlank(task.getJobCronTime())) {
      trigger = TriggerBuilder.newTrigger().forJob(jobDetail)
          .withIdentity(task.getJobName() + "_trigger", task.getGroupName())
          .withSchedule(CronScheduleBuilder.cronSchedule(task.getJobCronTime())).build();
    } else {
      trigger = TriggerBuilder.newTrigger().forJob(jobDetail)
          .withIdentity(task.getJobName() + "_trigger", task.getGroupName())
          .startAt(new Date(task.getJobTime()))
          .build();
    }

    scheduler.scheduleJob(jobDetail, trigger);
  }


  // 修改 一个job的 时间表达式
  @SneakyThrows
  public void updateJob(String jobName, String jobGroupName, String jobTime) {
    TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroupName);
    CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
    trigger = trigger.getTriggerBuilder().withIdentity(triggerKey)
        .withSchedule(CronScheduleBuilder.cronSchedule(jobTime)).build();
    // 重启触发器
    scheduler.rescheduleJob(triggerKey, trigger);
  }

  @SneakyThrows
  public void removeTask(JobKey jobKey) {
    scheduler.deleteJob(jobKey);
  }

  // 暂停一个job
  @SneakyThrows
  public void pauseJob(JobKey jobKey) {
    scheduler.pauseJob(jobKey);
  }

  // 恢复一个job
  @SneakyThrows
  public void resumeJob(JobKey jobKey) {
    scheduler.resumeJob(jobKey);
  }

  // 立即执行一个job
  @SneakyThrows
  public void runJobNow(JobKey jobKey) {
    scheduler.triggerJob(jobKey);
  }

  // 获取所有计划中的任务列表
  public List<JobResponse> queryAllJob() throws SchedulerException {
    List<JobResponse> jobResponses = new ArrayList<>();
    GroupMatcher<JobKey> matcher = GroupMatcher.anyJobGroup();
    Set<JobKey> jobKeys = scheduler.getJobKeys(matcher);
    for (JobKey jobKey : jobKeys) {
      List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
      for (Trigger trigger : triggers) {
        Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
        JobResponse jobResponse = getJobResponse(jobKey, trigger, triggerState);
        jobResponses.add(jobResponse);
      }
    }
    return jobResponses;
  }

  private JobResponse getJobResponse(JobKey jobKey, Trigger trigger, TriggerState triggerState) {
    JobResponse jobResponse = JobResponse.builder().jobName(jobKey.getName())
        .groupName(jobKey.getGroup()).triggerKey(trigger.getKey().toString()).build();
    jobResponse.setJobStatus(triggerState.name());
    if (trigger instanceof CronTrigger) {
      CronTrigger cronTrigger = (CronTrigger) trigger;
      String cronExpression = cronTrigger.getCronExpression();
      jobResponse.setJobCronTime(cronExpression);
    }
    return jobResponse;
  }

  // 获取所有正在运行的job
  public List<JobResponse> queryRunJob() throws SchedulerException {
    List<JobResponse> jobResponses = new ArrayList<>();
    List<JobExecutionContext> executingJobs = scheduler.getCurrentlyExecutingJobs();
    for (JobExecutionContext executingJob : executingJobs) {
      JobDetail jobDetail = executingJob.getJobDetail();
      JobKey jobKey = jobDetail.getKey();
      Trigger trigger = executingJob.getTrigger();
      Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
      JobResponse jobResponse = getJobResponse(jobKey, trigger, triggerState);
      jobResponses.add(jobResponse);
    }
    return jobResponses;
  }
}

4、自定义 Job 类

@Setter
@Slf4j
public class MessageJob implements Job {

  private ApplicationContext applicationContext;

  private String jobData;

  private String jobHandlerClass;

  @SneakyThrows
  @Override
  public void execute(JobExecutionContext context) {
    log.info("quartz job data: " + jobData + ", jobHandlerClass: " + jobHandlerClass);
    MessageHandler messageHandler = (MessageHandler) applicationContext.getBean(jobHandlerClass);
    messageHandler.handlerMessage(jobData);
  }
}

5、创建定时任务处理接口

public interface MessageHandler {

  void handlerMessage(String jobData) throws JsonProcessingException;

}

不同的定时任务对应不同的任务处理类,即实现 MessageHandler 接口。

业务实现

1、UserService,包括用户注册,给用户发送欢迎消息,以及发送天气温度通知。

@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {

  private final ScheduleTaskService scheduleTaskService;
  private final UserMapper userMapper;
  private final UserStruct userStruct;
  private final WeatherService weatherService;

  /**
   * 假设有这样一个业务需求,每当有新用户注册,则1分钟后会给用户发送欢迎通知.
   *
   * @param userRequest 用户请求体
   */
  public void register(UserRequest userRequest) {
    if (Objects.isNull(userRequest) || isBlank(userRequest.getUsername()) ||
        isBlank(userRequest.getPassword())) {
      BusinessException.fail("账号或密码为空!");
    }

    User user = userStruct.toUser(userRequest);
    userMapper.insert(user);

    scheduleTaskService.createTask(user.getUsername());
  }


  public void sayHelloToUser(String username) {
    User user = userMapper.selectByUserName(username);
    String message = "Welcome to Java,I am hresh.";
    log.info(user.getUsername() + " , hello, " + message);
  }


  public void pushWeatherNotification(List<User> users) {
    log.info("执行发送天气通知给用户的任务。。。");
    WeatherInfo weatherInfo = weatherService.getWeather(WeatherConstant.WU_HAN);
    for (User user : users) {
      log.info(user.getUsername() + "----" + weatherInfo.toString());
    }
  }
}

2、ScheduleTaskService 创建定时任务

@Service
@RequiredArgsConstructor
public class ScheduleTaskService {

  private final QuartzTaskService quartzTaskService;
  private final UserMapper userMapper;

  @SneakyThrows
  public void createTask(String username) {
    LocalDateTime scheduleTime = LocalDateTime.now().plusMinutes(1L);
    String jobName = "sayHello";
    String jogGroupName = "group1";
    String jobHandlerClass = "sayHelloHandler";

    ScheduleTask scheduleTask = ScheduleTask.builder()
        .jobName(jobName)
        .groupName(jogGroupName)
        .jobData(new ObjectMapper().writeValueAsString(username))
        .jobHandlerClass(jobHandlerClass)
        .jobTime(DateUtil.toEpochMilli(scheduleTime))
        .build();

    quartzTaskService.createJob(scheduleTask);
  }

  @SneakyThrows
  public void createWeatherNotificationTask(String jobTime) {
    String jobName = "weatherNotification";
    String jogGroupName = "group2";
    String jobHandlerClass = "weatherNotificationHandler";

    List<User> users = userMapper.queryAll();

    ScheduleTask scheduleTask = ScheduleTask.builder()
        .jobName(jobName)
        .groupName(jogGroupName)
        .jobData(JSON.toJSONString(users))
        .jobHandlerClass(jobHandlerClass)
        .jobCronTime(jobTime)
        .build();

    quartzTaskService.createJob(scheduleTask);
  }

}

3、WeatherService,获取天气温度等信息

@Service
@RequiredArgsConstructor
public class WeatherService {

  private final RestTemplate restTemplate;

  public WeatherInfo getWeather(WeatherConstant weatherConstant) {
    String json = restTemplate
        .getForObject("http://t.weather.sojson.com/api/weather/city/" + weatherConstant.getCode()
            , String.class);
    JSONObject jsonObject = JSONObject.parseObject(json);
    Integer status = jsonObject.getInteger("status");

    String currentDay = DateUtil.getDay(LocalDateTime.now());

    if (status == 200) {
      JSONObject data = jsonObject.getJSONObject("data");
      String quality = data.getString("quality");
      String notice = data.getString("ganmao");
      String currentTemperature = data.getString("wendu");

      JSONArray forecast = data.getJSONArray("forecast");
      JSONObject dayInfo = forecast.getJSONObject(0);
      String high = dayInfo.getString("high");
      String low = dayInfo.getString("low");
      String weather = dayInfo.getString("type");
      String windDirection = dayInfo.getString("fx");

      return WeatherInfo.builder().airQuality(quality + "污染").date(currentDay)
          .cityName(weatherConstant.getCityName()).temperature(low + "-" + high).weather(weather)
          .windDirection(windDirection).notice(notice).currentTemperature(currentTemperature)
          .build();
    }
    return null;
  }

}

4、UserController,对外暴露接口

@RestController
@RequiredArgsConstructor
public class UserController {

  private final UserService userService;
  private final ScheduleTaskService scheduleTaskService;

  @PostMapping("/register")
  public Result<Object> register(@RequestBody UserRequest userRequest) {
    userService.register(userRequest);
    return Result.ok();
  }


  @PostMapping("/weather-notification")
  public Result<Object> scheduledSayHello(@RequestParam("jobTime") String jobTime) {
    scheduleTaskService.createWeatherNotificationTask(jobTime);
    return Result.ok();
  }

}

还有一些代码没有展示出来,感兴趣的朋友到时候可以去我的 Github 上看一下项目源码。

测试

为了演示效果,发送天气温度通知,我们暂时设为每2分钟一次。

首先通过 postman 来注册用户

可以到数据库中看一下 qrtz_job_details 表中的数据,如下所示:

等待一分钟后,控制台会输出如下内容:

执行完定时任务后,qrtz_job_details 表中相关数据也会被删除掉。

接着来测试发送天气通知

因为咱们测试的是每两分钟跑一次定时任务,所以 qrtz_job_details 表中会一直存在这么一条数据:

问题记录

1、初次启动定时任务报错

Couldn't acquire next trigger: Unknown column 'SCHED_TIME' in 'field list'

原因:我们下载的 SQL 文件有问题,在 qrtz_fired_triggers 表的构建语句中缺少 sched_time 字段,完整的 SQL 语句如下:

create table qrtz_fired_triggers
  (
    sched_name varchar(120) not null,
    entry_id varchar(95) not null,
    trigger_name varchar(200) not null,
    trigger_group varchar(200) not null,
    instance_name varchar(200) not null,
    fired_time bigint(13) not null,
    sched_time bigint(13) not null,
    priority integer not null,
    state varchar(16) not null,
    job_name varchar(200) null,
    job_group varchar(200) null,
    is_nonconcurrent varchar(1) null,
    requests_recovery varchar(1) null,
    primary key (sched_name,entry_id)
);

2、增加数据源配置过程中遇到的坑:

在 quartz.properties 文件中没有增加 org.quartz.dataSource.myDS.provider = hikaricp 配置时,启动一直报错:

Caused by: org.quartz.SchedulerException: Could not initialize DataSource: qzDS

Caused by: org.quartz.SchedulerException: ConnectionProvider class ‘org.quartz.utils.C3p0PoolingConnectionProvider’ could not be instantiated.

后来增加了 provider: hikaricp 这个配置,启动不报错。

总结

Quartz 框架出现的比较早,后续不少定时框架,或多或少都基于 Quartz 研发的,比如当当网的elastic-job就是基于quartz二次开发之后的分布式调度解决方案。

并且,Quartz 并没有内置 UI 管理控制台,不过你可以使用 quartzui 这个开源项目来解决这个问题。

虽然 Quartz 可以实现我们的需求,但代码入侵比较严重,使用起来比较麻烦,后续我们再研究一下其他的定时任务框架。

感兴趣的朋友可以去我的 Github 下载相关代码,如果对你有所帮助,不妨 Star 一下,谢谢大家支持!

参考文献

Quartz配置文件详解&生产配置

Quartz.NET Configuration Reference

springBoot整合Quartz定时任务(持久化到数据库,开箱即食)

Quartz学习总结之Job存储模式和集群

Quartz应用与集群原理分析

quartz (从原理到应用)详解篇(转)

有关SpringBoot结合Quartz实现定时任务的更多相关文章

  1. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  2. ruby-on-rails - 结合 meta_search 与 acts_as_taggable_on - 2

    我在开发的Rails3网站的一些搜索功能上遇到了一个小问题。我有一个简单的Post模型,如下所示:classPost我正在使用acts_as_taggable_on来更轻松地向我的帖子添加标签。当我有一个标记为“rails”的帖子并执行以下操作时,一切正常:@posts=Post.tagged_with("rails")问题是,我还想搜索帖子的标题。当我有一篇标题为“Helloworld”并标记为“rails”的帖子时,我希望能够通过搜索“hello”或“rails”来找到这篇帖子。因此,我希望标题列的LIKE语句与acts_as_taggable_on提供的tagged_with方法

  3. ruby - 如何使用 RSpec::Core::RakeTask 创建 RSpec Rake 任务? - 2

    如何使用RSpec::Core::RakeTask初始化RSpecRake任务?require'rspec/core/rake_task'RSpec::Core::RakeTask.newdo|t|#whatdoIputinhere?endInitialize函数记录在http://rubydoc.info/github/rspec/rspec-core/RSpec/Core/RakeTask#initialize-instance_method没有很好的记录;它只是说:-(RakeTask)initialize(*args,&task_block)AnewinstanceofRake

  4. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  5. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  6. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  7. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  8. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  9. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

  10. ruby-on-rails - Rake 任务仅调用一次时执行两次 - 2

    我写了一个非常简单的rake任务来尝试找到这个问题的根源。namespace:foodotaskbar::environmentdoputs'RUNNING'endend当在控制台中执行rakefoo:bar时,输出为:RUNNINGRUNNING当我执行任何rake任务时会发生这种情况。有没有人遇到过这样的事情?编辑上面的rake任务就是写在那个.rake文件中的所有内容。这是当前正在使用的Rakefile。requireFile.expand_path('../config/application',__FILE__)OurApp::Application.load_tasks这里

随机推荐