Java-SpringBoot使用@Scheduled与scheduledExecutorService
简要分析如何通过@Scheduled注解类或者是ScheduledExecutorService方法实现定时执行任务的功能。
以及在使用过程中遇到的注入失败问题。
简单实现定时器的方式一般是采用SpringBoot提供的@Scheduled
注解,这个方法能够很方便的实现一个类似于Linux crontab
的功能。
如果只是一个固定时间间隔执行的任务,也可以使用jdk5提供的ScheduledExecutorService
类里的scheduleAtFixedRate
等方法。
这次的定时任务是通过Service层的方法,查询数据库数据,涉及到两个基础类,采用Spring的@Service与@Repository注解:
ProcessAnalyse类: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
public class ProcessAnalyseImpl implements ProcessAnalyse, Runnable {
private PlatFormDao platFormDao;
private Long flowId;
public ProcessAnalyseImpl(Long flowId) {
this.flowId = flowId;
}
public ProcessAnalyseImpl() {
}
public Long getFlowId() {
return flowId;
}
public void setFlowId(Long flowId) {
this.flowId = flowId;
}
public IapDFlowConfig findBillProcess(Long flowId) {
return platFormDao.queryIapDFlowConfigByFlowId(flowId);
}
public void run() {
// System.out.println(Thread.currentThread().getId());
// System.out.println(platFormDao);
platFormDao = ApplicationContextUtil.getBean(PlatFormDao.class);
// System.out.println(platFormDao);
try {
IapDFlowConfig iapDFlowConfig = this.findBillProcess(this.flowId);
} catch (Exception e) {
e.printStackTrace();
}
}
public void run(Long flowId) {
// System.out.println(Thread.currentThread().getId());
try {
IapDFlowConfig iapDFlowConfig = this.findBillProcess(flowId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
PlatFormDao实现类:1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PlatFormDaoImpl implements PlatFormDao {
private EntityManager entityManager;
public IapDFlowConfig queryIapDFlowConfigByFlowId(Long flowId) {
String sqlString = "...";
Query query = entityManager.createNativeQuery(sqlString, IapDFlowConfig.class);
IapDFlowConfig iapDFlowConfig = (IapDFlowConfig) query.getSingleResult();
return iapDFlowConfig;
}
}
下面先看一下采用定时器与线程的方法
@Scheduled注解
使用起来与crontab具有相同的逻辑
使用@Scheduled
注解需要搭配@EnableScheduling
,具体的使用方式为:
1、启动类中增加@EnableScheduling
注解,在执行定时任务的类上定义也可以1
2
3
4
5
6
7
8
9
public class BillStateAnalyseApplication {
public static void main(String[] args) {
SpringApplication.run(BillStateAnalyseApplication.class, args);
}
}
2、在定时任务方法上使用@Scheduled
注解1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CronServiceImpl implements CronService {
private ProcessAnalyse processAnalyse;
private final static long [] flowIdList = {2270L, 2267L};
"0 * * * * ?") (cron =
public Boolean run() {
for (int i = 0, len = flowIdList.length; i < len; i++) {
processAnalyse.run(flowIdList[i]);
}
return null;
}
}
scheduledExecutorService.scheduleAtFixedRate线程池方法
通过外部触发定时的线程池启动,设置延迟周期实现定时执行任务1
2
3
4
5
6
7
8
9
10
11
12
public class CronServiceImpl implements CronService {
private final static ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
private final static long [] flowIdList = {2270L, 2267L};
public Boolean run() {
for (int i = 0, len = flowIdList.length; i < len; i++) {
scheduledExecutorService.scheduleAtFixedRate(new ProcessAnalyseImpl(flowIdList[i]), 0, 1, TimeUnit.MINUTES);
}
return null;
}
}
总结
做的时候遇到了三个问题,这两个问题都是在做线程池的时候遇到的。
第一个问题
最开始做的时候Runnable
的run()
方法中没判断异常(或者叫中断,《java并发编程实战》第7章的内容),导致数据库查询出现Exception后,控制台没有输出任何异常信息,看起来是个完美运行,但是没有结果,定时任务也仅执行了一次。
上面的ProcessAnalyseService
类中的run()
方法仅进行了异常捕获,因为测试的时候这个异常不再需要中断冒泡或者被上一层捕获,就没再恢复中断或者处理中断。注意生产中有必要的话,得慎重处理。
第二个问题
dao层的对象无法通过@Autowired注入。这个问题困扰了大概三个小时……
这个问题的所在是在Spring中,通过new ProcessAnalyseImpl()
方法创建的对象,其中的注解类注入都不生效,因为这个Bean没有交给Spring的上下文来管理。(一开始以为是多线程导致子进程中拿不到上下文中的bean)
解决方案就是在使用到dao对象的时候,重新从上下文中获取:1
ApplicationContextUtil.getBean(PlatFormDao.class);
ApplicationContextUtil类如下;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
public class ApplicationContextUtil implements ApplicationContextAware {
/**
* 上下文对象实例
*/
private static ApplicationContext applicationContext;
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 获取applicationContext
*
* @return
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 通过name获取 Bean.
*
* @param name
* @return
*/
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
/**
* 通过class获取Bean.
*
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
/**
* 通过name,以及Clazz返回指定的Bean
*
* @param name
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}
针对第二个问题,线程池似乎也可以这么实现:通过注解类注入实现了Runnable接口的ProcessAnalyseImpl
,然后通过设置不同参数来执行。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CronServiceImpl implements CronService {
private ProcessAnalyseImpl processAnalyse;
private final static ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
private final static long [] flowIdList = {2270L, 2267L};
public Boolean run() {
for (int i = 0, len = flowIdList.length; i < len; i++) {
processAnalyse.setFlowId(flowIdList[i]);
scheduledExecutorService.scheduleAtFixedRate(new ProcessAnalyseImpl(flowIdList[i]), 0, 1, TimeUnit.MINUTES);
}
return null;
}
}
当然了,上面这个方法是不对的,因为没有考虑多线程中的线程安全问题,两个线程都是用的同一个processAnalyse对象,这个对象执行processAnalyse.setFlowId(flowIdList[i]);
会影响到两个线程。
第三个问题
跟第二个问题最后的解决方案差不多的问题,EntityManager不是线程安全的,尤其涉及事务的话得考虑考虑。