SpringBoot结合Quartz实现定时任务

news2024/11/27 8:25:56

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

工具

  • 比MyBatis Generator更强大的代码生成器

ORM框架选型

  • SpringBoot项目基础设施搭建
  • SpringBoot集成Mybatis项目实操
  • SpringBoot集成MybatisPlus项目实操
  • SpringBoot集成Spring Data JPA项目实操

数据库变更管理

  • 数据库变更管理:Liquibase or Flyway

  • SpringBoot结合Liquibase实现数据库变更管理

定时任务框架

  • Java定时任务技术分析
  • SpringBoot结合Quartz实现定时任务

缓存

  • 待更新

安全框架

  • 待更新

开发规范

  • 待更新

前言

需求

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

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将任务调度的运行信息保存在内存中,这种方法提供了最佳的性能,因为内存中数据访问最快。不足之处是缺乏数据的持久性,当程序路途停止或系统崩溃时,所有运行的信息都会丢失。

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

Quartz的两种存储方式区别

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

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表新增数据

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

Quartz执行定时任务

执行完定时任务后,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 (从原理到应用)详解篇(转)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/51718.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入讲解Netty那些事儿之从内核角度看IO模型(上)

我们都知道Netty是一个高性能异步事件驱动的网络框架。 它的设计异常优雅简洁&#xff0c;扩展性高&#xff0c;稳定性强。拥有非常详细完整的用户文档。 同时内置了很多非常有用的模块基本上做到了开箱即用&#xff0c;用户只需要编写短短几行代码&#xff0c;就可以快速构建…

8、python中的模块和包

文章目录模块模块导入的方式直接导入部分导入import module 和from module import *的区别模块的其他信息_ _ name _ _ 的特殊使用模块的分类包从包中导入模块的方式模块 模块就是工具包,要想使用这个工具包中的工具(就好比函数),就需要导入这个模块 模块是非常简单的Python文…

pandas交叉表与透视表pd.crosstab()和pd.pivot_table()函数详解

一、交叉表 交叉表&#xff1a;用于计算一列数据对于另外一列数据的分组个数(用于统计分组频率的特殊透视表)&#xff0c;pd.crosstab(value1, value2)pandas.crosstab(index, columns, valuesNone, rownamesNone, colnamesNone, aggfuncNone, marginsFalse, margins_nameAll,…

【虚幻引擎】UE4/UE5 动画蓝图,混合空间,目标偏移,动画蒙太奇之间的联系

一、UE动画介绍 虚幻引擎在为角色设置移动行走时&#xff0c;为了更好的调节和控制人物的相关动画&#xff0c;设置了一系列的跟人物相关的动画&#xff0c;其中包括一维混合空间&#xff0c;二维混合空间&#xff0c;动画蒙太奇&#xff0c;目标偏移等&#xff0c;动画蓝图的出…

Day16--加入购物车-动态设置tabBar的数组徽标

问题1&#xff1a; ①&#xff1a;刚开始 ②&#xff1a;点击购物车的图标后&#xff0c;跳转到cart页面发现&#xff0c;并没有徽标在tabbar上&#xff1a; 提纲挈领&#xff1a; 我的操作&#xff1a; 1》把 Store 中的 total 映射到 cart.vue 中使用&#xff1a; 2》在页面…

Java ArrayLIst与顺序表

什么是集合类&#xff1f; Java当中的集合类&#xff0c;其实就是封装号的数据结构 原始的数据结构——>Java当中封装成的集合对应的那个原始的数据结构——>用Java封装的集合对应的。 集合类所在的包&#xff1a;java.util这个包底下 顺序表的底层是一个数组&#xff0…

Flutter状态管理

前言 状态管理是什么&#xff1f;简单的来说&#xff0c;就是当某个状态发生变化的时候&#xff0c;告知该状态的监听者&#xff0c;让状态所监听的属性随之而改变&#xff0c;达到UI层随着数据层变化而变化的效果。在Flutter中的状态(State)是一个组件的UI数据模型&#xff0…

【MySQL 读写分离】Sharding JDBC + Spring boot 实现数据库读写分离的登录 Demo

上篇文章我们搭建了 MySQL 数据库主从复制集群 MySQL 搭建主从复制集群~~~ 本篇文章我们利用搭建好的主从复制集群&#xff0c;使用 SpringBoot 结合 Sharding-JDBC 搭建一个小的 登录 Demo&#xff0c;测试实现数据库的读写分离 项目源码地址&#xff1a; https://gitee.com/l…

13 【操作mysql数据库】

13 【操作mysql数据库】 1.mysql 介绍 付费的商用数据库&#xff1a; Oracle&#xff0c;典型的高富帅&#xff1b;SQL Server&#xff0c;微软自家产品&#xff0c;Windows定制专款&#xff1b;DB2&#xff0c;IBM的产品&#xff0c;听起来挺高端&#xff1b;Sybase&#x…

android WebRtc 视频通话(P2P)

概述 WebRTC名称源自网页实时通信(Web Real-Time Communication)的缩写&#xff0c;是一个支持网页浏览器进行实时语音对话或视频对话的技术&#xff0c;是谷歌2010年以6820万美元收购Global IP Solutions公司而获得的一项技术。Google于2011年6月3日开源的即时通讯项目&#x…

centos7中mysql5.7.32服务离线升级到5.7.39教程

目录 一、导入新的离线安装包 二、备份原有mysql数据库 1、停止tomcat服务 2、查看mysql服务 3、备份数据库 三、停止mysql服务并打包备份旧版本 1、停止mysql 2、打包旧的mysql文件夹 3、删除旧的mysql文件夹 4、删除/etc/init.d/下跟mysql有关的全部文件&#xff0…

MongoDB数据迁移之迁移工具Kettle

MongoDB数据迁移之迁移工具Kettle ETL:简介 ETL&#xff08;Extract-Transform-Load的缩写&#xff0c;即数据抽取、转换、装载的过程&#xff09;&#xff0c;对于企业或行业应用来说&#xff0c;我们经常会遇到各种数据的处理&#xff0c;转换&#xff0c;迁移&#xff0c;所…

Java+JSP+MySQL基于SSM的医院挂号就诊系统-计算机毕业设计

项目介绍 随着计算机科技的快速发展&#xff0c;很多地方都实现了自动化管理&#xff0c;医院也不例外。在大多数医院&#xff0c;无论是挂号处&#xff0c;还是取药的窗口&#xff0c;都会看到有很长的队伍&#xff0c;很显然这样会让患者就医的过程中浪费太多的时间。其次&a…

【读论文】GANMcC

GANMcC简单介绍网络结构生成器辨别器损失函数生成器损失函数辨别器tips总结参考论文&#xff1a;https://ieeexplore.ieee.org/document/9274337 如有侵权请联系博主 这几天又读了一篇关于GAN实现红外融合的论文&#xff0c;不出意外&#xff0c;还是FusionGAN作者团队的人写…

Python语音合成小工具(PyQt5 + pyttsx3)

TTS简介 TTS&#xff08;Text To Speech&#xff09;是一种语音合成技术&#xff0c;可以让机器将输入文本以语音的方式播放出来&#xff0c;实现机器说话的效果。 TTS分成语音处理及语音合成&#xff0c;先由机器识别输入的文字&#xff0c;再根据语音库进行语音合成。现在有…

JavaScript -- 三种循环语句的介绍及示例代码

文章目录循环语句1 While循环2 do-while循环3 for循环4 嵌套循环循环语句 通过循环语句可以使指定的代码反复执行 JS中一共有三种循环语句 while语句do-while语句for语句 通常编写一个循环&#xff0c;要有三个要件 初始化表达式&#xff08;初始化变量&#xff09;条件表…

风云气象卫星系列介绍

风云气象卫星系列是中国于1977年开始研制的气象卫星系列&#xff0c;目前发射了风云一号、风云二号、风云三号、风云四号等卫星。 风云一号 FY-1卫星分为两个批次&#xff0c;各两颗星。01批的FY-1A星于1988年7月9日发射&#xff0c;FY-1B星于1990年9月3日发射。02批卫星在01批…

Word处理控件Aspose.Words功能演示:在 Java 中将 Word 文档转换为 EPUB

大多数智能设备&#xff0c;如智能手机、平板电脑、笔记本电脑等&#xff0c;都支持EPUB格式来查看或阅读文档。它是电子书或电子出版物的常用格式。另一方面&#xff0c;MS Word 格式&#xff0c;如DOCX、DOC等&#xff0c;是数字世界中广泛使用的文档格式之一。在本文中&…

Web3中文|NFT无法保障数字所有权?

来源 | nftnow 编译 | DaliiNFTnews.com 2021年&#xff0c;有这样一个头条新闻&#xff1a;一家投资公司以大约400万美元的价格在The Sandbox上买下了2000英亩的虚拟地产。 通过在以太坊区块链上购买792个NFT&#xff0c;该公司得到了元宇宙平台上的1200个城市街区。 但是…

家用宽带如何叠加多条宽带,提高局域网速度

前言 关于多条宽带如何合并&#xff0c;使局域网内带宽更快&#xff1f;通常我们在企业网络或实际项目中&#xff0c;随着用户的增加&#xff0c;一条或者几条带宽不能满足正常使用&#xff0c;便可以对带宽进行叠加&#xff0c;便于网络带度更快&#xff1b; 一、为什么要用…