某退休程序猿的工作日志
本文适用需求场景
原本使用cron表达式或fixedDelay/fixedRate的@Scheduled简单定时任务,但是间隔有时会改变,希望能运行时修改而无需每次更改代码和重启服务器
分析
scheduling-enable-annotation-support
根据文档,想实现更细粒度的控制,需要实现SchedulingConfigurer
@FunctionalInterface
public interface SchedulingConfigurer {
void configureTasks(ScheduledTaskRegistrar taskRegistrar);
}
scheduling-trigger-implementations
想配置可动态修改的定时任务,需要实现自己的Trigger
类
原生的Trigger
实现类只有CronTrigger
、PeriodicTrigger
CronTrigger部分源码
private final CronExpression expression;
private final ZoneId zoneId;
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
Date date = triggerContext.lastCompletionTime();
if (date != null) {
Date scheduled = triggerContext.lastScheduledExecutionTime();
if (scheduled != null && date.before(scheduled)) {
// Previous task apparently executed too early...
// Let's simply use the last calculated execution time then,
// in order to prevent accidental re-fires in the same second.
date = scheduled;
}
}
else {
date = new Date(triggerContext.getClock().millis());
}
ZonedDateTime dateTime = ZonedDateTime.ofInstant(date.toInstant(), this.zoneId);
ZonedDateTime next = this.expression.next(dateTime);
return (next != null ? Date.from(next.toInstant()) : null);
}
计算下次执行时间的方法依赖于这两个private final参数,无法被修改
PeriodicTrigger部分源码
private final long period;
private final TimeUnit timeUnit;
private volatile long initialDelay;
private volatile boolean fixedRate;
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
Date lastExecution = triggerContext.lastScheduledExecutionTime();
Date lastCompletion = triggerContext.lastCompletionTime();
if (lastExecution == null || lastCompletion == null) {
return new Date(triggerContext.getClock().millis() + this.initialDelay);
}
if (this.fixedRate) {
return new Date(lastExecution.getTime() + this.period);
}
return new Date(lastCompletion.getTime() + this.period);
}
同样无法修改
为了最大程度兼容已有定时任务,减少修改量,直接基于原有两种Trigger
,创建新的实现类
public class MutableCronTrigger implements Trigger {
private CronExpression expression;
private ZoneId zoneId;
public void reconstruct(String expression) {
reconstruct(expression, ZoneId.systemDefault());
}
public void reconstruct(String expression, TimeZone timeZone) {
reconstruct(expression, timeZone.toZoneId());
}
public void reconstruct(String expression, ZoneId zoneId) {
Assert.hasLength(expression, "Expression must not be empty");
Assert.notNull(zoneId, "ZoneId must not be null");
this.expression = CronExpression.parse(expression);
this.zoneId = zoneId;
}
...
可通过reconstruct
方法重新修改参数
public class MutablePeriodicTrigger implements Trigger {
public void setPeriod(long period) {
this.period = period;
}
private volatile long period;
private final TimeUnit timeUnit;
private volatile long initialDelay;
private volatile boolean fixedRate;
...
可修改执行间隔即period
,fixedRate
(Rate/Delay切换)setter函数原就是public
自定义ISchedule接口
这里设计一个接口是为了
- 方便一次加载所有自定义的定时任务
- 便于已有@Scheduled任务升级(原定时任务方法放在
execute
中,注解的参数单独拿出作为Trigger
)
import org.springframework.scheduling.Trigger;
public interface ISchedule {
void execute();
Trigger trigger();
}
SchedulingConfigurer 实现类
@Configuration
@EnableScheduling
public class SchedulerConfig implements SchedulingConfigurer {
@Autowired
List<ISchedule> scheduleList;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
if (scheduleList != null && scheduleList.size() != 0)
scheduleList.forEach(schedule -> taskRegistrar.addTriggerTask(schedule::execute, schedule.trigger()));
}
}
测试类
@Configuration
public class TestSchedule implements ISchedule {
@Override
@Async
public void execute() {
System.out.println("------");
System.out.println(new Date());
System.out.println("------");
}
@Override
public Trigger trigger() {
return trigger;
}
Trigger trigger = new MutableCronTrigger("*/5 * * * * *");
}
启动项目,成功打印日志
@RestController
public class TestController {
@Autowired
BlockSchedule blockSchedule;
@RequestMapping("test")
public void test() {
((MutableCronTrigger) blockSchedule.trigger()).reconstruct("*/10 * * * * *");
}
}
测试修改,每整10秒一次,成功(最后一次会按老的cron执行,然后nextExecutionTime
方法获取下一次执行时间才会改变)
后记
@Scheduled只支持单机,轻量便于快速开发,如果有集群需求,更换quartz