定时任务
项目开发中会涉及很多需要定时执行的代码,如每日凌晨对前一日的数据进行汇总,或者系统缓存的清理、对每日的数据进行分析和总结等需求,这些都是定时任务。单体系统和分布式系统的分布式任务有很大的区别,单体系统就一个任务执行类,非常简单,分布式系统则要保证定时任务执行的唯一性,不能让一个定时任务被执行多次。
实现定时任务的5种方式
Java定时任务目前主要有以下5种实现方式。
JDK自带的实现方式,如JDK自带的Timer和JDK 1.5+新增的ScheduledExecutor- Service;
elastic-job:功能完备的分布式定时任务框架;
Spring 3.0以后自带的task:可以将它看成一个轻量级的任务调度;
使用Quartz实现定时任务;
分布式任务调度:可以使用国产组件XXL-Job实现。
下面分别讲解不同的定时任务的实现。
实战:基于JDK方式实现简单定时
使用JDK方式实现定时任务有两种方法:
(1)第一种是使用Timer类进行实现,Timer是JDK自带的定时任务执行类,任何项目都可以直接使用Timer来实现定时任务,因此Timer的优点就是使用方便。但是Timer的缺点也很明显,这是个单线程的实现,如果任务执行时间太长或者发生异常,则会影响其他任务执行。在开发和测试环境中可以用Timer类进行测试,强烈建议在生产环境中谨慎使用,使用Timer实现的定时任务代码如下:
package com.example.springextenddemo.dingshi;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Timer;
import java.util.TimerTask;
public class TimerDemo {
//定义时间格式
private static DateTimeFormatter pattern =
DateTimeFormatter.ofPattern
("yyyy-MM-dd HH:MM:ss");
public static void main(String[] args) {
Timer timer = new Timer();
/**
* 从当前时刻开始,每1s执行一次,方法的入参单位为毫秒(ms,1000毫秒即
1s)
*/
timer.schedule(new MyTask(),0,1000);
}
/**
* 自定义任务实现
*/
private static class MyTask extends TimerTask {
@Override
public void run() {
LocalDateTime now = LocalDateTime.now();
System.out.println("这是定时任务,时间
是:"+pattern.format(now));
}
}
}
执行当前的main()方法,可以看到控制台打印的定时任务日志如图6.10所示。
图6.10 Timer定时任务
Timer类设定定时任务有以下3种重载方法:
schedule(TimerTask task, long delay):延迟delay毫秒再执行任务;
schedule(TimerTask task, Date time):在特定的时间执行任务;
schedule(TimerTask task, long delay, long period):延迟delay毫秒执行并每隔period毫秒执行一次。
(2)使用JDK实现定时任务的第二种方式就是使用ScheduledExecutorService类。该类是Java 1.5后新增的定时任务接口,主要有以下几种方法:
public interface ScheduledExecutorService extends ExecutorService {
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit); public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable
command,
long initialDelay,
long delay,
TimeUnit unit);
}
ScheduledExecutorService类的基本原理和Timer相似,下面使用ScheduledExecutor- Service实现和Timer一样的定时任务功能:
package com.example.springextenddemo.dingshi;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledExecutorServiceDemo {
//时间格式
private static DateTimeFormatter pattern =
DateTimeFormatter.ofPattern
("yyyy-MM-dd HH:MM:ss");
public static void main(String[] args) {
ScheduledExecutorService service =
Executors.newScheduledThreadPool(1);
service.scheduleAtFixedRate(() -> { LocalDateTime now = LocalDateTime.now();
System.out.println("schedule 这是定时任务,时间是:" +
pattern.format(now));
}, 0, 1000, TimeUnit.MILLISECONDS);
}
}
执行main()方法,控制台打印的日志如图6.11所示,与上面的Timer实现了相同的效果。在开发过程中,如果只是简单的定时任务,建议直接采用ScheduleExecutorsService类来处理,这是线程池技术,能够实现线程的复用。
实战:基于Spring Task实现定时任务
Spring Task的核心实现类位于spring-context包中,在Spring项目中可以直接使用该定时任务类。下面演示Spring Task定时任务的实现过程。添加一个新的类SpringTaskDemo,代码如下:
package com.example.springextenddemo.dingshi;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@EnableScheduling //开启定时任务
@Component
public class SpringTaskDemo {
private static DateTimeFormatter pattern =
DateTimeFormatter.ofPattern
("yyyy-MM-dd HH:MM:ss");
/**
* 每秒钟执行一次
*/
@Scheduled(cron = "0/1 * * * * ?")
public void cron() {
LocalDateTime now = LocalDateTime.now();
System.out.println("spring task 这是定时任务,时间是:" +
pattern.format(now));
}
}
再次启动SpringBoot项目,然后就可以自动启动Spring Task了,定时任务执行结果如图6.12所示。@EnableScheduling注解表示开启SpringTask任务,如果不开启,就没有办法执行定时任务。@Scheduled(cron = "0/1 * ** * ?")注解表示每分钟执行一次,注解中的“0/1 * * * * ?”是cron表达式,cron表达式包括Seconds、Minutes、Hours、Day-of-Month、Month、Day-of-Week和Year(可选字段),它们之间以空隔分隔。读者可根据要实现的业务完成cron表达式的拼接。cron中一些特殊字符的含义如表6.3所示。
@Scheduled注解支持非常多的参数,以帮助开发者快速完成定时任务的开发,这些参数如下:
cron:cron表达式,指定任务在特定的时间执行;
fixedDelay:上一次任务执行完成后隔多长时间再次执行,参数类型为long,单位为ms;
fixedDelayString:与fixedDelay的含义一样,只是参数类型变为String;
fixedRate:按一定的频率执行任务,参数类型为long,单位为ms;fixedRateString:与fixedRate的含义一样,只是参数类型变为String;
initialDelay:第一次任务延迟多久再执行,参数类型为long,单位为ms;
initialDelayString:与initialDelay的含义一样,只是参数类型变为String;
zone:时区,默认为当前时区,一般不用。
基于Spring Task强大的功能和便捷性,在开发Spring项目时,笔者推荐
使用Spring Task完成定时任务的需求。
实战:基于Quartz实现定时调度
Quartz是一个由Java编写的开源任务调度框架,其通过触发器设置作业定时运行规则,控制作业的运行时间。Quartz还可以搭建成集群服务,其中,Quartz集群通过故障切换和负载平衡的功能,能给调度器带来高可用性和伸缩性。我们一般用Quartz来执行定时任务,如定时发送信息、定时生成报表等。在分布式系统中,也可以使用Quartz完成任务调度的需求。
Quartz框架的核心组件包括调度器、触发器和作业。调度器是作业的总指挥,触发器是作业的操作者,作业为应用的功能模块。
Quartz框架中的Job为任务接口,该接口只有一个方法voidexecute(JobExecution- Context context),自定义定时任务的类需要实现Job接口并重写execute()方法,在该方法中完成定时业务逻辑。
JobExecutionContext类提供了调度上下文的各种信息。每次执行Job时均需要重新创建一个Job实例。
下面再介绍几个Quartz常用的几个类:JobDetail类用来描述Job的实现类及其他相关的静态信息;Trigger类是定时任务的定时管理工具,一个Trigger只能对应一个定时任务,而一个定时任务却可对应多个触发器;
Scheduler类是定时任务的管理容器,是Quartz最上层的接口,它管理所有触发器和定时任务,使它们协调工作,每个Scheduler都保存有JobDetail和Trigger的注册信息,一个Scheduler类中可以注册多个JobDetail和多个Trigger。
基于以上介绍,使用Quartz重新实现6.2.2小节的定时任务。
(1)在pom.xml中添加Quartz的依赖,其坐标如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
(2)修改log4j2.xml的日志记录器,添加Quartz包的日志级别为INFO,不要打印DEBUG级别的日志。
<loggers>
<!--Spring和MyBatis的日志级别为INFO-->
<logger name="org.springframework" level="INFO"></logger>
<logger name="org.mybatis" level="INFO"></logger>
<logger name="org.quartz" level="INFO"></logger>
<!-- 自定义包设置为INFO,则可以看见输出的日志不包含DEBUG输出了 -->
<logger name="com.example.springextenddemo" level="INFO"/>
<root level="all">
<appender-ref ref="myAppender"/>
</root>
</loggers>
(3)自定义任务执行类,添加Quartz的任务类:
package com.example.springextenddemo.dingshi;import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class MyQuartzTask extends QuartzJobBean{
private static DateTimeFormatter pattern =
DateTimeFormatter.ofPattern
("yyyy-MM-dd HH:MM:ss");
@Override
public void executeInternal(JobExecutionContext context) throws
JobExecutionException {
LocalDateTime now = LocalDateTime.now();
System.out.println("quartz 这是定时任务,时间是:" +
pattern.format(now));
}
}
(4)添加Quzrtz的配置类,配置定时任务的执行时间和频率。
package com.example.springextenddemo.dingshi;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QuartzConfig {
@Bean
public JobDetail testQuartz1() {
return JobBuilder.newJob(MyQuartzTask.class).withIdentity
("myQuartzTask") .storeDurably().build();
}
@Bean
public Trigger testQuartzTrigger1() {
//1s执行一次
SimpleScheduleBuilder scheduleBuilder =
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(1)
.repeatForever();
return TriggerBuilder.newTrigger().forJob(testQuartz1())
.withIdentity("myQuartzTask")
.withSchedule(scheduleBuilder)
.build();
}
}
(5)启动当前项目会自动加载定时任务,通过控制台就能看到Quartz定时任务的执行情况,控制台打印的日志如图6.13所示。
至此,在项目中使用定时任务的例子便介绍完了,在开发中可以直接使用Timer或者ScheduledExecutorService进行定时任务的测试,在实际的生产环境中,应根据项目情况选择使用Spring Task或者Quartz来实现需求。