本章介绍Spring Boot中定时器的使用方法,总结个人对一些定时器用法的了解,如有错误欢迎指正。

定时器是什么?

定时器就是让程序定时执行某些任务,不需要人工手动执行。

为什么要使用定时器?

使用定时器,有很多好处。举个例子:在平台api对接中,请求通常需要携带token信息,而token有可能有固定时效。在token失效后,需要重新获取token信息。此时可以使用定时任务重新获取token。

怎么用定时器?

在Java中,有很多中方式处理定时任务。Timer,SpringBoot的@Scheduled注解,SpringBoot整合Quartz等方式。

整合Quartz

Spring Boot中使用Quartz是通过继承QuartzJobBean的方式,创建JobDetail和Trigger以达到定时效果。

1、创建SimpleScheduleExtendsQuartzJobTask任务类



package com.study.controller.schedule;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

import java.util.Date;

public class SimpleScheduleExtendsQuartzJobTask extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("do schedule extends quartz job task " + new Date());
    }
}



2、创建JobDetail和Trigger。创建JobDetail时需要使用JobBuilder.newJob()方法添加任务类,withIdentity()方法指定key,build()方法进行构建。TriggerBuilder.forJob()方法添加JobDetail,withSchedule()方法添加schedule。schedule可以通过SimpleScheduleBuilder进行构建,withIntervalInSeconds()指明每次执行定时任务的间隔时间。



package com.study.config;

import com.study.controller.schedule.SimpleScheduleExtendsQuartzJobTask;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SimpleScheduleConfig {
    @Bean
    public JobDetail simpleJobBeanDetails() {
        return JobBuilder.newJob(SimpleScheduleExtendsQuartzJobTask.class).withIdentity("simpleJobBean").storeDurably().build();
    }

    @Bean
    public Trigger simpleTrigger() {
        SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(15).repeatForever();
        return TriggerBuilder.newTrigger().withIdentity("simpleTrigger").forJob(simpleJobBeanDetails()).withSchedule(scheduleBuilder).build();
    }
}



上述方法已经可以到达效果,但仍然有需要改进的地方。我们来思考几个问题:

1、有大量的定时任务时如何解决?

2、当某个任务的执行时间需要修改时如何解决?

3、如何控制任务的启动和停止?

我们是否可以将所有的任务的类名,执行方法, 参数,以及执行时间保存在数据库中,通过对数据库的修改和访问,实现任务调度。

我们约定所有的定时脚本,都通过http://localhost:8080/authenticate/runScript?type=DoSimpleJobTask&args=aa方式进行访问,其中type指定类名,设置统一的访问方法为runScript()方法,通过反射的方式处理不同的任务。创建AuthenticateController 



package com.study.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

@RestController
public class AuthenticateController {
    @RequestMapping("authenticate/runScript")
    public String runScript(HttpServletRequest request, HttpServletResponse response) {
        String type = request.getParameter("type");
        if (null == type) {
            System.out.println("type is null");
            return "ok";
        }
        String argList = request.getParameter("args");
        type = type.replace("/", ".");
        String className = "script." + type;
        String methodName = "runScript";
        try {
            Class<?> customClass = Class.forName(className);
            Method method = customClass.getMethod(methodName, String.class);
            Object result = method.invoke(customClass.newInstance(), argList);
            if (null != result) {
                if ((Boolean) result) {

                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "ok";
    }
}



因为配置信息保存在数据库中,所以添加maven依赖访问数据库



<dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.1</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>6.0.6</version>
        </dependency>



application.properties中设置mysql的连接配置。此处做下说明,设置时区serverTimezone=UTC,useSSL=false,因为mysql驱动为6.0版本,driver也改成了com.mysql.cj.jdbc.Driver



#mysql
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver



任务配置类



package com.study.orm;

public class ScheduleTaskConfig {
    private Integer id;
    private String name;
    private String task;
    private String cron;

    public ScheduleTaskConfig() {
    }

    public ScheduleTaskConfig(Integer id, String name, String task, String cron) {
        this.id = id;
        this.name = name;
        this.task = task;
        this.cron = cron;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getTask() {
        return task;
    }

    public void setTask(String task) {
        this.task = task;
    }

    public String getCron() {
        return cron;
    }

    public void setCron(String cron) {
        this.cron = cron;
    }
}



创建Mapper



package com.study.mapper;

import com.study.orm.ScheduleTaskConfig;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ScheduleTaskConfigMapper {
    @Select("select * from tb_task")
    List<ScheduleTaskConfig> fetchList();
}



service接口及实现



package com.study.service;

import com.study.orm.ScheduleTaskConfig;

import java.util.List;

public interface ScheduleTaskConfigService {
    List<ScheduleTaskConfig> fetchList();
}



package com.study.service.impl;

import com.study.mapper.ScheduleTaskConfigMapper;
import com.study.orm.ScheduleTaskConfig;
import com.study.service.ScheduleTaskConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ScheduleTaskConfigServiceImpl implements ScheduleTaskConfigService {
    @Autowired
    private ScheduleTaskConfigMapper scheduleTaskConfigMapper;

    @Override
    public List<ScheduleTaskConfig> fetchList() {
        return scheduleTaskConfigMapper.fetchList();
    }
}



扫描mapper



package com;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@MapperScan("com.study.mapper")
@SpringBootApplication
@EnableScheduling
public class SpringbootApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootApplication.class, args);
    }

}



考虑到需要控制任务的启动和停止,采用ThreadPoolTaskScheduler进行任务调度。ThreadPoolTaskScheduler的schedule方法可以创建ScheduledFuture,ScheduledFuture中可以通过cancel(true),取消任务。



package com.study.controller.schedule;

import com.mchange.v1.util.MapUtils;
import com.study.orm.ScheduleTaskConfig;
import com.study.service.ScheduleTaskConfigService;
import com.study.utils.ServiceResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;

@RequestMapping("schedule")
@RestController
public class ScheduleTaskManageController {

    @Autowired
    private ThreadPoolTaskScheduler threadPoolTaskScheduler;

    @Bean
    public ThreadPoolTaskScheduler getThreadPoolTaskScheduler() {
        return new ThreadPoolTaskScheduler();
    }

    private Map<Integer, ScheduledFuture> scheduledFutureMap = new HashMap<>();
    @Autowired
    private ScheduleTaskConfigService scheduleTaskConfigService;

    public void _init(){
        System.out.println("++++++++++++init++++++++++++++++");
        List<ScheduleTaskConfig> scheduleTaskConfigs = scheduleTaskConfigService.fetchList();
        if (!CollectionUtils.isEmpty(scheduleTaskConfigs)) {
            for (ScheduleTaskConfig scheduleTaskConfig : scheduleTaskConfigs) {
                ScheduledFuture future = threadPoolTaskScheduler.schedule(new Runnable() {
                    @Override
                    public void run() {
                        try {
                  //task的内容设置为 curl "http://localhost:8080/authenticate/runScript?type=DoSimpleJobTask&args=aa"
                 //请求通过authenticate/runScript映射,通过反射调用script中的任务
                            System.out.println(scheduleTaskConfig.getTask());
                            Process process = Runtime.getRuntime().exec(scheduleTaskConfig.getTask());
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }, new Trigger() {
                    @Override
                    public Date nextExecutionTime(TriggerContext triggerContext) {
                        return new CronTrigger(scheduleTaskConfig.getCron()).nextExecutionTime(triggerContext);
                    }
                });
                scheduledFutureMap.put(scheduleTaskConfig.getId(), future);
            }
        }
    }
    public void _destory(){
        System.out.println("++++++++++++++++destory++++++++++++");
        if (scheduledFutureMap != null && !scheduledFutureMap.isEmpty()) {
            for (Integer key : scheduledFutureMap.keySet()) {
                System.out.println("scheduledFutureMap :" + key);
                scheduledFutureMap.get(key).cancel(true);
            }
        }
        scheduledFutureMap.clear();
    }
    @RequestMapping("start")
    public String startScheduleTask() {
        _init();
        return "success";
    }

    @RequestMapping("stop")
    public String stopScheduleTask() {
        _destory();
        return "success";
    }
}



创建tb_task存放需要执行的任务



CREATE TABLE `tb_task` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(1000) DEFAULT NULL,
  `task` varchar(1000) DEFAULT NULL,
  `cron` varchar(1000) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8



select * from tb_task   

1 测试 curl "http://localhost:8080/authenticate/runScript?type=DoSimpleJobTask&args=aa" 0/35 * * * * ?

2 测试访问百度 curl "www.baidu.com" 0/35 * * * * ?

最后再script下添加不同的任务类



package script;

public class DoSimpleJobTask {
    public Boolean runScript(String args) {
        System.out.println("this is test schedule task : " + args);
        return false;
    }
}



附cron表达式说明:

0/35 * * * * ?

秒 分 时 天 年 ?

大概的思路就是这样,可以根据自己需要添加其它。如

  • 添加event,在项目启动之后,自动执行_init方法进行初始化。
  • 当重新添加或修改了task表中的数据时,可以先调用stop方法,取消所有的任务,然后再调用start方法添加。
  • 或者在页面编辑数据表中数据时,对应新增的方法,可以添加到schedule中,对于修改的方法可以根据id值获取到scheduledFutureMap的值,先取消,然后重新添加一个等等。
package com.study.event;

import com.study.controller.schedule.ScheduleTaskManageController;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class ApplicationStartup implements ApplicationListener<ApplicationStartedEvent> {
    @Override
    public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {
        System.out.println("++++++++++++++application started event+++++++++++++++");
        ScheduleTaskManageController scheduleTaskManageController = applicationStartedEvent.getApplicationContext().getBean(ScheduleTaskManageController.class);
        scheduleTaskManageController._destory();
        scheduleTaskManageController._init();
    }
}



如果有什么错误,或者有更好的处理方式,也可以留言告诉我