SpringBoot框架中常用的定时任务大致可以分为三种:第一种,使用Java Timer 这种不常用这里不做阐述;第二种,使用Spring Task;第三种,使用Quartz;笔者下面来阐述一下后两种的基本用法,不足之处欢迎留言交流。
版本
Spring Boot2.0及以上因为已经集成了Scheluder相关功能
Spring Task
可以理解为简化版的Quartz,支持Cron语法以及扩展
启动准备
需要做的操作就一步,那就是在Springboot的启动类中开启定时任务的支持,也就是@EnableScheduling注解,如果没有这个注解那么就算你在定时任务的执行方法中标记了这个方法是定时任务的方法也不会被执行
单线程模式
只需要编写实体类标记@Scheduled注解那么这个定时任务就会自动被Spring Task装载在定时队列中;
package com.zhy.springboot.scheduler.task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* @Title: TestTask.java
* @Package com.zhy.springboot.scheduler
* @Description: 基于 Spring Task的定时任务
* @author John_Hawkings
* @date 2018年12月25日
* @version V1.0
*/
//用于标识多线程
@Component
public class TestTask {
private static Logger logger = LoggerFactory.getLogger(TestTask.class);
//fixedDelay = 5000表示当前方法执行完毕5000ms后,Spring scheduling会再次调用该方法
//fixedRate = 5000表示当前方法开始执行5000ms后,Spring scheduling会再次调用该方法
//initialDelay = 1000表示延迟1000ms执行第一次任务
//@Async
@Scheduled(cron = "0/5 * * * * *")
public void scheduled1(){
logger.info("=====>>>>>使用cron1 {}",System.currentTimeMillis());
}
@Scheduled(cron = "0/10 * * * * *")
public void scheduled2(){
logger.info("=====>>>>>使用cron2 {}",System.currentTimeMillis());
}
}
执行效果
可以看到两个定时任务正常执行,但是有一个缺陷,那就是这两个定时任务都是一个线程在跑的,多个的时候也是一样,这样会造成其中一个定时任务挂掉,其余的定时任务都会挂掉执行不到,那么正规开发中这种情况当然是要避免的,怎么避免?多线程。
多线程模式
为了避免像上面那种单线程情况下的问题,我们需要对Spring Task进行相关配置来解决单线程定时任务中的问题,常用的多线程配置基本有两种情况。
第一种:基于注解@EnableAsync
说简单点就是基于Springboot的@EnableAsync与@Async注解来标记定时任务的异步执行,SpringBoot通过任务执行器(TaskExecutor)来实现多线程和并发编程。使用ThreadPoolTaskExecutor可实现一个基于线程池的TaskExecutor.在开发中实现异步任务,我们可以在配置类中添加@EnableAsync开始对异步任务的支持,并在相应的方法中使用@Async注解来声明一个异步任务。
具体配置
package com.zhy.springboot.scheduler;
import java.util.concurrent.Executor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* @Title: AsyncConfig.java
* @Package com.zhy.springboot.scheduler
* @Description: 定时任务多线程执行
* @author John_Hawkings
* @date 2018年12月25日
* @version V1.0
*/
@Configuration
@EnableAsync
public class AsyncConfig {
@Value("${task.corePoolSize}")
private int corePoolSize;
@Value("${task.maxPoolSize}")
private int maxPoolSize;
@Value("${task.queueCapacity}")
private int queueCapacity;
@Value("${task.keepAliveSeconds}")
private int keepAliveSeconds;
@Bean
public Executor taskExecutor() {
/*
* 如果线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maxPoolSize,建新的线程来处理被添加的任务。
如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maxPoolSize,
那么通过handler所指定的策略来处理此任务。也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程 maximumPoolSize,
如果三者都满了,使用handler处理被拒绝的任务。
当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
*/
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//线程池维护线程的最少数量
executor.setCorePoolSize(corePoolSize);
//线程池维护线程的最大数量
executor.setMaxPoolSize(maxPoolSize);
//缓存队列
executor.setQueueCapacity(queueCapacity);
//允许的空闲时间
executor.setKeepAliveSeconds(keepAliveSeconds);
executor.initialize();
return executor;
}
}
YML文件配置
#定时任务配置
task:
corePoolSize: 10
maxPoolSize: 100
queueCapacity: 10
keepAliveSeconds: 300
这样配置后其实是通过注解开启了ThreadPoolTaskExecutor的异步支持,然后通过扫描到@Async注解启动多线程执行,如果不加这个注解那么还是像单线程那种一个线程在执行此定时;需要注意的是,如果该注解标记在类上,那么默认所有此定时类里面的方法都是多线程执行的,如果只标记在方法上那么就是方法的多线程执行,下面来看具体演示
标记在类上
可以看到这两个测试的定时都是由ThreadPoolTaskExecutor线程池中的不同线程执行的
标记在方法上
通过执行结果可以清晰的看到加了异步的注解一直由taskExecutor的不同线程在执行,而没加注解的定时一直由同一个标记出来的线程在执行。使用的时候分清业务需求合理使用该异步注解,如果注解子啊类上,方法中没必要在加该注解以免不必要的错误
第二种:实现SchedulingConfigurer接口
这种配置相比于第一种,虽然也能达到多线程并发执行的效果,但是这种还是有一点局限性,因为JDK1.5后Executors就不推荐使用了,该方案的主要操作就是实现SchedulingConfigurer接口,并重写setSchedulerfang方法
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
}
}
运行效果
Quartz
Pom.xml
org.springframework.boot
spring-boot-starter-quartz
配置Job类交给Spring管理
package com.zhy.springboot.quartz;
import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
/**
* @Title: QuartzConfig.java
* @Package com.zhy.springboot.quartz
* @Description: 定时任务配置类
* @author John_Hawkings
* @date 2018年12月25日
* @version V1.0
*/
@Configuration
public class QuartzConfig {
@Autowired
private SpringJobFactory springJobFactory;
@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setJobFactory(springJobFactory);
return schedulerFactoryBean;
}
@Bean
public Scheduler scheduler() {
return schedulerFactoryBean().getScheduler();
}
}
package com.zhy.springboot.quartz;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;
/**
* @Title: SpringJobFactory.java
* @Package com.zhy.springboot.quartz
* @Description: 配置job交给spring管理,主要目的就是解决job类 注入其他service或者使用Spring组件
* @author John_Hawkings
* @date 2018年12月25日
* @version V1.0
*/
@Component
public class SpringJobFactory extends AdaptableJobFactory {
@Autowired
private AutowireCapableBeanFactory capableBeanFactory;
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
Object jobInstance = super.createJobInstance(bundle);
capableBeanFactory.autowireBean(jobInstance);
return jobInstance;
}
}
自定义注解标记定时任务类
package com.zhy.springboot.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Title: QuartzJob.java
* @Package com.zhy.springboot.annotation
* @Description: 时间表达式注解
* @author John_Hawkings
* @date 2018年12月25日
* @version V1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface QuartzJob {
String cron();//表达式
String groupName();
String triggerName();
}
定时任务,实现Quartz Job接口
package com.zhy.springboot.quartz.task;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.zhy.springboot.annotation.QuartzJob;
@Component
@QuartzJob(cron="0/10 * * * * ?",groupName="QuartzTest1Group",triggerName="QuartzTest1Trigger")
public class QuartzTest1 implements Job{
private Logger logger = LoggerFactory.getLogger(QuartzTest1.class);
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
logger.info("QuartzTest1 Has Started");
}
}
package com.zhy.springboot.quartz.task;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.zhy.springboot.annotation.QuartzJob;
@Component
@QuartzJob(cron="0/5 * * * * ?",groupName="QuartzTest2Group",triggerName="QuartzTest2Trigger")
public class QuartzTest2 implements Job{
private Logger logger = LoggerFactory.getLogger(QuartzTest2.class);
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
logger.info("QuartzTest2 Has Started");
}
}
监听启动类装配定时
package com.zhy.springboot.quartz;
import java.util.Map;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import com.zhy.springboot.annotation.QuartzJob;
@Configuration
public class ApplicationStartQuartzJobListener implements ApplicationListener {
private static Logger logger = LoggerFactory.getLogger(ApplicationStartQuartzJobListener.class);
@Autowired
private QuartzSchedulerHandler quartzSchedulerHandler;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
try {
Map beansWithAnnotation = event.getApplicationContext().getBeansWithAnnotation(QuartzJob.class);
quartzSchedulerHandler.startJobs(beansWithAnnotation);
logger.info("Quartz Taks Were Started!!!");
} catch (Exception e) {
logger.error("Quartz Taks Started Failed!!! Cause By: "+e.getMessage(),e);
}
}
//初始注入scheduler
@Bean
public Scheduler scheduler() throws SchedulerException{
SchedulerFactory schedulerFactoryBean = new StdSchedulerFactory();
return schedulerFactoryBean.getScheduler();
}
}
定时任务加载处理
package com.zhy.springboot.quartz;
import java.util.Map;
import org.quartz.CronScheduleBuilder;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.zhy.springboot.annotation.QuartzJob;
@Component
public class QuartzSchedulerHandler {
private static Logger logger = LoggerFactory.getLogger(QuartzSchedulerHandler.class);
private static final String JOB_GROUP = "event_job_group";
private static final String TRIGGER_GROUP = "event_trigger_group";
@Autowired
private Scheduler scheduler;
/**
* 创建定时任务
* @param jobDetailName
* @param cronExpression
* @param jobClass
* @throws SchedulerException
*/
public void createScheduleJob(String groupName,String triggerName, String cronExpression, Class jobClass) throws SchedulerException {
JobDetail jobDetail = JobBuilder.newJob(jobClass)
.withIdentity("task_" + groupName, JOB_GROUP).storeDurably().requestRecovery().build();
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
Trigger trigger = TriggerBuilder.newTrigger().withIdentity("task_" + triggerName, TRIGGER_GROUP).withSchedule(scheduleBuilder).build();
scheduler.scheduleJob(jobDetail, trigger);
}
@SuppressWarnings("unchecked")
public void startJobs(Map beansWithAnnotation) {
try {
if(beansWithAnnotation.isEmpty()) {
logger.info("There is not Quartz Task!!!");
return;
}
for (String beanName : beansWithAnnotation.keySet()) {
Class jobClass = (Class) beansWithAnnotation.get(beanName).getClass();
QuartzJob quartzJob = beansWithAnnotation.get(beanName).getClass().getAnnotation(QuartzJob.class);
try {
createScheduleJob(quartzJob.groupName(),quartzJob.triggerName(),quartzJob.cron(),jobClass);
} catch (Exception e) {
logger.error("Quartz Task Has Set Failed,The Question Task GroupName Was: "+quartzJob.groupName(),e);
continue;
}
}
scheduler.start();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
从最上面的图可以看到这种方式是可以不用开启定时任务注解的,因为是通过Quartz来执行与管理的定时任务,而不是spring本事管理的。