需求: 在项目中免不了会使用定时任务来执行一些自动化的操作。简单的定时任务可以在方法上加上@Schedule的注解来执行定时任务。但是如果有多个同级的模块在不同的时间点执行同一个方法,就没办法仅仅使用@Schedule来执行了。
侃场景: 现在某一个系统有不同的用户等级,如氪金大佬-vip、豹子头零充-poor和穷逼vip-complimentary。不同等级的用户经验获取情况不一样(不同的时间间隔执行获得经验方法),获得福利的情况也不一样(发放福利的时间不同)
Quartz 是OpenSymphony开源组织在Job scheduling领域又一个开源项目,是完全由java开发的一个开源的任务日程管理系统,“任务进度管理器”就是一个在预先确定(被纳入日程)的时间到达时,负责执行(或者通知)其他软件组件的系统。
1、Quartz的简单使用
1.1 首先需要导入相关依赖。
我这里使用的springboot版本是2.1.4版本的
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
1.2 实现你想要调度器执行的任务组件需要实现的接口Job
Job接口中只有一个方法execute(),这里面编写你实际的逻辑,这里我们用简单的输出来看效果。
public class UserLevelJob implements Job{
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 这里进行实际逻辑的实现
System.out.println("开始调用实际逻辑[" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "]");
}
}
1.3 定义Scheduler,JobDetail和Trigger
Scheduler 是Quartz Scheduler的主要接口,代表一个独立运行容器。调度程序维护JobDetails和触发器的注册表。 一旦注册,调度程序负责执行作业,当他们的相关联的触发器触发(当他们的预定时间到达时)。
我们需要新建一个自定义的Scheduler类,并在其中创建一个调度器
@Component
public class UserLevelScheduler {
public void scheduleJob() throws SchedulerException {
// 创建调度器Scheduler
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
}
}
JobDetail 对象是在将 job 加入 scheduler 时,由客户端程序(你的程序)创建的。它包含 job 的各种属性设置,以及用于存储 job 实例状态信息的 JobDataMap。JobDetail实例是通过JobBuilder类创建的,并且在创建的时候绑定我们在上面定义的job。
@Component
public class UserLevelScheduler {
public void scheduleJob() throws SchedulerException {
// 创建调度器Scheduler,见以上步骤
// 创建JobDetail实例,并与Job类绑定(Job执行内容)
String jobName = "userLevelJob";
JobDetail jobDetail = JobBuilder.newJob(UserLevelJob.class).withIdentity(jobName, "group1").build();
}
}
Trigger 用于触发 Job 的执行。当你准备调度一个 job 时,你创建一个 Trigger 的实例,然后设置调度相关的属性。Quartz 自带了各种不同类型的 Trigger,最常用的主要是 SimpleTrigger 和 CronTrigger。我们这里使用CronTrigger。先将我们需要执行任务的cron表达式构建一个CronScheduleBuilder实例,然后使用TriggerBuilder和CronScheduleBuilder构建出CronTrigger
@Component
public class UserLevelScheduler {
public void scheduleJob() throws SchedulerException {
// 创建调度器Scheduler
// 创建JobDetail实例,并与Job类绑定(Job执行内容),见以上步骤
// 构建Trigger实例,根据cron表达式进行执行
String triggerName = "userLevelTrigger";
String cron = "*/10 * * * * ?";
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerName, "group1").withSchedule(scheduleBuilder).build();
}
}
构建完成后,将我们的JobDetail和Trigger配置到Scheduler并且启动调度
@Component
public class UserLevelScheduler {
public void scheduleJob() throws SchedulerException {
// 创建调度器Scheduler
// 创建JobDetail实例,并与Job类绑定(Job执行内容)
// 构建Trigger实例,根据cron表达式进行执行,见以上步骤
// 进行调度
scheduler.scheduleJob(jobDetail,cronTrigger);
scheduler.start();
}
}
1.4 测试
Quartz简单应用环境搭建好之后,我们需要调用调度器UserLevelScheduler的scheduleJob()方法来开启,这里随便写一个测试controller
@RestController
public class TestController {
@Autowired
private UserLevelScheduler scheduler;
@GetMapping("/jobTest")
public void jobTest() throws SchedulerException {
scheduler.scheduleJob();
}
}
调用controller的接口后,日志会出现以下内容,表示我们的调度器已经正式开启了
INFO 22424 --- [ main] org.quartz.core.QuartzScheduler : Scheduler meta-data: Quartz Scheduler (v2.3.0) 'DefaultQuartzScheduler' with instanceId 'NON_CLUSTERED'
Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
NOT STARTED.
Currently in standby mode.
Number of jobs executed: 0
Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.
INFO 22424 --- [ main] org.quartz.impl.StdSchedulerFactory : Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties'
INFO 22424 --- [ main] org.quartz.impl.StdSchedulerFactory : Quartz scheduler version: 2.3.0
INFO 22424 --- [ main] org.quartz.core.QuartzScheduler : Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.
接下来就等待cron表达式指定的时间执行实际逻辑即可
2、需求其他要素的整合
2.1 相关配置
按照需求的要求,不同的用户等级的相同类型的方法有不同的定时执行时间,这里给每个等级的用户建一个配置文件,以vip为例。这里配置文件放在resources下
# vip.properties
userLevel=vip
experienceCron=
welfareCron=
然后定义需要启用的用户等级,后续会通过这里的定义读取到上面的对用的配置文件的内容。这里我们写在application.yml中
userLevels: vip,poor
2.2 读取配置
首先新建一个配置类,并从主配置文件中读取定义到的userLevels,然后根据这些值读取相关的配置文件的内容并封装到Propertits中。最后把所有的的Properties定义到Bean中,在后面的部分可以通过注入使用这些Properties。
@Configuration
public class UserLevelConfig {
@Value("${userLevels}")
// 从主配置文件中读取需要开启的用户级别列表
private String userLevels;
@Autowired
ResourceLoader resourceLoader;
@Bean
// 将需要开启的用户级别配置信息作为Bean注入
public List<Properties> userLevelPropertiesList() throws IOException{
List<String> userLevelList = Arrays.asList(userLevels.split(","));
if(userLevelList.size() == 0){
return null;
}
List<Properties> userLevelPropertiesList = new ArrayList<>();
for (String u : userLevelList) {// 针对开启的每个用户级别进行操作
Properties properties = new Properties();
// 获得配置文件名称,默认放在resources下
String propertiesFilePath = "classpath:" + u + ".properties";
// 读取配置文件
Resource resource = resourceLoader.getResource(propertiesFilePath);
InputStream inputStream = resource.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
// 加载到properties中
properties.load(bufferedReader);
// 读取成功则加到列表中
userLevelPropertiesList.add(properties);
}
return userLevelPropertiesList;
}
}
这里我们可以写个操作类,注入获取到的Properties并通过传入的用户类型值来获得其对应的Properties
@Component
public class UserLevelPropertiesOperation {
@Autowired
@Qualifier("userLevelPropertiesList")
// 获得用户配置列表的bean
private List<Properties> userLevelPropertiesList;
/**
* 根据用户级别获得对应的配置信息
* @param userLevel 传入用户级别
* @return 返回配置信息
*/
public Properties getUserLevelProperties(String userLevel){
if(StringUtils.isEmpty(userLevel)) {
System.out.println("传入无效值");
return null;
}
// 循环判断,返回匹配的配置信息
for (Properties properties : userLevelPropertiesList) {
if(userLevel.equals(properties.getProperty("userLevel"))){
return properties;
}
}
return null;
}
/**
* 根据用户级别获得对应的配置信息
* @param userLevel 传入用户级别
* @return 返回Map类型的配置信息
*/
public Map<String, String> getUserLevelPropertiesMap(String userLevel){
Properties properties = getUserLevelProperties(userLevel);
if(properties == null){
return null;
}
return (Map) properties;
}
}
2.3 配置事件监听
在1.4中我们使用一个接口来开启任务调度器,在这里我们可以写一个事件监听,在项目启动ApplicationContext 被初始化或刷新时,就可以启动任务调度器
@Configuration
public class UserLevelSchedulerListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private UserLevelScheduler scheduler;
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
try {
scheduler.scheduleJob();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
2.4 改造任务调度器Scheduler
针对于每个cron任务,其创建步骤都是类似的,我们将这个重复性的内容抽取成一个方法
@Component
public class UserLevelScheduler {
@Autowired
@Qualifier("userLevelPropertiesList")
// 获得用户配置列表的bean
private List<Properties> userLevelPropertiesList;
public void scheduleJob() throws SchedulerException {
}
public void setJob(Scheduler scheduler, String jobNameSuffix, String cron, String type, String level) throws SchedulerException {
String jobName = jobNameSuffix + "-" + type;
String GroupName = "group-" + type;
// 创建JobDetail实例,并与Job类绑定(Job执行内容)
JobDetail jobDetail = JobBuilder.newJob(UserLevelJob.class).withIdentity(jobName, GroupName).build();
// 构建Trigger实例,根据cron表达式进行执行
String triggerName = level + "Trigger-" + type;
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerName, GroupName).withSchedule(scheduleBuilder).build();
// 进行调度
scheduler.scheduleJob(jobDetail,cronTrigger);
}
}
在我们调度任务的时候,可以给job实例增加属性或配置。JobDataMap中可以包含不限量的(序列化的)数据对象,在job实例执行的时候,可以使用其中的数据;JobDataMap是Java Map接口的一个实现,额外增加了一些便于存取基本类型的数据的方法。
public void setJob(Scheduler scheduler, String jobNameSuffix, String cron, String type, String level) throws SchedulerException {
// 创建JobDetail实例,并与Job类绑定(Job执行内容)
// 向JobDetail设置需要传递的信息
jobDetail.getJobDataMap().put("userLevel", level);
jobDetail.getJobDataMap().put("type", type);
// 构建Trigger实例,根据cron表达式进行执行
// 进行调度
}
然后我们就可以针对每个用户等级的配置来创建不同的JobDetail及其相对应的Trigger,然后进行任务调度
@Component
public class UserLevelScheduler {
@Autowired
@Qualifier("userLevelPropertiesList")
// 获得用户配置列表的bean
private List<Properties> userLevelPropertiesList;
public void scheduleJob() throws SchedulerException {
// 创建调度器Scheduler
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
for (Properties properties : userLevelPropertiesList) {
String level = (String) properties.get("userLevel");
String jobName = level + "Job";
if (properties.get("experienceCron") != null && !"".equals(properties.get("experienceCron").toString().trim())){
setJob(scheduler, jobName, properties.get("experienceCron").toString(), "experience", level);
}
if (properties.get("welfareCron") != null && !"".equals(properties.get("welfareCron").toString().trim())){
setJob(scheduler, jobName, properties.get("welfareCron").toString(), "welfare", level);
}
scheduler.start();
}
}
}
2.5 改造任务Job
在任务调度器中,我们在JobDetail中设置了一些参数,可以在这里获取到设置的值
public class UserLevelJob implements Job{
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 获取JobDetail传递的信息
String userLevel = context.getJobDetail().getJobDataMap().getString("userLevel");
String type = context.getJobDetail().getJobDataMap().getString("type");
// 这里进行实际逻辑的实现
System.out.println(userLevel + "开始调用实际" + type + "逻辑[" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "]");
}
}