Springboot动态定时任务

项目上遇到用户动态调整定时任务执行时间的需求,一开始是使用数据库存储任务执行时间的方式,每次执行任务之前会从数据库中重新读取一次配置信息,从而更改下次执行时间。这样会有一个问题,比如任务是每天10:00执行,用户在11点更新下次执行时间,但实际第二天是未生效的,要在下一个执行周期才会生效,不符合需求。最后使用反射实现。

1.编写反射工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
public class BeanUtils {
public static Field findField(Class<?> clazz, String name) {
try {
return clazz.getField(name);
} catch (NoSuchFieldException ex) {
return findDeclaredField(clazz, name);
}
}

public static Field findDeclaredField(Class<?> clazz, String name) {
try {
return clazz.getDeclaredField(name);
} catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null) {
return findDeclaredField(clazz.getSuperclass(), name);
}
return null;
}
}

public static Method findMethod(Class<?> clazz, String methodName, Class<?>... paramTypes) {
try {
return clazz.getMethod(methodName, paramTypes);
} catch (NoSuchMethodException ex) {
return findDeclaredMethod(clazz, methodName, paramTypes);
}
}

public static Method findDeclaredMethod(Class<?> clazz, String methodName, Class<?>... paramTypes) {
try {
return clazz.getDeclaredMethod(methodName, paramTypes);
} catch (NoSuchMethodException ex) {
if (clazz.getSuperclass() != null) {
return findDeclaredMethod(clazz.getSuperclass(), methodName, paramTypes);
}
return null;
}
}

public static Object getProperty(Object obj, String name) throws NoSuchFieldException {
Object value;
Field field = findField(obj.getClass(), name);
if (field == null) {
throw new NoSuchFieldException("no such field [" + name + "]");
}
boolean accessible = field.isAccessible();
field.setAccessible(true);
try {
value = field.get(obj);
} catch (Exception e) {
throw new RuntimeException(e);
}
field.setAccessible(accessible);
return value;
}

public static void setProperty(Object obj, String name, Object value) throws NoSuchFieldException {
Field field = findField(obj.getClass(), name);
if (field == null) {
throw new NoSuchFieldException("no such field [" + name + "]");
}
boolean accessible = field.isAccessible();
field.setAccessible(true);
try {
field.set(obj, value);
} catch (Exception e) {
throw new RuntimeException(e);
}
field.setAccessible(accessible);
}

public static Map<String, Object> obj2Map(Object obj, Map<String, Object> map) {
if (map == null) {
map = new HashMap<>();
}
if (obj != null) {
try {
Class<?> clazz = obj.getClass();
do {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
int mod = field.getModifiers();
if (Modifier.isStatic(mod)) {
continue;
}
boolean accessible = field.isAccessible();
field.setAccessible(true);
map.put(field.getName(), field.get(obj));
field.setAccessible(accessible);
}
clazz = clazz.getSuperclass();
} while (clazz != null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return map;
}

/**
* 获得父类集合,包含当前class
*
* @param clazz
* @return
*/
public static List<Class<?>> getSuperclassList(Class<?> clazz) {
List<Class<?>> clazzes = new ArrayList<>(3);
clazzes.add(clazz);
clazz = clazz.getSuperclass();
while (clazz != null) {
clazzes.add(clazz);
clazz = clazz.getSuperclass();
}
return Collections.unmodifiableList(clazzes);
}
}

2.定时任务配置类

通过反射在程序运行时获取到定时任务对象,对其进行添加、删除、重置任务等操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
@Configuration
@EnableScheduling
public class CompleteScheduleConfig implements SchedulingConfigurer {
private ScheduledTaskRegistrar taskRegistrar;
private Set<ScheduledFuture<?>> scheduledFutures = null;
private final Map<String, ScheduledFuture<?>> taskFutures = new ConcurrentHashMap<>();

@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
this.taskRegistrar = taskRegistrar;
}

@SuppressWarnings("unchecked")
private Set<ScheduledFuture<?>> getScheduledFutures() {
if (scheduledFutures == null) {
try {
// spring版本不同选用不同字段scheduledFutures
scheduledFutures = (Set<ScheduledFuture<?>>) BeanUtils.getProperty(taskRegistrar, "scheduledTasks");
} catch (NoSuchFieldException e) {
throw new SchedulingException("not found scheduledFutures field.");
}
}
return scheduledFutures;
}

/**
* 添加任务
*/
public void addTriggerTask(String taskId, TriggerTask triggerTask) {
if (taskFutures.containsKey(taskId)) {
throw new SchedulingException("the taskId[" + taskId + "] was added.");
}
TaskScheduler scheduler = taskRegistrar.getScheduler();
ScheduledFuture<?> future = scheduler.schedule(triggerTask.getRunnable(), triggerTask.getTrigger());
getScheduledFutures().add(future);
taskFutures.put(taskId, future);
}

/**
* 取消任务
*/
public void cancelTriggerTask(String taskId) {
ScheduledFuture<?> future = taskFutures.get(taskId);
if (future != null) {
future.cancel(true);
}
taskFutures.remove(taskId);
getScheduledFutures().remove(future);
}

/**
* 重置任务
*/
public void resetTriggerTask(String taskId, TriggerTask triggerTask) {
cancelTriggerTask(taskId);
addTriggerTask(taskId, triggerTask);
}

/**
* 任务编号
*/
public Set<String> taskIds() {
return taskFutures.keySet();
}

/**
* 是否存在任务
*/
public boolean hasTask(String taskId) {
return this.taskFutures.containsKey(taskId);
}

/**
* 任务调度是否已经初始化完成
*/
public boolean inited() {
return this.taskRegistrar != null && this.taskRegistrar.getScheduler() != null;
}
}

3.转换时间日期为cron表达式工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CronUtils {
/***
* 功能描述:日期转换cron表达式
* @param date
* @return
*/
public static String formatDateByPattern(Date date) {
final SimpleDateFormat sdf = new SimpleDateFormat("ss mm HH dd MM ?");
String formatTimeStr = null;
if (Objects.nonNull(date)) {
formatTimeStr = sdf.format(date);
}
return formatTimeStr;
}

public static String formatTimeByPattern(Time time) {
final SimpleDateFormat sdf = new SimpleDateFormat("ss mm HH * * ?");
String formatTimeStr = null;
if (Objects.nonNull(time)) {
formatTimeStr = sdf.format(time);
}
return formatTimeStr;
}
}

4.初始化定时任务

因为程序的定时任务信息是从数据库中获取的,所以我们要在springboot容器创建完成之后对定时任务进行初始化操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Slf4j
@Component
public class ContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> {
private final CompleteScheduleConfig completeScheduleConfig;
private final AlarmService alarmService;
private final AlarmConfMapper alarmConfMapper;

@Autowired
public ContextRefreshedListener(AlarmService alarmService, AlarmConfMapper alarmConfMapper, CompleteScheduleConfig completeScheduleConfig) {
this.alarmService = alarmService;
this.alarmConfMapper = alarmConfMapper;
this.completeScheduleConfig = completeScheduleConfig;
}

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
log.debug("初始化定时任务");
Time time = alarmConfMapper.queryEmployeeCheckTime();
if (ObjectUtils.isNotEmpty(time)) {
completeScheduleConfig.addTriggerTask("employeeCountAlarm", new TriggerTask(
alarmService::employeeCountAlarm,
new CronTrigger(CronUtils.formatTimeByPattern(time))));
}
}
}

5.重置定时任务时间

接收到用户更新的任务执行时间之后,对当前任务进行重置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@PutMapping("/conf/{id}")
public BaseResponse<String> updateAlarmConf(@PathVariable Integer id, @RequestBody AlarmConfDto alarmConfDto) {
Boolean success = alarmService.updateAlarmConf(id, alarmConfDto);
if (success) {
Time time = alarmConfMapper.queryEmployeeCheckTime();
if (ObjectUtils.isNotEmpty(time)) {
completeScheduleConfig.resetTriggerTask("employeeCountAlarm", new TriggerTask(
alarmService::employeeCountAlarm,
new CronTrigger(CronUtils.formatTimeByPattern(time))));
} else {
completeScheduleConfig.cancelTriggerTask("employeeCountAlarm");
}
return BaseResponse.ok("告警规则修改成功");
}
return BaseResponse.ok("告警规则修改失败");
}