SpringTask动态配置定时任务的使用

SpringTask动态配置定时任务的使用

一、需求

  • 在项目开发中、根据业务的需求需要要在在一定的时间或每隔多长时间去执行特定的程序,比如:每天凌晨去统计一些报表。

二、技术分类

2.1、java自带的java.util.Timer

这个类允许调度一个`java.utils.TimerTask`任务。使用这种方式可以让程序在某一个时间间隔循环执行,但不能在指定时间运行。(一般用的较少)

2.2、使用Quartz

这个是一个功能比较强大的任务调度器,可以让你的程序在每到指定时间执行,也可以按照某一时间间隔循环执行。在功能强大的同时,配置起来稍微的复杂。

2.3、SpringTask(自Spring3.0以后自带task功能)

SpringTask可以看成一个轻量级的Quartz,用起来比Quartz简单的多,配置也比较少,和自家的集成也是非常方便。(同样支持每隔指定时间触发执行、每到指定时间触发执行)
  • 注:
    • 时间间隔为毫秒
    • 指定时间为cron表达式

三、SpringTask普通用法说明

3.1、SpringTask简述

本文使用的是Spring3.0以后自主开发的定时任务工具,他可以看做一个轻量级的Quartz,而且使用起来很简单,除spring相关的包外不需要额外的包,而且支持注解和配置文件两种方式。这里两种方式都介绍一下:

3.2、XML配置文件的使用

  • 第一步:在spring的配置文件中引入命名空间

    1
    2
    3
    4
    xmlns:task="http://www.springframework.org/schema/task"

    http://www.springframework.org/schema/task
    http://www.springframework.org/schema/task/spring-task-3.1.xsd"
  • 第二步:添加定时任务扫描注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <context:component-scan base-package="package" />
    <!-- 定时任务扫描注解(使用@Scheduled时可不配置)-->
    <task:annotation-driven/>
    <task:scheduled-tasks scheduler="scheduler">
    <task:scheduled ref="ReportCacheTask" method="cacheUpdateSchedule" cron="0 17 16 * * ?" />
    </task:scheduled-tasks>

    <task:scheduler id="scheduler" pool-size="10" />
    <!--注意这边需要配置供扫描的包 (如果类上加@Service可不配置)-->
    <bean id="ReportCacheTask" class="com.cym.bip.report.data.cache.ReportCacheTask" />
  • 第三步:创建一个com.cym.bip.report.data.cache.ReportCacheTask类,里面

有个cacheUpdateSchedule方法。(代码省略)

3.3、注解方式(重点)

  • 第一步:在spring的配置文件中引入task的命名空间

    1
    2
    3
    4
    xmlns:task="http://www.springframework.org/schema/task"

    http://www.springframework.org/schema/task
    http://www.springframework.org/schema/task/spring-task-3.1.xsd"
  • 第二步:添加定时任务扫描注解

    1
    2
    3
    4
    5
    <context:component-scan base-package="package" />
    <!-- 定时任务扫描注解(使用@Scheduled时可不配置)-->
    <task:annotation-driven scheduler="scheduler"/>
    <!--配置定时任务的线程池(推荐配,若不配置多任务下可能有问题)-->
    <task:scheduler id="scheduler" pool-size="10" />
  • 第三步:创建定时任务类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package com.leaseBack.apollo;

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.scheduling.annotation.EnableScheduling;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;

    /**
    * 定时获得定时任务列表
    */
    @Component
    @EnableScheduling //可不配置
    public class SpringTimeTask {
    private static Logger logger = LoggerFactory.getLogger(SpringTimeTask.class);

    // @Scheduled(cron = "0 0/6 * * * ?") //根据cron表达式执行
    @Scheduled(fixedDelay = 10 * 1000) //每隔10秒执行一次
    public void timeTaskRun() {
    logger.info("Refresh TimeTask......");
    }
    }

    注:

    • @EnableScheduling:此注解添加在类上表示开启对定时任务的支持(添加此注解,可不在xml中配置注解驱动)
    • @Scheduled:此注解表示在方法上表示此方法按一定时间规则执行

四、SpringTask高级应用(实现动态配置定时任务)

4.1、简述

  • 业务需求

    根据项目需要定时任务方法的逐渐增多和时间规则有可能发变化,以上普通做法在变动时改动比较大,并且定时任务运行起来无法手动操作,不易编码及操作。所以就有了实现动态配置定时任务的想法。

  • 动态定时任务设想

    当我们想定时执行一个任务时,只需要告诉程序要执行的类名(类的全路径)、方法名及cron表达式即可,并且定时任务有开启和关闭功能,根据需求来配置就行。这样做优点:增加了程序的灵活性,减少了硬编码,易于维护。

  • 实现方案

    当我们开启定时任务时,从数据库中获得类的全路径、方法名、cron表达式,根据全路径方法名利用反射机制来新建一个线程去执行方法,并且指定一个cron时间规则

4.2、动态定时任务的实(注解方式)

4.2.1、配置参考普通应用配置(在此省略)
4.2.2、创建一个反射类实现Runnable接口
  • 根据类名,方法名利用反射机制创建一个线程去执行
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
package com.leaseBack.apollo;

import com.apollo.entity.TimeTaskLog;
import com.apollo.interfaces.ITimeTaskLogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.util.Date;

/**
* 利用反射执行方法,并将日志持久化
*/
public class TaskRun implements Runnable {
private static Logger logger = LoggerFactory.getLogger(TaskRun.class);

private String classPath;
private String methodName;

static private ITimeTaskLogService timeTaskLogService= SpringContextHolder.getBean(ITimeTaskLogService.class);

public TaskRun(String classPath, String methodName) {
this.classPath = classPath;
this.methodName = methodName;
}

@Override
public void run() {
try {
//获得字节码对象
Class<?> clazz = Class.forName(classPath);
Object obj = clazz.newInstance();
//获得方法
Method run = clazz.getMethod(methodName);
//执行方法
run.invoke(obj);
logger.info("Start TimeTask Success --" + classPath + "--" + methodName + "--" + new Date().toLocaleString());
} catch (Exception e) {
e.printStackTrace();
logger.error("Start TimeTask Failure --" + classPath + "--" + methodName + "--" + new Date().toLocaleString());
}
}
}
4.2.3、定时任务开启关闭的Service、
  • ThreadPoolTaskScheduler(定时任务线程池)去执行一个线程并且按cron时间表达式去执行任务,将返回的ScheduledFuture(线程信息)保存在private static Map<String, ScheduledFuture> map = new HashMap<>();,后面根据ScheduledFuture信息来进行关闭。
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
package com.leaseBack.apollo;

import com.apollo.entity.TimeTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;

/**
* 启动定时任务
* 关闭定时任务
*/
@Component
public class TaskService {
private static Logger logger = LoggerFactory.getLogger(TaskService.class);

@Autowired
private RedisTemplate redisTemplate;

@Autowired
private ThreadPoolTaskScheduler threadPoolTaskScheduler;
//创建定时任务的线程池
@Bean
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
return new ThreadPoolTaskScheduler();
}

//正在执行的任务
//ScheduledFuture继承了Future是对于具体的 Runnable任务的执行的储存
//可以对ScheduledFuture操作当前线程任务的取消、完成状态、获取结果。
//必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
private static Map<String, ScheduledFuture> map = new HashMap<>();

/**
* 开启定时任务
* @param timeTask
* @return
*/
public boolean startTimeTask(TimeTask timeTask) {

//获得类路径
String classPath = timeTask.getClassPath();
//获得方法名
String methodName = timeTask.getMethodName();
//获得cron表达式
String cron = timeTask.getCron();
//判断线程是否正在运行
ScheduledFuture sFuture = map.get(classPath + methodName);
if (sFuture == null) {
//利用反射获得一个线程
TaskRun taskRun = new TaskRun(timeTask.getId(), classPath, methodName);
//按一定时间规则运行定时任务
ScheduledFuture schedule = threadPoolTaskScheduler.schedule(taskRun, new CronTrigger(cron));
logger.info("Start TimeTask......" + classPath + "--" + methodName + "--" + cron + "--" + new Date().toLocaleString());
//将当前线程信息保存
map.put(classPath + methodName, schedule);
return true;
}
return false;
}

/**
* 停止定时任务
* @param timeTask
* @return
*/
public boolean stopTimeTask(TimeTask timeTask) {
String key = timeTask.getClassPath() + timeTask.getMethodName();
//获得储存线程的信息
ScheduledFuture sf = map.get(key);
if (sf != null) {
//取消当前线程任务
sf.cancel(true);
//从map中删除掉
map.remove(key);
logger.info("Stop TimeTask......" + timeTask.getClassPath() + "--" + timeTask.getMethodName() + "--" + new Date().toLocaleString());
return true;
}
return false;
}

/**
* 获得定时任务详细信息,并且执行对应的方法
*/
public void timeTaskRun() {
BoundHashOperations timetaskRedis = redisTemplate.boundHashOps("timeTask");
Set keys = timetaskRedis.keys();
for (Object key : keys) {
TimeTask timeTask = (TimeTask) timetaskRedis.get(key);
if (timeTask.getIsStart() == 1) {
boolean result = startTimeTask(timeTask);
}
if (timeTask.getIsStart() == 0) {
boolean result = stopTimeTask(timeTask);
timetaskRedis.delete(key);
}
}
}
}

4.2.4、编写定时任务的方法

  • 创建一个普通类
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
package com.timetask.apollo;

import com.apollo.interfaces.ICollectionDailyReportService;
import com.leaseBack.apollo.SpringContextHolder;
import com.xy52.common.utils.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;

/**
* 定时任务报表统计功能
* @author YongZheng
*/
public class ReportStatistics implements ApplicationContextAware {
private static Logger logger = LoggerFactory.getLogger(ReportStatistics.class);

private static ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

/**
* 催收员业绩日报统计
* 按日,统计昨天的数据
*/
public void collectionDailyReport() {
logger.info("collectionDailyReport starting......");
//从applicationContext中获得bean
ICollectionDailyReportService collectionDailyReportService = applicationContext.getBean(ICollectionDailyReportService.class);
collectionDailyReportService.insertCollectionDailyReport(DateUtil.getYesterdayDate(new Date(), null));
logger.info("collectionDailyReport end......");
}
}

五、总结

  • 传输定时任务详细信息时:

    问题:因为p2p-scheduleP2PManager是都web项目service不能相互调用,导致定时任务详细信息不能传递。

    解决方案:一、模仿HTTP请求 二、借助于Redis来储存信息,并且写个定时任务每十秒获得一次Redis中的信息,根据信息去执行(本项目中使用的)

  • 利用反射执行方法时,嵌套了其他service

    问题:利用反射执行方法时,嵌套了其他service(用@Autowired注入),在执行时service报空指针异常,不能正常注入。

    解决方案:一、编写一个工具类 二、实现ApplicationContextAware接口本文章中定时任务的service方法)

  • p2p-schedule项目启动就要执行开启的定时任务

    问题:p2p-schedule项目启动就要执行开启的定时任务,并且还要把所有service都注入之后执行。

    一开始试了好几种方式,都是加载service报错(貌似是null指针异常,具体忘啦),经过一番查资料找到了一些解决办法。

    解决方案:编写一个类实现了ApplicationListener<ContextRefreshedEvent>接口,重写onApplicationEvent方法。业务编码逻辑:当加载完所有Bean时去掉用service服务方法。

    代码如下:

    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
    > package com.leaseBack.apollo;
    >
    > import org.slf4j.Logger;
    > import org.slf4j.LoggerFactory;
    > import org.springframework.beans.factory.annotation.Autowired;
    > import org.springframework.context.ApplicationListener;
    > import org.springframework.context.event.ContextRefreshedEvent;
    > import org.springframework.stereotype.Component;
    > import java.util.Date;
    >
    > /**
    > * 项目启动时执行定时任务
    > */
    > @Component
    > public class StartConstruct implements ApplicationListener<ContextRefreshedEvent> {
    > private static Logger logger = LoggerFactory.getLogger(StartConstruct.class);
    > @Autowired
    > private TaskService taskService;
    >
    > @Override
    > public void onApplicationEvent(ContextRefreshedEvent event) {
    > if (event.getApplicationContext().getParent() == null) {
    > logger.info("Start all TimeTask--"+new Date().toLocaleString());
    > taskService.timeTaskRun();
    > logger.info("Start all TimeTask Success--" + new Date().toLocaleString());
    > }
    > }
    > }
    >
-------------本文结束感谢您的阅读-------------