任务背景
与项目公司进行对接,接入了公司的单点登录系统,同时使用shiro进行了授权相关功能开发。因此需要将该项目相关的该公司的组织机构信息导入到项目数据库中。
由于公司的组织机构处于动态变化中,该组织机构信息的导入不是一劳永逸的,而是需要定时更新的。也就是需要按照一定的频率执行增量更新。
第一反应Timer
定时,看到这个词的第一反应就是定时器Timer。
于是,第一版的实现也就出来了,如下所示:
- 通过Calendar设定每天执行定时任务的时间;
- 通过Timer的schedule方法来启动定时任务:schedule方法有两个重载,一个指定延时,一个指定时间,这里用的是指定时间的方法,第三个参数代表两次任务执行之间的间隔;
- schedule方法的第一个参数是TimerTask子类型,需要override它的run方法,执行的逻辑就写在run方法中;
- 然后实现一个ServletContextListener监听器,在其contextInitialized方法中建立TimerManager,从而启动定时任务;
- web.xml中配置监听器;
public class TimerManager {
private static final long PERIOD_DAY = 24 * 60 * 60 * 1000;
@Autowired
public TimerManager() {
Calendar calendar = Calendar.getInstance();
// 定制每日凌晨2:00执行任务
calendar.set(Calendar.HOUR_OF_DAY, 10);
calendar.set(Calendar.MINUTE, 44);
calendar.set(Calendar.SECOND, 0);
/*
如果第一次执行定时任务的时间小于当前时间:
此时要在第一次执行定时任务的时间上加一天,以便此任务在下一个时间点执行;
如果不加一天,任务会立即执行,循环执行的周期会以当前时间为准
*/
Date date = calendar.getTime();
if (date.before(new Date())) {
date = this.addDay(date, 1);
}
Timer timer = new Timer();
SyncOrgTask task = new SyncOrgTask();
// timer.schedule(task, date, PERIOD_DAY);
}
public Date addDay(Date date, int num) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DAY_OF_MONTH, num);
return calendar.getTime();
}
}
public class SyncOrgTask extends TimerTask {
Logger logger = LoggerFactory.getLogger(SyncOrgTask.class);
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Autowired
private ISyncOrgInfoService syncOrgInfoService;
public void run() {
try {
List<Organization> orgList = Collections.synchronizedList(new ArrayList<Organization>());
Map<String, String> parentOrgMap = Collections.synchronizedMap(new HashMap<String, String>());
List<Staff> staffList = Collections.synchronizedList(new ArrayList<Staff>());
Map<String, String> staffOrgMap = Collections.synchronizedMap(new HashMap<String, String>());
// 部分项目公司内部接口调用……
logger.info("begin transfer into DB");
if (syncOrgInfoService != null) {
logger.info("syncOrgInfoService is not null");
syncOrgInfoService.syncOrgInfo(orgList, parentOrgMap, staffList, staffOrgMap);
}
logger.info("end transfer into DB");
logger.info("执行任务时间:" + sdf.format(Calendar.getInstance().getTime()));
} catch (Exception e) {
e.printStackTrace();
}
logger.info("sync org succeed");
}
}
public class SyncOrgTaskListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
new TimerManager();
}
public void contextDestroyed(ServletContextEvent sce) {
}
}
<listener>
<listener-class>
com.xxx.listener.SyncOrgTaskListener
</listener-class>
</listener>
问题出现
根据调试需要设定我们定时任务的执行时间,然后启动调试。
问题出现在SyncOrgTask(TimerTask子类)中,通过Spring依赖注入向该类注入了一个执行写入数据库逻辑的Service对象(在该Service对象中启用了事务),如下:
@Autowired
private ISyncOrgInfoService syncOrgInfoService;
但是在调试过程中发现,该service对象为null。
问题分析如下:
- 该SyncOrgTask类没有添加Spring相关注解以使得该类的对象被Spring管理起来,因此在该类上怎加了@Component注解;
@Component(value = "syncOrgTask")
public class SyncOrgTask /*extends TimerTask*/
- 该类所在的包没有被Spring扫描到,因此在applicationContext.xml中增加了包扫描路径,如下:
<context:component-scan base-package="com.xxx.auth.task"/>
经过以上处理以后,问题依然存在。
问题再定位
怀疑是由于定时任务导致的问题,所以尝试通过ajax请求来调用该方法。在某个Controller中提供相应的请求接口如下:
@Autowired
private SyncOrgTask syncOrgTask;
@PostMapping(value = "/xxx")
public String xxx() {
syncOrgTask.run();
return "success";
}
启动服务,通过浏览器控制台发送请求并进行断点跟踪,发现SyncOrgTask运行正常,且service对象正常注入。
因此问题并没有出在SyncOrgTask这个类中,而是出现在TimerManager中了。
给TimerManager增加@Component注解后依然不起作用。
此时发现对于SyncOrgTask的调用是通过如下方式进行的:
Timer timer = new Timer();
SyncOrgTask task = new SyncOrgTask();
timer.schedule(task, date, PERIOD_DAY);
于是问题明白了,SyncOrgTask是由Timer在启动的时候新创建new出来的,而不是由Spring容器管理的。所以其service对象无法注入也就可以理解了。
解决办法初探
既然SyncOrgTask是由TimerManager自行创建导致了问题,那么我们的解决办法就是让SyncOrgTask对象自动注入TimerManager。
@Autowired
private SyncOrgTask task;
启动调试,直接报错!原因是timer.schedule的第一个参数,也就是自动注入的这个task为null。
此处没有再跟踪,简单分析就是Spring容器的启动和timer的启动不在一个频道上,timer要启动任务的时候,Spring还没有完成相关bean的管理,因此没有完成注入。
此时问题就比较麻烦起来,通过@lazy等方式也没有能够解决问题。
放弃Timer
最开始做这一块任务的时候,看过Quartz这个开源作业调度框架。但是当时觉得太重型了,有点杀鸡用牛刀的感觉。
了解了Spring中定时任务实现的时候,发现Spring task这个组件就已经可以实现定时任务的功能,而且实现起来非常简洁明快,如下:
- 实现一个POJO,注解为@Component;
- 在该类中实现定时任务逻辑的方法,并注解@Scheduled,其参数cron标识了一个时间触发器;@Schedule还可以通过fixedRate或者fixedDelay来指定频率触发器、延时触发器等;
- 在Spring的配置文件applicationContext.xml中增加相应的包扫描,并配置task通过注解驱动;
@Component(value = "syncOrgTask")
public class SyncOrgTask {
Logger logger = LoggerFactory.getLogger(SyncOrgTask.class);
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Autowired
private ISyncOrgInfoService syncOrgInfoService;
@Scheduled(cron = "0 4 14 * * ?")
public void syncOrg() {
try {
List<Organization> orgList = Collections.synchronizedList(new ArrayList<Organization>());
Map<String, String> parentOrgMap = Collections.synchronizedMap(new HashMap<String, String>());
List<Staff> staffList = Collections.synchronizedList(new ArrayList<Staff>());
Map<String, String> staffOrgMap = Collections.synchronizedMap(new HashMap<String, String>());
// 部分项目公司内部接口调用……
logger.info("begin transfer into DB");
if (syncOrgInfoService != null) {
logger.info("syncOrgInfoService is not null");
syncOrgInfoService.syncOrgInfo(orgList, parentOrgMap, staffList, staffOrgMap);
}
logger.info("end transfer into DB");
logger.info("执行任务时间:" + sdf.format(Calendar.getInstance().getTime()));
} catch (Exception e) {
e.printStackTrace();
}
logger.info("sync org succeed");
}
}
<context:component-scan base-package="com.hdzbk.auth.task"/>
<!-- spring task -->
<task:annotation-driven scheduler="scheduler" mode="proxy"/>
<task:scheduler id="scheduler" pool-size="10"/>
后记:Spring真是NB!