需求
实现消息定时推送策略的增删改查,并根据策略内容发送邮件。
实现过程
其实从一开始打算使用java自带timer来实现定时,但是马上就遇到了问题,不管是用schedule方法还是scheduleAtFixedRate都会出现一旦设置的时间已经过去,这两种方法都会先执行一次。特别是scheduleAtFixedRate方法,它会先计算出设置的已经过去的时间相对与现在的时间一个差值,然后这个差值包含多少个时间间隔,它就会执行多少次。这显然与需求不相符。
如果当前时间为2021-02-19 10:52:00那么下面的代码就会打印5+1次。
public static void main(String[] args) throws ParseException {
Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2021-02-19 10:00:00");
new Timer().scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println("哈哈");
}
},date,1000*60*10);
}
而且需求中不是单一的定时策略,而且存在对定时任务的增删改查,如果继续使用timer可能需要引入线程池来解决,这样就变得复杂了。这时候quartz就登场了。
Quartz
Quartz是一个开源项目,完全由Java开发,可以用来执行定时任务,类似于java.util.Timer。但是相较于Timer, Quartz增加了很多功能,但是相比于timer我觉得最重要的就是对每一个定时任务可以很方便的进行管理。
下面的图是我对于Quartz的一个大概的理解。
在使用时建议创建QuartzUtil类方便管理。
public class Quartz {
@Autowired
private Scheduler scheduler;
public void addJob(String jobName, String jobGroup, String triggerName, String triggerGroup, String strDate, String fre, PushStrategyPO pushStrategyPO) throws InterruptedException{
try {
DateFormat bf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date=null;
try {
date = bf.parse(strDate);
} catch (ParseException e) {
e.printStackTrace();
}
if (null == date || StringUtils.isEmpty(fre)) {
return;
}
String cronExpress="";
if (fre.equals("d")){
cronExpress = getEveryDayCron(date);
}
if (fre.equals("w")){
cronExpress = getEveryWeekCron(date);
}
// Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
//创建任务器:定义任务细节
JobDetail jobDetail=null;
if(pushStrategyPO.getType()==0){
jobDetail = JobBuilder.newJob(CustomerEmailJob.class).withIdentity(jobName, jobGroup).build();
}
if(pushStrategyPO.getType()==1){
jobDetail = JobBuilder.newJob(ContractEmailJob.class).withIdentity(jobName, jobGroup).build();
}
JobDataMap jobDataMap = jobDetail.getJobDataMap();
jobDataMap.put("fre",fre);
jobDataMap.put("pushStrategyPO",pushStrategyPO);
CronTrigger trigger = TriggerBuilder
.newTrigger()
.withIdentity(triggerName, triggerGroup) //创建一个标识符
//每秒钟触发一次任务
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpress))
.build();
//将任务和触发器注册到调度器中
scheduler.scheduleJob(jobDetail, trigger);
if (!scheduler.isShutdown()) {
scheduler.start();
}
} catch (SchedulerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private String formatDateByPattern(Date date,String dateFormat){
SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
String formatTimeStr = null;
if (date != null) {
formatTimeStr = sdf.format(date);
}
return formatTimeStr;
}
/***
* convert Date to cron ,eg. "0 07 10 15 1 ? 2016"
* @param date : 时间点
* @return
*/
public String getEveryDayCron(Date date){
String dateFormat="ss mm HH * * ?";
return formatDateByPattern(date, dateFormat);
}
/**
*默认周一发
* @param date
* @return
*/
public String getEveryWeekCron(Date date){
String dateFormat="ss mm HH ? * 2";
return formatDateByPattern(date, dateFormat);
}
public void modifyJobTime(String jobName, String jobGroupName, String triggerName, String triggerGroupName,
String cron) {
try {
TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName);
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
if (trigger == null) {
return;
}
String oldTime = trigger.getCronExpression();
if (!oldTime.equalsIgnoreCase(cron)) {
// 触发器
TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
// 触发器名,触发器组
triggerBuilder.withIdentity(triggerName, triggerGroupName);
triggerBuilder.startNow();
// 触发器时间设定
triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
// 创建Trigger对象
trigger = (CronTrigger) triggerBuilder.build();
// 方式一 :修改一个任务的触发时间
scheduler.rescheduleJob(triggerKey, trigger);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 功能: 移除一个任务
*
* @param jobName
* @param jobGroupName
* @param triggerName
* @param triggerGroupName
*/
public void removeJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName) {
try {
TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName);
// 停止触发器
scheduler.pauseTrigger(triggerKey);
// 移除触发器
scheduler.unscheduleJob(triggerKey);
// 删除任务
scheduler.deleteJob(JobKey.jobKey(jobName, jobGroupName));
System.out.println("removeJob:"+JobKey.jobKey(jobName));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
*
* 功能:启动所有定时任务
*/
public void startJobs() {
try {
scheduler.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 功能:关闭所有定时任务
*/
public void shutdownJobs() {
try {
if (!scheduler.isShutdown()) {
scheduler.shutdown();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public List<String> getSchedulerJobs() {
List<String> jobGroupNames=null;
try {
jobGroupNames = scheduler.getJobGroupNames();
} catch (SchedulerException e) {
e.printStackTrace();
}
return jobGroupNames;
}
public String createJobID(int n )
{
String val = "";
Random random = new Random();
for ( int i = 0; i < n; i++ )
{
String str = random.nextInt( 2 ) % 2 == 0 ? "num" : "char";
if ( "char".equalsIgnoreCase( str ) )
{ // 产生字母
int nextInt = random.nextInt( 2 ) % 2 == 0 ? 65 : 97;
// System.out.println(nextInt + "!!!!"); 1,0,1,1,1,0,0
val += (char) ( nextInt + random.nextInt( 26 ) );
}
else if ( "num".equalsIgnoreCase( str ) )
{ // 产生数字
val += String.valueOf( random.nextInt( 10 ) );
}
}
return val;
}
}
整合到SpringBoot时的问题
springboot服务中使用Quartz的时候需要做一定的修改,要不然会存在即使加了@bean或者@component,job实现类注入不到spring容器,报空指。
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
Job实现类重新集成spring的QuartzJobBean
public class sendEmailJob extends QuartzJobBean {
private static final Logger logger = LoggerFactory.getLogger(sendEmailJob.class);
@Autowired
private ProjectServiceImpl projectService;
@Autowired
private SendEmailUtil sendEmailUtil;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
...
}
需要往Job实现类中传参时
因为job实现类重写了方法,不能在参数中再去添加传入的参数。这时候就需要通过任务管理器去传参,调度器在定时执行的时候再去将参数通过JobExecutionContext传入。
JobDataMap jobDataMap = jobDetail.getJobDataMap();
jobDataMap.put("fre",fre);
jobDataMap.put("pushStrategyPO",pushStrategyPO);
JobDataMap jobDataMap =jobExecutionContext.getJobDetail().getJobDataMap();
String fre = jobDataMap.getString("fre");
PushStrategyPO pushStrategyPO = (PushStrategyPO) jobDataMap.get("pushStrategyPO");
当服务重启时需要重新载入schedule中
当服务在重启时,需要去读数据库,然后将每一条定时任务重新载入任务管理当中。
@Component
public class ApplicationRunnerImpl implements ApplicationRunner {
private static final Logger logger = LoggerFactory.getLogger(ModuleController.class);
@Autowired
private MessageService messageService;
@Autowired
private Quartz quartz;
@Override
public void run(ApplicationArguments args) throws Exception {
//查询策略表,包括项目管理和合同管理
logger.info("Querying all pushStrategies for mysql...");
ArrayList<PushStrategyPO> pushStrategyPOS = messageService.queryAllJob();
logger.info("pushStrategies is"+pushStrategyPOS.toString());
logger.info("Begin to start all timed task...");
for (PushStrategyPO ps : pushStrategyPOS) {
//启动所有定时任务
String jobName=ps.getJobId();
String jobGroup=ps.getJobId()+"-Group";
String triggerName=ps.getJobId()+"-Trigger";
String triggerGroup=ps.getJobId()+"-TriggerGroup";
String frequency = ps.getFrequency();
String setDate = ps.getSetDate();
quartz.addJob(jobName,jobGroup,triggerName,triggerGroup,setDate,frequency,ps);
}
List<String> schedulerJobs = quartz.getSchedulerJobs();
logger.info("there are "+schedulerJobs.size()+"job in schedule");
for (String schedulerJob : schedulerJobs) {
logger.info("job is :"+schedulerJob);
}
}
}