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

东西

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

ORM结构选型

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

数据库改动办理

  • 数据库改动办理:Liquibase or Flyway
  • SpringBoot结合Liquibase完成数据库改动办理

守时使命结构

  • Java守时使命技术分析
  • SpringBoot结合Quartz完成守时使命
  • SpringBoot结合XXL-JOB完成守时使命

缓存

  • Spring Security结合Redis完成缓存功用

安全结构

  • Java运用程序安全结构
  • Spring Security系列文章
  • Spring Security结合JWT完成认证与授权

开发规范

  • 后端必知:遵从Google Java规范并引进checkstyle查看

前言

需求

假定咱们有这样两个需求:

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将使命调度的运转信息保存在内存中,这种办法供给了最佳的性能,由于内存中数据拜访最快。不足之处是缺少数据的耐久性,当程序路途中止或系统溃散时,一切运转的信息都会丢掉。

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

SpringBoot结合Quartz实现定时任务

JDBCJobStore 存储能够完成 Quartz 集群形式,实际场景下,咱们必然需要考虑守时使命的高可用,即选用集群形式。

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

SpringBoot结合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 来注册用户

SpringBoot结合Quartz实现定时任务

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

SpringBoot结合Quartz实现定时任务

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

SpringBoot结合Quartz实现定时任务

履行完守时使命后,qrtz_job_details 表中相关数据也会被删除去。

接着来测验发送气候告诉

SpringBoot结合Quartz实现定时任务

由于咱们测验的是每两分钟跑一次守时使命,所以 qrtz_job_details 表中会一向存在这么一条数据:

SpringBoot结合Quartz实现定时任务

问题记载

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 (从原理到运用)详解篇(转)