Spring自带定时任务相关类位于spring-context包
@Scheduled
标记方法定时执行。所标记的方法必须没有参数,返回值会被忽视。以下属性必须满足一个:
cron
支持cron表达式,不支持year字段fixedDelay
上次调用结束和下次调用开始间隔时间,单位毫秒fixedDelayString
支持毫秒字符串、占位符、符合java.time.Duration
解析的字符串fixedRate
上次调用开始和下次调用开始间隔时间,单位毫秒fixedRateString
和fixedDelayString
格式类似还有两个延迟属性
initialDelay
初始延迟毫秒数,默认-1initialDelayString
和fixedDelayString
格式类似@EnableScheduling
开启任务调度,可以和@SpringBootApplication
一起使用,或者和@Configuration
一起,会确保配置类中或者使用@ComponentScan
所扫描到的带有@Scheduled
注解的Bean任务执行。
默认情况下,会在配置类中优先搜索org.springframework.scheduling.TaskScheduler
类型或名字为taskScheduler
的Bean,如果没找到会搜索java.util.concurrent.ScheduledExecutorService
类型的Bean,如果二者都没找到,会默认通过ConcurrentTaskScheduler
创建一个单线程的ScheduledExecutorService
( SpringBoot 2.1.0 后会默认创建ThreadPoolTaskExecutor
不再是单线程)
Executors.newSingleThreadScheduledExecutor()
@Async
标记目标方法异步执行,标记类代表所有方法异步执行,不能标记带有@Configuration
的类。
目标方法返回值被限定为void
、java.util.concurrent.Future
java.util.concurrent.Executor
或org.springframework.core.task.TaskExecutor
的Bean名字,存在多个实例,可以指定Bean名字执行方法@EnableAsync
开启异步方法执行,和@EnableScheduling
类似,会搜索org.springframework.core.task.TaskExecutor
,或名字叫taskExecutor
的java.util.concurrent.Executor
类型的Bean,两个都没找到会默认创建org.springframework.core.task.SimpleAsyncTaskExecutor
。
返回值为void的方法,异常不能被捕获,为了解决这个问题,可以通过实现AsyncConfigurer
接口自定义配置。
springboot 2.1.0之后自带任务调度器,可以通过以下参数进行配置,也可以自己使用@Configuration
自定义不同的任务调度器
spring:
task:
execution:
pool:
allow-core-thread-timeout: true # 是否允许核心线程超时
core-size: 8 # 核心线程数量
keep-alive: 60s #存活时间
max-size: Integer.MAX_VALUE # 最大线程数量
queue-capacity: Integer.MAX_VALUE #队列容量
ScheduledAnnotationBeanPostProcessor
主要是通过这个类来处理定时任务,这个类是Bean的生命周期接口BeanPostProcessor
的实现类。
postProcessAfterInitialization
通过这个方法查找Bean中@Scheduled
或@Schedules
标记的类和方法
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||
bean instanceof ScheduledExecutorService) {
// Ignore AOP infrastructure such as scoped proxies.
return bean;
}
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
if (!this.nonAnnotatedClasses.contains(targetClass) &&
AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) {
Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
(MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
method, Scheduled.class, Schedules.class);
return (!scheduledMethods.isEmpty() ? scheduledMethods : null);
});
if (annotatedMethods.isEmpty()) {
this.nonAnnotatedClasses.add(targetClass);
}
else {
annotatedMethods.forEach((method, scheduledMethods) ->
scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
}
}
return bean;
}
processScheduled
通过此方法来解析定时任务方法,三种注解所带来不同的定时任务
ScheduledTaskRegistrar
定时任务注册器,可通过此类在配置类中添加定时任务。
定时任务执行的cron表达式存放在数据库中,可动态修改动态执行
DROP TABLE IF EXISTS `cron`;
CREATE TABLE `cron` (
`id` int(11) NOT NULL,
`cron` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
spring:
datasource:
username:
password:
url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useSSL=false&useUnicode=true
driver-class-name: com.mysql.jdbc.Driver
@Mapper
@Repository
public interface CronMapper {
@Select("select cron from cron limit 1")
String getCron();
}
public class OneTask implements Runnable{
private static final Logger logger = LoggerFactory.getLogger(OneTask.class);
@Override
public void run() {
logger.info("->执行");
}
}
@Configuration
@EnableScheduling
public class MyScheduleConfig implements SchedulingConfigurer {
private static final Logger logger = LoggerFactory.getLogger(MyScheduleConfig.class);
@Autowired
private CronMapper mapper;
private String preCron;
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.addTriggerTask(new OneTask(), triggerContext -> {
String cron = mapper.getCron();
// 校验cron表达式是否合法
if (!CronSequenceGenerator.isValidExpression(cron)) {
logger.info("数据库cron表达式非法:{}", cron);
return new CronTrigger( preCron).nextExecutionTime(triggerContext);
}
preCron = cron;
return new CronTrigger(cron).nextExecutionTime(triggerContext);
});
}
}
Scheduler
Quartz实现任务调用最主要的接口,它包含JobDetail
和Trigger
的注册和调度器的执行
SchedulerFactory
提供获取Scheduler
的工厂接口,一般使用quartz提供实现类StdSchedulerFactory
Trigger
触发器接口,和Job执行相关,包含触发开始时间,结束时间, 下次开始时间等信息
SimpleTrigger
给定时间间隔触发,还可以指定次数
CronTrigger
cron表达式触发
CalendarIntervalTrigger
和SimpleTrigger
类似,间隔时间触发,不同的是SimpleTrigger
间隔时间是毫秒,不能指定每个月(毫秒不固定)触发,CalendarIntervalTrigger
可以根据日历单位为时间间隔触发
DailyTimeIntervalTrigger
可以指定每天的某个时间段内,以一定的时间间隔执行任务,支持指定星期
JobDetail
描述Job实例详细属性的接口
Job
定时任务所要实现的接口
ScheduleBuilder
SimpleScheduleBuilder
// 每隔1s执行,重复10次
SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(1)
.withRepeatCount(10);
CronScheduleBuilder
// 每分钟0 10 .. 50秒执行
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ?");
CalendarIntervalScheduleBuilder
// 每隔1个月执行1次
CalendarIntervalScheduleBuilder cbuilder = CalendarIntervalScheduleBuilder.calendarIntervalSchedule()
.withIntervalInMonths(1);
DailyTimeIntervalScheduleBuilder
// 每天9点半到18点半,每隔30秒执行一次
DailyTimeIntervalScheduleBuilder dailyBuilder = DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()
.withIntervalInSeconds(30)
.startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 30))
.endingDailyAt(TimeOfDay.hourAndMinuteOfDay(18, 30));
JobBuilder
JobDetail jobDetail = JobBuilder.newJob(CustomJob.class).withIdentity("job1", "group1").build();
TriggerBuilder
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1")
.startNow()
.withSchedule(simpleScheduleBuilder).build();
实际执行次数=重复次数+1,重复次数和结束时间冲突看谁先结束
示例
public class CustomJob implements Job {
private static final Logger logger = LoggerFactory.getLogger(CustomJob.class);
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
logger.info("-> invoke");
}
}
SchedulerFactory factory = new StdSchedulerFactory();
Scheduler scheduler = factory.getScheduler();
SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(10)
.withRepeatCount(1);
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ?");
CalendarIntervalScheduleBuilder cbuilder = CalendarIntervalScheduleBuilder.calendarIntervalSchedule()
.withIntervalInMonths(1);
DailyTimeIntervalScheduleBuilder dailyBuilder = DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()
.withIntervalInSeconds(30)
.startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 30))
.endingDailyAt(TimeOfDay.hourAndMinuteOfDay(18, 30));
LocalDateTime now = LocalDateTime.now().plusMinutes(1);
ZonedDateTime zonedDateTime = now.atZone(ZoneId.systemDefault());
Instant instant = zonedDateTime.toInstant();
Date newDate = Date.from(instant);
JobDetail jobDetail = JobBuilder.newJob(CustomJob.class).withIdentity("job1", "group1").build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1")
.startNow()
.endAt(newDate)
.withSchedule(simpleScheduleBuilder).build();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
cron表达式是由六个或七个子表达式(字段)组成的字符串,用于描述计划的各个详细信息
秒 分 时 日 月 周 年
字段名字 | 是否必填 | 允许值 | 允许特殊字符 |
---|---|---|---|
秒 | Y | 0-59 | , - * / |
分 | Y | 0-59 | , - * / |
时 | Y | 0-23 | , - * / |
日 | Y | 1-31 | , - * / ? L W C |
月 | Y | 0-11 或 JAN-DEC | , - * / |
周 | Y | 1-7 或 SUN-SAT | , - * / ? L C # |
年 | N | 空或 1970-2099 | , - * / |
特殊字符说明
*
: 代表所有,每个值。例如 *在秒字段,代表每秒?
: 代表无具体值。常用于周和日上,选定某日,不在乎是否周几或者选定周几,不在乎某日。-
用于指定范围。例如1-3 在小时字段代表1,2,3小时,
用于指定一些值。例如1,2,4 在小时字段代表1,2,4小时/
用于指定增量。例如 0/15 在分钟字段代表第0,15,30,45分钟,5/15代表第5,20,35,50分钟L
代表最后一个,在日和周两个字段中,单独使用L,在日中代表月的最后一日,在周中代表周六。如果加上数字,例如6L在周字段上,代表月最后一个星期五。也可以用L-3在日字段上表示月的倒数第三天。W
用于指定最接近的工作日(周一到周五),只能用于日字段。指定的工作日的范围是当前搜索的月,W不能用户日期范围或日期列表。例如:15W 在日字段表示距离15日最近的工作日#
用于指定月第几个周几,只能用于日字段。例如6#3 代表月第三个周五,如果日期不存在则不会触发例子
0 0 12 * * ?
每天12点触发0 0 10-20 * * ?
每天10点到20点 整点触发0 0 10,15,20 * * ?
每天10,15,20点 整点触发0 */10 * * *?
每10分钟触发0 0/10 * * * ?
每小时 0,10,20,30,40,50分时触发0 30 17 25 * ?
每月25号17点30触发0 30 17 25W * ?
每月距离25日最近的工作日 17点30分触发0 30 17 LW * ?
每月最后一个工作日17点30分触发0 30 17 ? * 6L
每月最后礼拜五17点30分触发多机部署实例时,定时任务也会同步执行,涉及数据库操作时,会发生数据库重复写,发生不可预知的错误,这时希望一个任务只有一台机器执行。分布式锁可以解决这种问题。针对这种情况,可以分为以下两种.
如果是简单的定时任务,直接使用@Scheduled注解实现的定时任务,可以使用shedlock这个轻量级框架。官方地址:https://github.com/lukas-krecan/ShedLock
使用自定义定时任务,而不是使用注解,可以考虑redis,使用setnx实现分布式锁功能
TriggerListener
接口的实现类可以监听触发器执行任务,可以在执行之前判断是否能拿到分布式锁,然后判断是否执行。