南京做网站建设有哪些企业网站推广的一般策略
前言:
在企业项目中,往往有定时任务发布的需求,比如每天晚9点将今日数据备份一次,或每月一号将上月的销售数据邮件发送给对应的工作人员。显然这些操作不可能是人工到时间点调用一次接口,需要编写专门的模块完成任务的调度。
Quartz 是一个功能强大的开源任务调度框架,广泛应用于 Java 应用程序中。它允许开发者根据时间表(如固定时间、间隔时间或 Cron 表达式)来调度任务。Quartz 提供了丰富的 API 和灵活的配置选项,适用于从简单的任务调度到复杂的分布式任务调度场景。
尽管quartz库本身提供很多方法,但在定时任务中,外界往往只能感知到任务的启动与暂停,以及原始quartz库对于每一种定时任务的实现都需要新添加一种实现接口,不方便拓展的同时程序的耦合度较高。经过前面两章的迭代已经成功开发了数据库持久化、便于维护、日志记录的定时任务模块,具体可参考下方链接,本章将介绍如何将模块整合至微服务项目中。
从零搭建微服务项目Pro(第1-2章——Quartz实现定时任务模块优化)-CSDN博客https://blog.csdn.net/wlf2030/article/details/145933078?spm=1001.2014.3001.5501
本章最终源码链接如下:
wlf728050719/SpringCloudPro1-3https://github.com/wlf728050719/SpringCloudPro1-3以及本专栏会持续更新微服务项目,每一章的项目都会基于前一章项目进行功能的完善,欢迎小伙伴们关注!同时如果只是对单章感兴趣也不用从头看,只需下载前一章项目即可,每一章都会有前置项目准备部分,跟着操作就能实现上一章的最终效果,当然如果是一直跟着做可以直接跳过这一部分。专栏目录链接如下,其中Base篇为基础微服务搭建,Pro篇为复杂模块实现。
从零搭建微服务项目(全)-CSDN博客https://blog.csdn.net/wlf2030/article/details/145799620
一、Quartz系统介绍
核心概念
-
Job: 定义要执行的任务,实现
Job
接口并重写execute
方法。 -
JobDetail: 包含
Job
的详细信息,如名称、组名等。 -
Trigger: 定义任务的触发条件,如开始时间、结束时间、重复频率等。
-
Scheduler: 负责调度任务,将
JobDetail
和Trigger
绑定并管理任务执行。 -
JobDataMap: 任务数据映射,用于在任务执行时传递数据。
底层实现
-
任务存储
Quartz 支持内存存储(RAMJobStore)和数据库存储(JDBCJobStore)。RAMJobStore 将任务存储在内存中,适合简单的应用场景;JDBCJobStore 将任务存储在数据库中,适合分布式和持久化的场景。 -
线程池
Quartz 使用线程池来执行任务。默认情况下,Quartz 使用SimpleThreadPool
,开发者可以配置线程池的大小以适应不同的负载。 -
触发器调度
Quartz 使用QuartzSchedulerThread
来检查触发器并根据时间表调度任务。该线程会定期检查触发器,并根据触发器的状态决定是否执行任务。 -
任务执行
当触发器触发时,Quartz 会从线程池中获取一个线程来执行任务。任务执行过程中,Quartz 会处理任务的并发性、异常处理等。
Scheduler
是 Quartz 框架的核心接口,负责管理和调度任务(Job)和触发器(Trigger)。以下是 Scheduler
接口中常用的方法及其说明:
1. 调度器生命周期管理
这些方法用于启动、暂停、恢复和关闭调度器。
方法名 | 说明 |
---|---|
void start() | 启动调度器,开始执行任务。调度器启动后,触发器会根据时间表触发任务。 |
void startDelayed(int seconds) | 延迟指定秒数后启动调度器。 |
void standby() | 暂停调度器,暂停后触发器不会触发任务,但任务和触发器的状态会保留。 |
void shutdown() | 关闭调度器,停止所有任务执行。 |
void shutdown(boolean waitForJobsToComplete) | 关闭调度器,并决定是否等待正在执行的任务完成。true 表示等待任务完成后再关闭。 |
2. 任务和触发器管理
这些方法用于添加、删除、暂停、恢复任务和触发器。
方法名 | 说明 |
---|---|
void scheduleJob(JobDetail jobDetail, Trigger trigger) | 将任务(JobDetail)和触发器(Trigger)绑定,并添加到调度器中。 |
Date scheduleJob(Trigger trigger) | 添加触发器,并返回触发器首次触发的时间。 |
void addJob(JobDetail jobDetail, boolean replace) | 添加任务到调度器中。replace 为 true 时,如果任务已存在则替换。 |
boolean deleteJob(JobKey jobKey) | 根据任务键(JobKey)删除任务。返回 true 表示删除成功。 |
boolean deleteJobs(List<JobKey> jobKeys) | 批量删除任务。 |
void pauseJob(JobKey jobKey) | 暂停指定任务。 |
void pauseJobs(GroupMatcher<JobKey> matcher) | 暂停匹配指定组的所有任务。 |
void resumeJob(JobKey jobKey) | 恢复指定任务。 |
void resumeJobs(GroupMatcher<JobKey> matcher) | 恢复匹配指定组的所有任务。 |
void pauseTrigger(TriggerKey triggerKey) | 暂停指定触发器。 |
void pauseTriggers(GroupMatcher<TriggerKey> matcher) | 暂停匹配指定组的所有触发器。 |
void resumeTrigger(TriggerKey triggerKey) | 恢复指定触发器。 |
void resumeTriggers(GroupMatcher<TriggerKey> matcher) | 恢复匹配指定组的所有触发器。 |
3. 任务和触发器查询
这些方法用于查询调度器中的任务和触发器。
方法名 | 说明 |
---|---|
JobDetail getJobDetail(JobKey jobKey) | 根据任务键(JobKey)获取任务详情(JobDetail)。 |
List<? extends Trigger> getTriggersOfJob(JobKey jobKey) | 获取与指定任务关联的所有触发器。 |
Trigger getTrigger(TriggerKey triggerKey) | 根据触发器键(TriggerKey)获取触发器。 |
Set<JobKey> getJobKeys(GroupMatcher<JobKey> matcher) | 获取匹配指定组的所有任务键。 |
Set<TriggerKey> getTriggerKeys(GroupMatcher<TriggerKey> matcher) | 获取匹配指定组的所有触发器键。 |
SchedulerMetaData getMetaData() | 获取调度器的元数据,如调度器名称、版本、运行状态等。 |
4. 任务和触发器状态检查
这些方法用于检查任务和触发器的状态。
方法名 | 说明 |
---|---|
boolean checkExists(JobKey jobKey) | 检查指定任务是否存在。 |
boolean checkExists(TriggerKey triggerKey) | 检查指定触发器是否存在。 |
boolean isJobGroupPaused(String groupName) | 检查指定任务组是否处于暂停状态。 |
boolean isTriggerGroupPaused(String groupName) | 检查指定触发器组是否处于暂停状态。 |
5. 任务和触发器操作
这些方法用于直接操作任务和触发器。
方法名 | 说明 |
---|---|
void triggerJob(JobKey jobKey) | 立即触发指定任务。 |
void triggerJob(JobKey jobKey, JobDataMap data) | 立即触发指定任务,并传递额外的任务数据(JobDataMap)。 |
void interrupt(JobKey jobKey) | 中断正在执行的任务。 |
boolean unscheduleJob(TriggerKey triggerKey) | 移除指定触发器。 |
boolean unscheduleJobs(List<TriggerKey> triggerKeys) | 批量移除触发器。 |
6. 调度器状态检查
这些方法用于检查调度器的运行状态。
方法名 | 说明 |
---|---|
boolean isStarted() | 检查调度器是否已启动。 |
boolean isShutdown() | 检查调度器是否已关闭。 |
boolean isInStandbyMode() | 检查调度器是否处于暂停状态(standby mode)。 |
7. 调度器监听器管理
这些方法用于管理调度器的监听器。
方法名 | 说明 |
---|---|
void addJobListener(JobListener listener) | 添加任务监听器。 |
void addTriggerListener(TriggerListener listener) | 添加触发器监听器。 |
void addSchedulerListener(SchedulerListener listener) | 添加调度器监听器。 |
boolean removeJobListener(String name) | 移除指定名称的任务监听器。 |
boolean removeTriggerListener(String name) | 移除指定名称的触发器监听器。 |
二、前置项目准备
本章使用的微服务项目博客链接如下,内有对应项目源码。
从零搭建微服务项目Base(第7章——微服务网关模块基础实现)-CSDN博客https://blog.csdn.net/wlf2030/article/details/1456645271.从github下载对应项目解压,重命名为Pro1_3打开。
2.重命名模块为Pro1_3。
3.父工程pom.xml中<name>改成Pro1_3。
4.选择环境为dev,并重新加载maven。
5.启动nacos(安装和启动见第三章)。
6.进入nacos网页 配置管理->配置列表确认有这些yaml文件。
(如果不是一直跟着专栏做自然是没有的,需要看第四章的环境隔离和配置拉取,记得把父工程pom文件中namespace的值与nacos中命名空间生成的保持一致)
7.配置数据源,更换两服务的resources下yml文件的数据库配置,数据库sql见第一章数据库准备部分。
测试数据库连接 属性->点击数据源->测试连接->输入用户名密码
8.添加运行配置 服务->加号->运行配置类型->spring boot。
启动服务,测试接口。
(只有请求参数中包含token才放行)
三、Quartz模块配置
1.新建spring boot模块。
2.选择导入quartz模块(不导入也行,反正后面要换pom文件)
删除quartz模块下多余文件,只保留src以及pom.xml
父pom中加入quartz模块
查看maven结构是否如下,并选择配置文件为dev(如果不是在本专栏Base第0章有解决方法)
惯例在父pom文件中为新添加的模块配置不同环境下端口
删去quartz模块resources目录下原有配置文件,并创建application.yml内容如下:
server:port: @quartz.port@
spring:cloud:nacos:server-addr: localhost:8848discovery:cluster-name: HZnamespace: @namespace@application:name: quartz
将quartz模块下pom内容替换如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>cn.bit</groupId><artifactId>Pro1_3</artifactId><version>1.0-SNAPSHOT</version><relativePath>../pom.xml</relativePath></parent><artifactId>quartz</artifactId><version>0.0.1-SNAPSHOT</version><name>quartz</name><description>quartz</description><dependencies><!-- quartz--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId></dependency><!-- web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- nacos客户端依赖包 --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
四、Demo测试
创建两个类,目录结构如下
DateJob内容如下:
package cn.bit.quartz.entity;import java.util.Date;import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;public class DateJob implements Job {@Overridepublic void execute(JobExecutionContext arg0) throws JobExecutionException {System.out.println(new Date());}}
QuartzController内容如下:
package cn.bit.quartz.controller;import cn.bit.quartz.entity.DateJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/quartz")
public class QuartzController {@GetMapping("/test")public String test() throws SchedulerException {SchedulerFactory schedulerFactory = new StdSchedulerFactory();Scheduler scheduler = schedulerFactory.getScheduler();JobDetail job = JobBuilder.newJob(DateJob.class).withIdentity("job1", "group1").build();Trigger trigger1 = TriggerBuilder.newTrigger().withIdentity("trigger1", "group1").startNow().withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).withRepeatCount(5)).build();scheduler.scheduleJob(job, trigger1);scheduler.start();return "ok";}
}
QuartzApplication设置排除默认数据源设置
启动 Quartz服务,并访问对应接口。
控制台间隔五秒输出当前时间。
五、核心包构建
0.创建数据库对应表以及修改application.yml文件,这里是创建了一个叫db_quartz的库
server:port: @quartz.port@
spring:datasource:url: jdbc:mysql://localhost:3306/db_quartz?useSSL=falseusername: rootpassword: 15947035212driver-class-name: com.mysql.jdbc.Driverapplication:name: quartz
mybatis:mapper-locations: classpath:mapper/*Mapper.xmltype-aliases-package: cn.bit.pro1_1.core.mapper
数据库sql如下:
create table tb_task
(id int auto_incrementprimary key,task_name varchar(255) not null,task_group varchar(255) not null,type int not null,bean_name varchar(255) null,class_name varchar(255) null,path varchar(255) null,method_name varchar(255) null,params varchar(255) null,cron_expression varchar(255) not null,description text null,status int default 0 not null,result int null
);
create table tb_task_log
(id int auto_incrementprimary key,task_id int not null,start_time datetime not null,execute_time varchar(255) not null,result tinyint not null,message varchar(255) not null,exception_info text null
);
删去之前移除数据源的部分
数据源添加quartz对应的库并测试连接成功
数据库显示有三个
1.删去demo部分创建的两个文件,创建core包,其中包结构如下(实际即为将1_2章项目源码中的core部分粘贴进来,并适当修改,有能力的小伙伴可以直接下载上章源码进行对应修改,当然一步步按顺序做便于理解模块)
2.entity创建任务实体类和日志实体类,代码如下
package cn.bit.quartz.core.entity;import lombok.Data;@Data
public class Task {private Integer id;private String taskName;private String taskGroup;private Integer type;//1、java类 2、Spring Bean 3、http请求private String beanName;//bean名称private String className;//java类名private String path;//rest请求路径private String methodName;//方法名private String params;//方法参数private String cronExpression;//cron表达式private String description;//描述private Integer status;//任务当前状态private Integer result;//任务执行结果
}
package cn.bit.quartz.core.entity;import lombok.Data;import java.util.Date;@Data
public class TaskLog {private Integer id;private Integer taskId;private Date startTime;private String executeTime;private Integer result;//0失败 1成功private String message;//日志信息private String exceptionInfo;//异常信息
}
3.enums包下创建任务结果、任务状态、任务类型枚举,具体如下
package cn.bit.quartz.core.enums;import lombok.AllArgsConstructor;
import lombok.Getter;@AllArgsConstructor
@Getter
public enum Result {FAIL(0,"失败"),SUCCESS(1,"成功");private final Integer code;private final String desc;
}
package cn.bit.quartz.core.enums;import lombok.AllArgsConstructor;
import lombok.Getter;@AllArgsConstructor
@Getter
public enum TaskStatus {PAUSE(0, "已发布"),RUNNING(1, "运行中");private final Integer code;private final String desc;
}
package cn.bit.quartz.core.enums;import lombok.AllArgsConstructor;
import lombok.Getter;@AllArgsConstructor
@Getter
public enum TaskType {JAVA_CLASS(1,"java类"),SPRING_BEAN(2,"spring bean"),HTTP(3,"http");private final Integer code;private final String desc;
}
4.exception包下创建任务持久化异常和任务执行异常,具体如下
package cn.bit.quartz.core.exception;import lombok.Getter;@Getter
public class TaskInvokeException extends Exception {private final Exception exception;public TaskInvokeException(String message,Exception exception) {super(message);this.exception = exception;}
}
package cn.bit.quartz.core.exception;import cn.bit.quartz.core.entity.Task;
import lombok.Getter;@Getter
public class TaskRepositoryException extends RuntimeException {private final Task task;public TaskRepositoryException(String message,Task task) {super(message);this.task = task;}
}
5.mapper包下创建实体类的持久化接口
package cn.bit.quartz.core.mapper;;import cn.bit.quartz.core.entity.TaskLog;
import org.apache.ibatis.annotations.Mapper;import java.util.List;@Mapper
public interface TaskLogMapper {int insert(TaskLog taskLog);List<TaskLog> selectByTaskId(int taskId);
}
package cn.bit.quartz.core.mapper;import cn.bit.quartz.core.entity.Task;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.List;@Mapper
public interface TaskMapper {List<Task> selectAllTask();int updateTaskInfo(@Param("task") Task task);int updateTaskStatus(@Param("task") Task task);int insertTask(@Param("task") Task task);int deleteTask(@Param("task") Task task);int setTaskResult(@Param("task") Task task);Task selectTaskByNameAndGroup(@Param("name") String name, @Param("group") String group );
}
6.以及resources下对应mapper.xml,注意目录结构,与application中的mybatis配置有关。
内容分别如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.bit.quartz.core.mapper.TaskLogMapper"><resultMap id="TaskLogResultMap" type="cn.bit.quartz.core.entity.TaskLog"><id property="id" column="id"/><result property="taskId" column="task_id"/><result property="startTime" column="start_time"/><result property="executeTime" column="execute_time"/><result property="result" column="result"/><result property="message" column="message"/><result property="exceptionInfo" column="exception_info"/></resultMap><insert id="insert" parameterType="cn.bit.quartz.core.entity.TaskLog">INSERT INTO tb_task_log(task_id, start_time, execute_time, result, message, exception_info)VALUES (#{taskId}, #{startTime}, #{executeTime}, #{result}, #{message}, #{exceptionInfo})</insert><select id="selectByTaskId" resultMap="TaskLogResultMap">SELECT * FROM tb_task_log WHERE task_id = #{taskId}</select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.bit.quartz.core.mapper.TaskMapper"><resultMap id="TaskResultMap" type="cn.bit.quartz.core.entity.Task"><result property="id" column="id"/><result property="taskName" column="task_name"/><result property="taskGroup" column="task_group"/><result property="type" column="type"/><result property="beanName" column="bean_name"/><result property="className" column="class_name"/><result property="path" column="path"/><result property="methodName" column="method_name"/><result property="params" column="params"/><result property="cronExpression" column="cron_expression"/><result property="description" column="description"/><result property="status" column="status"/><result property="result" column="result"/></resultMap><insert id="insertTask">INSERT INTO tb_task (task_name,task_group,type,bean_name,class_name,path,method_name,params,cron_expression,description) VALUES (#{task.taskName},#{task.taskGroup},#{task.type},#{task.beanName},#{task.className},#{task.path},#{task.methodName},#{task.params},#{task.cronExpression},#{task.description})</insert><delete id="deleteTask">delete from tb_taskwheretask_name = #{task.taskName} andtask_group = #{task.taskGroup};</delete><select id="selectAllTask" resultMap="TaskResultMap">SELECT * FROM tb_task;</select><select id="selectTaskByNameAndGroup" resultMap="TaskResultMap">select * from tb_taskwheretask_name = #{name} andtask_group = #{group}</select><update id="updateTaskInfo">UPDATE tb_taskSETtype = #{task.type},bean_name = #{task.beanName},class_name = #{task.className},path = #{task.path},method_name = #{task.methodName},params = #{task.params},cron_expression = #{task.cronExpression},description = #{task.description}WHEREtask_name = #{task.taskName} andtask_group = #{task.taskGroup};</update><update id="updateTaskStatus">UPDATE tb_taskSETstatus = #{task.status}WHEREtask_name = #{task.taskName} andtask_group = #{task.taskGroup};</update><update id="setTaskResult">update tb_tasksetresult = #{task.result}wheretask_name = #{task.taskName} andtask_group = #{task.taskGroup};</update>
</mapper>
7.创建对应持久化服务
内容如下:
package cn.bit.quartz.core.service;import cn.bit.quartz.core.entity.Task;import java.util.List;public interface TaskService {List<Task> selectAllTask();int updateTaskInfo(Task task);int updateTaskStatus(Task task);int insertTask(Task task);int deleteTask(Task task);int setTaskResult(Task task);Task selectTaskByNameAndGroup(String taskName, String groupName);
}
package cn.bit.quartz.core.service;import cn.bit.quartz.core.entity.TaskLog;import java.util.List;public interface TaskLogService {int insert(TaskLog taskLog);List<TaskLog> selectByTaskId(int taskId);
}
package cn.bit.quartz.core.service.impl;import cn.bit.quartz.core.entity.TaskLog;
import cn.bit.quartz.core.mapper.TaskLogMapper;
import cn.bit.quartz.core.service.TaskLogService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;import java.util.List;@Service
@AllArgsConstructor
public class TaskLogServiceImpl implements TaskLogService {private final TaskLogMapper taskLogMapper;@Overridepublic int insert(TaskLog taskLog) {return taskLogMapper.insert(taskLog);}@Overridepublic List<TaskLog> selectByTaskId(int taskId) {return taskLogMapper.selectByTaskId(taskId);}
}
package cn.bit.quartz.core.service.impl;import cn.bit.quartz.core.entity.Task;
import cn.bit.quartz.core.mapper.TaskMapper;
import cn.bit.quartz.core.service.TaskService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;import java.util.List;@Service
@AllArgsConstructor
public class TaskServiceImpl implements TaskService {private TaskMapper taskMapper;@Overridepublic List<Task> selectAllTask() {return taskMapper.selectAllTask();}@Overridepublic int updateTaskInfo(Task task) {return taskMapper.updateTaskInfo(task);}@Overridepublic int updateTaskStatus(Task task) {return taskMapper.updateTaskStatus(task);}@Overridepublic int insertTask(Task task) {return taskMapper.insertTask(task);}@Overridepublic int deleteTask(Task task) {return taskMapper.deleteTask(task);}@Overridepublic int setTaskResult(Task task) {return taskMapper.setTaskResult(task);}@Overridepublic Task selectTaskByNameAndGroup(String taskName, String groupName) {return taskMapper.selectTaskByNameAndGroup(taskName, groupName);}
}
8.util包下创建springbean的工具类(bean反射会用到)
package cn.bit.quartz.core.util;import lombok.Getter;
import lombok.NonNull;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;@Service
public class SpringContextHolder implements ApplicationContextAware {@Getterprivate static ApplicationContext applicationContext = null;@Overridepublic void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {SpringContextHolder.applicationContext = applicationContext;}
}
9.handler包下创建不同任务类型反射类,以及对应工厂类
package cn.bit.quartz.core.handler;import cn.bit.quartz.core.entity.Task;
import cn.bit.quartz.core.exception.TaskInvokeException;public interface ITaskHandler {void invoke(Task task) throws TaskInvokeException;
}
package cn.bit.quartz.core.handler.impl;import cn.bit.quartz.core.entity.Task;
import cn.bit.quartz.core.enums.Result;
import cn.bit.quartz.core.exception.TaskInvokeException;
import cn.bit.quartz.core.handler.ITaskHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;@Slf4j
@Component
public class JavaClassTaskHandler implements ITaskHandler {@Overridepublic void invoke(Task task) throws TaskInvokeException {try {Object target;Class<?> clazz;Method method;Result returnValue;clazz = Class.forName(task.getClassName());target = clazz.newInstance();if (task.getParams() == null || task.getParams().isEmpty()) {method = target.getClass().getDeclaredMethod(task.getMethodName());ReflectionUtils.makeAccessible(method);returnValue = (Result) method.invoke(target);} else {method = target.getClass().getDeclaredMethod(task.getMethodName(), String.class);ReflectionUtils.makeAccessible(method);returnValue = (Result) method.invoke(target, task.getParams());}//判断业务是否执行成功if (returnValue == null || Result.FAIL.equals(returnValue))throw new TaskInvokeException("JavaClassTaskHandler方法执行失败",null);} catch (NoSuchMethodException e) {throw new TaskInvokeException("JavaClassTaskHandler找不到对应方法", e);} catch (InvocationTargetException | IllegalAccessException e) {throw new TaskInvokeException("JavaClassTaskHandler执行反射方法异常", e);} catch (ClassCastException e) {throw new TaskInvokeException("JavaClassTaskHandler方法返回值定义错误", e);} catch (ClassNotFoundException e) {throw new TaskInvokeException("JavaClassTaskHandler找不到对应类", e);} catch (InstantiationException e) {throw new TaskInvokeException("JavaClassTaskHandler实例化错误", e);}}
}
package cn.bit.quartz.core.handler.impl;import cn.bit.quartz.core.entity.Task;
import cn.bit.quartz.core.enums.Result;
import cn.bit.quartz.core.exception.TaskInvokeException;
import cn.bit.quartz.core.handler.ITaskHandler;
import cn.bit.quartz.core.util.SpringContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;@Slf4j
@Component
public class SpringBeanTaskHandler implements ITaskHandler {@Overridepublic void invoke(Task task) throws TaskInvokeException {try {Object target;Method method;Result returnValue;//上下文寻找对应beantarget = SpringContextHolder.getApplicationContext().getBean(task.getBeanName());//寻找对应方法if(task.getParams()==null|| task.getParams().isEmpty()){method = target.getClass().getDeclaredMethod(task.getMethodName());ReflectionUtils.makeAccessible(method);returnValue = (Result) method.invoke(target);}else{method = target.getClass().getDeclaredMethod(task.getMethodName(), String.class);ReflectionUtils.makeAccessible(method);returnValue = (Result) method.invoke(target, task.getParams());}//判断业务是否执行成功if(returnValue==null || Result.FAIL.equals(returnValue))throw new TaskInvokeException("SpringBeanTaskHandler方法执行失败", null);}catch (NoSuchBeanDefinitionException e){throw new TaskInvokeException("SpringBeanTaskHandler找不到对应bean",e);} catch (NoSuchMethodException e) {throw new TaskInvokeException("SpringBeanTaskHandler找不到对应方法",e);} catch (InvocationTargetException | IllegalAccessException e) {throw new TaskInvokeException("SpringBeanTaskHandler执行反射方法异常",e);} catch (ClassCastException e) {throw new TaskInvokeException("SpringBeanTaskHandler方法返回值定义错误",e);}}
}
package cn.bit.quartz.core.handler;import cn.bit.quartz.core.entity.Task;
import cn.bit.quartz.core.enums.TaskType;
import cn.bit.quartz.core.handler.impl.JavaClassTaskHandler;
import cn.bit.quartz.core.handler.impl.SpringBeanTaskHandler;
import cn.bit.quartz.core.util.SpringContextHolder;
import org.springframework.stereotype.Component;@Component
public class TaskHandlerFactory {public static ITaskHandler getTaskHandler(Task task) {ITaskHandler taskHandler = null;if(TaskType.SPRING_BEAN.getCode().equals(task.getType())) {taskHandler = SpringContextHolder.getApplicationContext().getBean(SpringBeanTaskHandler.class);}if(TaskType.JAVA_CLASS.getCode().equals(task.getType())) {taskHandler = SpringContextHolder.getApplicationContext().getBean(JavaClassTaskHandler.class);}return taskHandler;}
}
10.events包下创建任务调用事件,监听者和发布者,其中发布者实现Job接口
package cn.bit.quartz.core.events.event;import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;@ToString
@Getter
@AllArgsConstructor
public class TaskInvokeEvent {private final String taskName;private final String taskGroup;
}
package cn.bit.quartz.core.events.listener;import cn.bit.quartz.core.entity.Task;
import cn.bit.quartz.core.entity.TaskLog;
import cn.bit.quartz.core.enums.Result;
import cn.bit.quartz.core.events.event.TaskInvokeEvent;
import cn.bit.quartz.core.exception.TaskInvokeException;
import cn.bit.quartz.core.handler.ITaskHandler;
import cn.bit.quartz.core.handler.TaskHandlerFactory;
import cn.bit.quartz.core.service.TaskLogService;
import cn.bit.quartz.core.service.TaskService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;import java.util.Date;@Slf4j
@AllArgsConstructor
@Component
public class TaskInvokeListener {private final TaskLogService taskLogService;private final TaskService taskService;@Async@Order@EventListener(TaskInvokeEvent.class)public void notifyTaskInvoke(TaskInvokeEvent event) {//从数据库中拿取任务Task task = taskService.selectTaskByNameAndGroup(event.getTaskName(), event.getTaskGroup());log.info("任务执行事件监听,准备执行任务{}",task);ITaskHandler handler = TaskHandlerFactory.getTaskHandler(task);long startTime = System.currentTimeMillis();TaskLog taskLog = new TaskLog();taskLog.setTaskId(task.getId());taskLog.setStartTime(new Date());boolean success = true;try {handler.invoke(task);} catch (TaskInvokeException e) {log.error("{},Task:{}", e.getMessage(),task);success = false;taskLog.setMessage(e.getMessage());if(e.getException()!=null){taskLog.setExceptionInfo(e.getException().getCause().toString());e.getException().printStackTrace();}}if(success){taskLog.setMessage("执行成功");taskLog.setResult(Result.SUCCESS.getCode());task.setResult(Result.SUCCESS.getCode());taskService.setTaskResult(task);}else{taskLog.setResult(Result.FAIL.getCode());task.setResult(Result.FAIL.getCode());taskService.setTaskResult(task);}long endTime = System.currentTimeMillis();taskLog.setExecuteTime(String.valueOf(endTime-startTime));taskLogService.insert(taskLog);}
}
package cn.bit.quartz.core.events.publisher;import cn.bit.quartz.core.entity.Task;
import cn.bit.quartz.core.events.event.TaskInvokeEvent;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;@AllArgsConstructor
@Slf4j
@Component
public class TaskInvokePublisher implements Job {private final ApplicationEventPublisher publisher;@Overridepublic void execute(JobExecutionContext jobExecutionContext){Task task = (Task) jobExecutionContext.getJobDetail().getJobDataMap().get("task");//发布事件异步执行任务TaskInvokeEvent event =new TaskInvokeEvent(task.getTaskName(),task.getTaskGroup());publisher.publishEvent(event);log.info("任务执行事件发布:{}",event);}
}
11.mvc下创建对应任务控制类
package cn.bit.quartz.core.mvc;import cn.bit.quartz.core.entity.Task;
import cn.bit.quartz.core.events.publisher.TaskInvokePublisher;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.stereotype.Service;@Slf4j
@Service
@AllArgsConstructor
public class TaskPool {public static JobKey getJobKey(@NonNull Task task) {return JobKey.jobKey(task.getTaskName(),task.getTaskGroup());}public static TriggerKey getTriggerKey(@NonNull Task task) {return TriggerKey.triggerKey(task.getTaskName(),task.getTaskGroup());}/*** 任务池添加任务* @param task* @param scheduler* @throws SchedulerException*/public void addTask(@NonNull Task task,Scheduler scheduler) throws SchedulerException {JobKey jobKey = getJobKey(task);TriggerKey triggerKey = getTriggerKey(task);JobDetail jobDetail = JobBuilder.newJob(TaskInvokePublisher.class).withIdentity(jobKey).build();jobDetail.getJobDataMap().put("task",task);CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(task.getCronExpression());CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();scheduler.scheduleJob(jobDetail, trigger);}/*** 任务池暂停并移除任务* @param task* @param scheduler* @throws SchedulerException*/public void pauseTask(@NonNull Task task,Scheduler scheduler) throws SchedulerException {scheduler.pauseJob(getJobKey(task));scheduler.deleteJob(getJobKey(task));}}
package cn.bit.quartz.core.mvc;import cn.bit.quartz.core.entity.Task;
import cn.bit.quartz.core.enums.TaskStatus;
import cn.bit.quartz.core.exception.TaskRepositoryException;
import cn.bit.quartz.core.service.TaskService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.stereotype.Service;import javax.annotation.PostConstruct;
import java.util.List;@Slf4j
@Service
@AllArgsConstructor
public class TaskManger {private final Scheduler scheduler;private final TaskService taskService;private final TaskPool taskPool;/*** 从数据库中反序列化任务数据,保证服务器重启后恢复任务池状态* @throws SchedulerException*/@PostConstructpublic void init() throws SchedulerException {log.info("TaskManager初始化开始...");List<Task> tasks = taskService.selectAllTask();if(tasks != null && !tasks.isEmpty()) {for (Task task : tasks){if(TaskStatus.RUNNING.getCode().equals(task.getStatus()))taskPool.addTask(task,scheduler);}log.info("初始化加载{}项任务", tasks.size());}log.info("TaskManager初始化结束...");}/*** 添加暂停且未被持久化的新任务* @param task* @throws SchedulerException*/public void addTask(Task task) throws SchedulerException {Task temp = taskService.selectTaskByNameAndGroup(task.getTaskName(),task.getTaskGroup());if(temp != null)throw new TaskRepositoryException("存在相同任务组和任务名任务",task);if(!TaskStatus.PAUSE.getCode().equals(task.getStatus()))throw new TaskRepositoryException("只能添加暂停的任务",task);taskService.insertTask(task);log.info("添加任务{}", task);}/*** 在任务暂停时更新任务信息* @param task* @throws SchedulerException*/public void updateTask(Task task) throws SchedulerException {Task temp = taskService.selectTaskByNameAndGroup(task.getTaskName(),task.getTaskGroup());if(temp == null)throw new TaskRepositoryException("不存在对应相同任务组和任务名任务",task);if(!TaskStatus.PAUSE.getCode().equals(temp.getStatus()))throw new TaskRepositoryException("只能暂停时更新任务",task);taskService.updateTaskInfo(task);log.info("更新任务{}", task);}/*** 启动暂停中任务* @param task 只使用name和group字段* @throws SchedulerException*/public void startTask(Task task) throws SchedulerException {Task temp = taskService.selectTaskByNameAndGroup(task.getTaskName(),task.getTaskGroup());if(temp == null)throw new TaskRepositoryException("不存在对应相同任务组和任务名任务",task);if(!TaskStatus.PAUSE.getCode().equals(temp.getStatus()))throw new TaskRepositoryException("只能启动暂停中任务",task);taskPool.addTask(temp,scheduler);//添加任务池未有异常时持久化数据temp.setStatus(TaskStatus.RUNNING.getCode());taskService.updateTaskStatus(temp);log.info("启动任务{}", temp);}/*** 暂停运行中任务* @param task 只使用name和group字段* @throws SchedulerException*/public void pauseTask(Task task) throws SchedulerException {Task temp = taskService.selectTaskByNameAndGroup(task.getTaskName(),task.getTaskGroup());if(temp == null)throw new TaskRepositoryException("不存在对应相同任务组和任务名任务",task);if(!TaskStatus.RUNNING.getCode().equals(temp.getStatus()))throw new TaskRepositoryException("只能暂停运行中任务",task);taskPool.pauseTask(temp,scheduler);//添加任务池未有异常时持久化数据temp.setStatus(TaskStatus.PAUSE.getCode());taskService.updateTaskStatus(temp);log.info("暂停任务{}", temp);}/*** 暂停暂停中任务* @param task 只使用name和group字段* @throws SchedulerException*/public void deleteTask(Task task) throws SchedulerException {Task temp = taskService.selectTaskByNameAndGroup(task.getTaskName(),task.getTaskGroup());if(temp == null)throw new TaskRepositoryException("不存在对应相同任务组和任务名任务",task);if(!TaskStatus.PAUSE.getCode().equals(temp.getStatus()))throw new TaskRepositoryException("只能删除暂停中任务",task);taskService.deleteTask(temp);log.info("删除任务{}", temp);}
}
12.以core同级创建controller包作为quartz服务对外接口,并创建类
package cn.bit.quartz.controller;import cn.bit.quartz.core.entity.Task;
import cn.bit.quartz.core.mvc.TaskManger;
import lombok.AllArgsConstructor;
import org.quartz.SchedulerException;
import org.springframework.web.bind.annotation.*;@RestController
@RequestMapping("/quartz")
@AllArgsConstructor
public class QuartzController {private TaskManger taskManger;@PostMapping("/add")public String add(@RequestBody Task task) throws SchedulerException {taskManger.addTask(task);return "success";}@PutMapping("/update")public String update(@RequestBody Task task) throws SchedulerException {taskManger.updateTask(task);return "success";}@PutMapping("/start")public String start(@RequestBody Task task) throws SchedulerException {taskManger.startTask(task);return "success";}@PutMapping("/pause")public String pause(@RequestBody Task task) throws SchedulerException {taskManger.pauseTask(task);return "success";}@DeleteMapping("/delete")public String delete(@RequestBody Task task) throws SchedulerException {taskManger.deleteTask(task);return "success";}
}
六、单模块测试
在controller包下创建测试类
package cn.bit.quartz.controller;import cn.bit.quartz.core.enums.Result;
import org.springframework.stereotype.Component;import java.util.Date;@Component("test")
public class Test {public Result test(String param) {System.out.println("test "+param+" "+new Date());return Result.SUCCESS;}
}
单独启动quartz服务,分别使用postman依次发出下面请求
{"taskName": "task1","taskGroup": "group1","type": 2,"beanName": "test","methodName": "test","params": "test1","cronExpression": "*/5 * * * * ?","status": 0
}
控制台输出:
{"taskName": "task1","taskGroup": "group1"
}
控制台每5s执行一次任务
{"taskName": "task1","taskGroup": "group1"
}
控制台显示任务被暂停
(从下面开始则为将quartz整合至微服务项目中,为组件复用,会有很多模块间的导入,但同时又需要考虑到spring上下文不互通导致无法获取其他模块的bean,本文采用的方式是将公共类抽取至common模块中,其他模块均导入common模块,选择性导入common中bean,但同时可能各模块core中的实体类不在本模块中,有利有弊,以及模块的导入涵盖了Base章节的大部分内容,有一定门槛建议先了解Base章节中内容再继续,当然如果只是想知道springboot中quartz模块的使用到这里就可以不用往下看了)
七、服务整合
1.服务注册
quartz模块创建bootstrap.yml内容如下:
spring:application:name: quartzcloud:nacos:server-addr: localhost:8848discovery:cluster-name: HZnamespace: @namespace@
添加后启动在nacos中能够看到服务被注册
2.网关路由
修改gateway配置文件内容如下
server:port: @gateway.port@
spring:application:name: gatewaycloud:nacos:server-addr: localhost:8848discovery:namespace: @namespace@gateway:routes:- id: user-serviceuri:lb://user-servicepredicates:- name: Pathargs:_genkey_0: /user/**filters:- name: AddRequestHeaderargs:name: sourcevalue: request user from gateway- id: order-serviceuri:lb://order-servicepredicates:- name: Pathargs:_genkey_0: /order/**filters:- name: AddRequestHeaderargs:name: sourcevalue: request order from gateway- id: quartzuri:lb://quartzpredicates:- name: Pathargs:_genkey_0: /quartz/**
启动网关服务和quartz服务后,通过网关端口调用接口(网关有个简单的鉴权需要token = admin放行)
3.全局异常处理
common模块创建exception包用于定义全局异常,创建业务异常内容如下:
package cn.bit.common.exception;import lombok.NoArgsConstructor;@NoArgsConstructor
public class BizException extends RuntimeException {public BizException(String message) {super(message);}public BizException(Throwable cause) {super(cause);}public BizException(String message, Throwable cause) {super(message, cause);}public BizException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {super(message, cause, enableSuppression, writableStackTrace);}}
各模块的业务逻辑异常应当由各模块自己处理,为方便复用,处理类放于common模块下,common导入web依赖
<dependency><groupId>org.springframework</groupId><artifactId>spring-web</artifactId></dependency>
在common模块新建handler包,以及异常处理类,内容如下,此处将Pro2-1章的jsr异常也提前添加。
package cn.bit.common.handler;import cn.bit.common.exception.BizException;
import cn.bit.common.pojo.vo.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;import java.nio.file.AccessDeniedException;
import java.util.List;@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/*** 全局异常.* @param e the e* @return R*/@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public R handleGlobalException(Exception e) {log.error("全局异常信息 ex={}", e.getMessage(), e);return R.failed(e.getLocalizedMessage());}/*** AccessDeniedException* @param e the e* @return R*/@ExceptionHandler(AccessDeniedException.class)@ResponseStatus(HttpStatus.FORBIDDEN)public R handleAccessDeniedException(AccessDeniedException e) {log.error("拒绝授权异常信息 ex={}", e.getLocalizedMessage(),e);return R.failed(e.getLocalizedMessage());}/*** 业务处理类* @param e the e* @return R*/@ExceptionHandler({ BizException.class })@ResponseStatus(HttpStatus.BAD_REQUEST)public R bizExceptionHandler(BizException e) {log.warn("业务处理异常,ex = {}", e.getMessage());return R.failed(e.getMessage());}/*** validation Exception* @param e the e* @return R*/@ExceptionHandler({ MethodArgumentNotValidException.class})@ResponseStatus(HttpStatus.BAD_REQUEST)public R handleBodyValidException(MethodArgumentNotValidException e) {List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();StringBuilder errorMsg = new StringBuilder();fieldErrors.forEach(fieldError -> {errorMsg.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(" ");});log.warn("参数绑定异常,ex = {}",errorMsg);return R.failed(errorMsg.toString());}/*** validation Exception (以form-data形式传参)* @param e the e* @return R*/@ExceptionHandler({ BindException.class})@ResponseStatus(HttpStatus.BAD_REQUEST)public R bindExceptionHandler(BindException e) {List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();StringBuilder errorMsg = new StringBuilder();fieldErrors.forEach(fieldError -> {errorMsg.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append("\n");});log.warn("参数绑定异常(form-data),ex = {}",errorMsg);return R.failed(errorMsg.toString());}
}
4.统一响应体格式以及异常统一
quartz中添加项目中common模块的依赖,从中拿取公共类
<dependency><groupId>cn.bit</groupId><artifactId>common</artifactId><version>1.0-SNAPSHOT</version></dependency>
修改quartz模块controller内容如下:
package cn.bit.quartz.controller;import cn.bit.common.exception.BizException;
import cn.bit.common.pojo.vo.R;
import cn.bit.quartz.core.entity.Task;
import cn.bit.quartz.core.exception.TaskRepositoryException;
import cn.bit.quartz.core.mvc.TaskManger;
import lombok.AllArgsConstructor;
import org.quartz.SchedulerException;
import org.springframework.web.bind.annotation.*;@RestController
@RequestMapping("/quartz")
@AllArgsConstructor
public class QuartzController {private TaskManger taskManger;@PostMapping("/add")public R add(@RequestBody Task task){try {taskManger.addTask(task);} catch (SchedulerException | TaskRepositoryException e) {throw new BizException(e);}return R.ok("添加任务成功");}@PutMapping("/update")public R update(@RequestBody Task task){try {taskManger.updateTask(task);}catch (SchedulerException | TaskRepositoryException e) {throw new BizException(e.getMessage());}return R.ok("任务更新成功");}@PutMapping("/start")public R start(@RequestBody Task task){try {taskManger.startTask(task);}catch (SchedulerException | TaskRepositoryException e) {throw new BizException(e.getMessage());}return R.ok("任务启动成功");}@PutMapping("/pause")public R pause(@RequestBody Task task){try {taskManger.pauseTask(task);} catch (SchedulerException | TaskRepositoryException e) {throw new BizException(e.getMessage());}return R.ok("任务暂停成功");}@DeleteMapping("/delete")public R delete(@RequestBody Task task){try {taskManger.deleteTask(task);} catch (SchedulerException | TaskRepositoryException e) {throw new BizException(e.getMessage());}return R.ok("任务删除成功");}
}
并在quartz模块的application中引入异常处理类的bean
package cn.bit.quartz;import cn.bit.common.handler.GlobalExceptionHandler;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;@SpringBootApplication
@Import(GlobalExceptionHandler.class)
public class QuartzApplication {public static void main(String[] args) {SpringApplication.run(QuartzApplication.class, args);}}
八、整合测试
启动网关和quartz模块,postman测试接口
访问网关接口,不加admin,返回401未鉴权错误码
尝试添加相同任务
启动暂停任务
启动运行任务
最后:
本来是想再添加对应的feignclient但实在想不出有什么业务需要调用order-service或user-service来开启定时任务,直接调用quartz模块不是来的更快。至此,关于quartz定时模块的研究到此正式完结撒花 ,基本框架不会变更了,后续仅做微调,以及最后希望大家多多支持!