简要分析如何通过@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
@Service
public class ProcessAnalyseImpl implements ProcessAnalyse, Runnable {
@Autowired
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;
}

@Override
public IapDFlowConfig findBillProcess(Long flowId) {
return platFormDao.queryIapDFlowConfigByFlowId(flowId);
}

@Override
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();
}
}

@Override
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
@Repository
public class PlatFormDaoImpl implements PlatFormDao {

@PersistenceContext
private EntityManager entityManager;

@Override
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
@SpringBootApplication
@EnableScheduling
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
@Service
public class CronServiceImpl implements CronService {
@Autowired
private ProcessAnalyse processAnalyse;

private final static long [] flowIdList = {2270L, 2267L};

@Scheduled(cron = "0 * * * * ?")
@Override
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
@Service
public class CronServiceImpl implements CronService {
private final static ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
private final static long [] flowIdList = {2270L, 2267L};
@Override
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;
}
}

总结

做的时候遇到了三个问题,这两个问题都是在做线程池的时候遇到的。

第一个问题

最开始做的时候Runnablerun()方法中没判断异常(或者叫中断,《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
@Component
public class ApplicationContextUtil implements ApplicationContextAware {
/**
* 上下文对象实例
*/
private static ApplicationContext applicationContext;

@Override
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
@Service
public class CronServiceImpl implements CronService {
@Autowired
private ProcessAnalyseImpl processAnalyse;

private final static ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
private final static long [] flowIdList = {2270L, 2267L};
@Override
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不是线程安全的,尤其涉及事务的话得考虑考虑。

参考文章

ScheduledExecutorService 延迟/周期执行线程池

SpringBoot2多线程无法注入问题