SpringBoot集成Quartz(解决@Autowired空指针Null问题即依赖注入的属性为null)
使用spring-boot作为基础框架,其理念为零配置文件,所有的配置都是基于注解和暴露bean的方式。
Quartz的4个核心概念:
1、Job
表示一个工作,要执行的具体内容。此接口中只有一个方法
void execute(JobExecutionContext context)
2、JobDetail
JobDetail表示一个具体的可执行的调度程序,Job是这个可执行程调度程序所要执行的内容,另外JobDetail还包含了这个任务调度的方案和策略。
3、Trigger代表一个调度参数的配置,什么时候去调。
4、Scheduler代表一个调度容器,一个调度容器中可以注册多个JobDetail和Trigger。当Trigger与JobDetail组合,就可以被Scheduler容器调度了。
集成Quartz的步骤如下:
1、POM中引入依赖
<!-- quartz --> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>${quartz.version}</version> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz-jobs</artifactId> <version>${quartz.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${spring-context-support.version}</version> </dependency>
注意版本号
<spring-context-support.version>4.1.6.RELEASE</spring-context-support.version> <quartz.version>2.2.3</quartz.version>
2、修改quartz.properties文件(可以存放在resources下,也可以存放未固定路径)
我们这里将使用基于DB的作业存储
# Default Properties file for use by StdSchedulerFactory # to create a Quartz Scheduler Instance, if a different # properties file is not explicitly specified. # # StdSchedulerFactory使用quartz.properties 创建一个Quartz Scheduler实例 # 参数请参考:http://www.quartz-scheduler.org/documentation/quartz-2.x/configuration/ # # Quartz提供两种基本作业存储类型 # --->第一种类型叫做RAMJobStore: # 最佳的性能,因为内存中数据访问最快 # 不足之处是缺乏数据的持久性,当程序路途停止或系统崩溃时,所有运行的信息都会丢失 # --->第二种类型叫做JDBC作业存储: # 通过调整其quartz.properties属性文件,持久化任务调度信息 # 使用数据库保存任务调度信息后,即使系统崩溃后重新启动,任务的调度信息将得到恢复 # #============================================================================ # 基础配置 #============================================================================ # 设置调度器的实例名(instanceName) 和实例ID (instanceId) org.quartz.scheduler.instanceName: DefaultQuartzScheduler #如果使用集群,instanceId必须唯一,设置成AUTO org.quartz.scheduler.instanceId = AUTO org.quartz.scheduler.rmi.export: false org.quartz.scheduler.rmi.proxy: false org.quartz.scheduler.wrapJobExecutionInUserTransaction: false #============================================================================ # 调度器线程池配置 #============================================================================ org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool # 指定多少个工作者线程被创建用来处理 Job org.quartz.threadPool.threadCount: 10 # 设置工作者线程的优先级(最大值10,最小值1,常用值5) org.quartz.threadPool.threadPriority: 5 # 加载任务代码的ClassLoader是否从外部继承 org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true org.quartz.jobStore.misfireThreshold: 60000 #============================================================================ # Configure JobStore 作业存储配置 #============================================================================ # 默认配置,数据保存到内存(调度程序信息是存储在被分配给JVM的内存里面,运行速度快) #org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore # 持久化配置(存储方式使用JobStoreTX,也就是数据库) org.quartz.jobStore.class:org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass:org.quartz.impl.jdbcjobstore.StdJDBCDelegate #使用自己的配置文件 org.quartz.jobStore.useProperties:true #数据库中quartz表的表名前缀 org.quartz.jobStore.tablePrefix:qrtz_ org.quartz.jobStore.dataSource:qzDS #是否使用集群(如果项目只部署到 一台服务器,就不用了) org.quartz.jobStore.isClustered = true #============================================================================ # Configure Datasources 配置数据源 #============================================================================ org.quartz.dataSource.qzDS.driver:oracle.jdbc.OracleDriver org.quartz.dataSource.qzDS.URL:jdbc:oracle:thin:@10.132.81.134:1521:dsdb1 org.quartz.dataSource.qzDS.user:masmf org.quartz.dataSource.qzDS.password:masmf org.quartz.dataSource.qzDS.maxConnections:10
注意:如果项目中同时运行了基于内存的任务调度(RAMJobStore)和基于数据库的任务调度(JobStoreTX)且属性文件中配置org.quartz.scheduler.instanceName=DefaultQuartzScheduler时,任务永远不会被写入到数据库,因为数据库的schedualer已被内存的schedualer覆盖
原因参考:
QUARTZ任务不写入数据库分析
3、自定义JobFactory
首先解释一个常见的困境:Spring容器可以管理Bean,但是Quartz的job是自己管理的,如果在Job中注入Spring管理的Bean,需要先把Quartz的Job也让Spring管理起来,因此,我们需要重写JobFactory,详细的源码分析,请参考:
Quartz与Spring集成 Job如何自动注入Spring容器托管的对象
Quartz入门实例14-让Quartz的Job使用Spring注入的Bean
import org.quartz.spi.TriggerFiredBundle; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.scheduling.quartz.AdaptableJobFactory; import org.springframework.stereotype.Component; /** * Description: 自定义JobFactory,使用Spring容器管理的Quartz的Bean(Job) * <p/> * AdaptableJobFactory是Spring提供的SchedulerFactoryBean的默认实例化工厂,将由直接实例化Job,没有被Spring管理 * User: lishaohua * Date: 2017/11/15 13:54 */ @Component public class MyJobFactory extends AdaptableJobFactory { /** * AutowireCapableBeanFactory接口是BeanFactory的子类 * 可以连接和填充那些生命周期不被Spring管理的已存在的bean实例 * 具体请参考:http://blog.csdn.net/iycynna_123/article/details/52993542 */ @Autowired private AutowireCapableBeanFactory capableBeanFactory; /** * 创建Job实例 * * @param bundle * @return * @throws Exception */ @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { // 实例化对象 Object jobInstance = super.createJobInstance(bundle); // 进行注入(Spring管理该Bean) capableBeanFactory.autowireBean(jobInstance); //返回对象 return jobInstance; } }
4、配置schedulerFactoryBean
Spring为了能集成Quartz,特意提供了管理Quartz的schedulerFactoryBean,必须配置,具体代码如下:
import org.quartz.ee.servlet.QuartzInitializerListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.FileSystemResource; import org.springframework.scheduling.quartz.SchedulerFactoryBean; import javax.sql.DataSource; import java.io.IOException; import java.util.Properties; /** * Description: Quartz调度配置 * <p/> * User: lishaohua * Date: 2017/11/14 10:27 */ @Configuration //类似xml中的<beans>标签,一般和@bean注解一起使用来配置一个Bean,让Spring来管理它的生命周期 @ConfigurationProperties(prefix = "quartz.config")//把配置文件的信息自动装配到Bean上(以quartz.config前缀的) public class SchedulerConfig { /** * 配置文件路径 */ private String propertiesPath;//quartz.config.propertiesPath @Autowired private MyJobFactory myJobFactory; /** * 配置SchedulerFactoryBean * * @param dataSource 数据源 * @return * @throws IOException */ @Bean //将一个方法产生为Bean并交给Spring容器管理(@Bean只能用在方法上) public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) throws IOException { //Spring提供SchedulerFactoryBean为Scheduler提供配置信息,并被Spring容器管理其生命周期 SchedulerFactoryBean factory = new SchedulerFactoryBean(); //启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 factory.setOverwriteExistingJobs(true); // 延时启动(秒) factory.setStartupDelay(20); //设置quartz的配置文件 factory.setQuartzProperties(quartzProperties()); //设置自定义Job Factory,用于Spring管理Job bean factory.setJobFactory(myJobFactory); return factory; } /** * 加载Quartz配置 * * @return * @throws IOException */ @Bean public Properties quartzProperties() throws IOException { //使用Spring的PropertiesFactoryBean对属性配置文件进行管理 PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean(); //注意:quartz的配置文件从指定系统目录中获取,而不是从classpath中获取 //propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties")); propertiesFactoryBean.setLocation(new FileSystemResource(propertiesPath));
//重要:保证其初始化 propertiesFactoryBean.afterPropertiesSet();
return propertiesFactoryBean.getObject(); } /** * 初始化Quartz监听器,让Spring boot启动时初始化Quartz * --web工程中,一般在web.xml中设置如下: * <listener> * <listener-class>org.quartz.ee.servlet.QuartzInitializerListener</listener-class> * </listener> * Quartz就会随着web容器启动,加载调度任务 * * @return */ @Bean public QuartzInitializerListener executorListener() { return new QuartzInitializerListener(); } //========get/set method============================ public String getPropertiesPath() { return propertiesPath; } public void setPropertiesPath(String propertiesPath) { this.propertiesPath = propertiesPath; } }
注意两点:
a、加载quartz.properties使用的是Spring的PropertiesFactoryBean,我将该文件存放在固定磁盘目录,因此使用了
new FileSystemResource(propertiesPath)
如果你的文件存放在resources下,请从classpath下加载:new ClassPathResource("/quartz.properties")
b、我将quartz.properties路径配置在SpringBoot的yml中,因此使用了@ConfigurationProperties,不需要请移除,如果使用请注意get/set方法不能少
c、注意,一定要先执行propertiesFactoryBean.afterPropertiesSet();然后propertiesFactoryBean.getObject();
5、初始化DB
不解释,直接从quartz官网下载并初始化到你的DB中
6、编写定时任务
直接上代码:
@Component public class UpdateCEBKeyJob extends QuartzJobBean { //日志记录 private static Logger logger = LoggerFactory.getLogger(UpdateCEBKeyJob.class); //ECB密钥更新 @Autowired private KeyOperationService keyOperationService; /** * 执行Job * * @param jobExecutionContext * @throws JobExecutionException */ @Override protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException { boolean updateFlag = false; logger.info("-------------------update ECB key job begin:{} ----------------", DateTimeUtil.getCurrentDateTime()); //===============01.正常执行任务====================== try { /* KeyOperationService keyOperationService = ContextWrapper .getBean("keyOperationService", KeyOperationService.class);*/ updateFlag = keyOperationService.updateCEBKey(); } catch (Exception e) { logger.error("更新光大密钥失败!", e); } //===============02.执行结果判定====================== if (!updateFlag) {//如果更新失败,创建一次性任务再次执行(Job仍然是当前class) createJob(2); } logger.info("-------------------update ECB key job end:{} ----------------", DateTimeUtil.getCurrentDateTime()); }
7、单元测试 或 手工添加任务的Controller
单元测试不解释,如果使用了界面,编写的Controller如下
import com.shengpay.mf.constant.ServerErrorEnum; import com.shengpay.mf.exception.ServerException; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.quartz.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.quartz.SchedulerFactoryBean; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; /** * Description: 定时任务 管理 * <p/> * User: lishaohua * Date: 2017/11/14 13:22 */ @Controller @Api("定时任务管理")//描述类的作用(Swagger注解) public class JobController { private static Logger logger = LoggerFactory.getLogger(JobController.class); /*@Autowired private SchedulerFactoryBean schedulerFactoryBean;*/ @Autowired private Scheduler scheduler; /** * 添加定时任务 * * @param request * @param jobClassName * @param jobGroupName * @param cronExpression * @return */ @ApiOperation("添加定时任务")//描述方法的作用(Swagger注解) @RequestMapping(value = "/addJob", method = {RequestMethod.POST}) @ResponseBody public Map<String, String> addJob(HttpServletRequest request, @RequestParam(value = "jobClassName") String jobClassName, @RequestParam(value = "jobGroupName") String jobGroupName, @RequestParam(value = "cronExpression") String cronExpression) { Map<String, String> returnData = new HashMap<String, String>(); try { /** * 构建JobDetail(表示一个具体的可执行的调度程序,Job是这个可执行程调度程序所要执行的内容) */ JobDetail jobDetail = JobBuilder.newJob(getClass(jobClassName).getClass())//工作项1:Job类 .withIdentity(jobClassName, jobGroupName)//工作项2:job名以及所属组 .build();//构建 /** * 构建触发器Trigger(调度参数的配置,代表何时触发该任务) */ //通过cron表达式构建CronScheduleBuilder CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression); //构建CronTrigger触发器 CronTrigger trigger = TriggerBuilder.newTrigger() .withIdentity(jobClassName, jobGroupName) //工作项1:job名以及所属组 .withSchedule(scheduleBuilder) //工作项2:指定调度参数 .build();//构建 /** *构建调度容器(当Trigger与JobDetail组合,就可以被Scheduler容器调度了) * 一个调度容器中可以注册多个JobDetail和Trigger。 */ //获得调度容器 //Scheduler scheduler = getCurrentScheduler(); //注册调度任务 scheduler.scheduleJob(jobDetail, trigger); //启动任务 scheduler.start(); returnData.put("msg", "添加调度任务成功"); } catch (SchedulerException e) { logger.error("构建调度任务异常", e); returnData.put("msg", "添加调度任务异常:" + e.getMessage()); } catch (ServerException e) { logger.error("内部异常", e); returnData.put("msg", "添加调度任务异常:" + e.getMessage()); } catch (Exception e) { logger.error("添加调度任务异常", e); returnData.put("msg", "添加调度任务异常:" + e.getMessage()); } return returnData; } @ApiOperation("暂停定时任务")//描述方法的作用(Swagger注解) @RequestMapping(value = "/pauseJob", method = {RequestMethod.POST}) @ResponseBody public Map<String, String> pauseJob(HttpServletRequest request, @RequestParam(value = "jobClassName") String jobClassName, @RequestParam(value = "jobGroupName") String jobGroupName) { Map<String, String> returnData = new HashMap<String, String>(); try { //获得调度容器 //Scheduler scheduler = getCurrentScheduler(); //JobKey定义了job的名称和组别 JobKey jobKey = JobKey.jobKey(jobClassName, jobGroupName); //暂停任务 scheduler.pauseJob(jobKey); returnData.put("msg", "暂停调度任务成功"); } catch (SchedulerException e) { logger.error("暂停调度任务异常", e); returnData.put("msg", "暂停调度任务异常:" + e.getMessage()); } catch (Exception e) { logger.error("暂停调度任务异常", e); returnData.put("msg", "暂停调度任务异常:" + e.getMessage()); } return returnData; } @ApiOperation("继续定时任务")//描述方法的作用(Swagger注解) @RequestMapping(value = "/resumeJob", method = {RequestMethod.POST}) @ResponseBody public Map<String, String> resumeJob(HttpServletRequest request, @RequestParam(value = "jobClassName") String jobClassName, @RequestParam(value = "jobGroupName") String jobGroupName) { Map<String, String> returnData = new HashMap<String, String>(); try { //获得调度容器 //Scheduler scheduler = getCurrentScheduler(); //JobKey定义了job的名称和组别 JobKey jobKey = JobKey.jobKey(jobClassName, jobGroupName); //继续任务 scheduler.resumeJob(jobKey); returnData.put("msg", "继续调度任务成功"); } catch (SchedulerException e) { logger.error("继续调度任务异常", e); returnData.put("msg", "继续调度任务异常:" + e.getMessage()); } catch (Exception e) { logger.error("继续调度任务异常", e); returnData.put("msg", "继续调度任务异常:" + e.getMessage()); } return returnData; } /** * 更新定时任务: * --传入的triggerKey有与之匹配的 * --旧触发器的触发时间没有完成 * * @param request * @param jobClassName * @param jobGroupName * @param cronExpression * @return */ @ApiOperation("更新定时任务")//描述方法的作用(Swagger注解) @RequestMapping(value = "/rescheduleJob", method = {RequestMethod.POST}) @ResponseBody public Map<String, String> rescheduleJob(HttpServletRequest request, @RequestParam(value = "jobClassName") String jobClassName, @RequestParam(value = "jobGroupName") String jobGroupName, @RequestParam(value = "cronExpression") String cronExpression) { Map<String, String> returnData = new HashMap<String, String>(); try { //获得调度容器 //Scheduler scheduler = getCurrentScheduler(); //构建旧的TriggerKey TriggerKey triggerKey = TriggerKey.triggerKey(jobClassName, jobGroupName); //通过cron表达式构建CronScheduleBuilder CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression); //从调度容器中获取旧的CronTrigger CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey); //更新CronTrigger trigger = trigger.getTriggerBuilder() .withIdentity(triggerKey) //工作项1:job名以及所属组 .withSchedule(scheduleBuilder) //工作项2:指定调度参数 .build();//构建 //更新调度任务 scheduler.rescheduleJob(triggerKey, trigger); returnData.put("msg", "更新调度任务成功"); } catch (SchedulerException e) { logger.error("更新调度任务异常", e); returnData.put("msg", "更新调度任务异常:" + e.getMessage()); } catch (ServerException e) { logger.error("内部异常", e); returnData.put("msg", "更新调度任务异常:" + e.getMessage()); } catch (Exception e) { logger.error("更新调度任务异常", e); returnData.put("msg", "更新调度任务异常:" + e.getMessage()); } return returnData; } @ApiOperation("删除定时任务")//描述方法的作用(Swagger注解) @RequestMapping(value = "/removeJob", method = {RequestMethod.POST}) @ResponseBody public Map<String, String> removeJob(HttpServletRequest request, @RequestParam(value = "jobClassName") String jobClassName, @RequestParam(value = "jobGroupName") String jobGroupName) { Map<String, String> returnData = new HashMap<String, String>(); try { //获得调度容器 //Scheduler scheduler = getCurrentScheduler(); //TriggerKey定义了trigger的名称和组别 TriggerKey triggerKey = TriggerKey.triggerKey(jobClassName, jobGroupName); //暂停触发器 scheduler.resumeTrigger(triggerKey); //暂停触发器 scheduler.unscheduleJob(triggerKey); //移除任务 scheduler.deleteJob(JobKey.jobKey(jobClassName, jobGroupName)); returnData.put("msg", "删除调度任务成功"); } catch (SchedulerException e) { logger.error("删除调度任务异常", e); returnData.put("msg", "删除调度任务异常:" + e.getMessage()); } catch (Exception e) { logger.error("删除调度任务异常", e); returnData.put("msg", "删除调度任务异常:" + e.getMessage()); } return returnData; } /** * 获得调度容器Scheduler * * @return * @throws SchedulerException */ /*private Scheduler getCurrentScheduler() throws SchedulerException { // 实例化Quartz默认的调度器工厂SchedulerFactory //SchedulerFactory sf = new StdSchedulerFactory(); // 获得调度容器 //Scheduler sched = sf.getScheduler(); //return sched; //Scheduler sched = schedulerFactoryBean.getScheduler(); //return sched; }*/ /** * 获得指定的类实例 * * @param classname * @return * @throws ServerException */ private Job getClass(String classname) throws ServerException { Job baseJob = null; try { //加载参数指定的类 Class<?> classTmp = Class.forName(classname); //实例化 baseJob = (Job) classTmp.newInstance(); } catch (ClassNotFoundException e) { logger.error("找不到指定的类", e); throw new ServerException(ServerErrorEnum.INTERNAL_ERROR); } catch (InstantiationException e) { logger.error("实例化类失败", e); throw new ServerException(ServerErrorEnum.INTERNAL_ERROR); } catch (IllegalAccessException e) { logger.error("实例化类失败", e); throw new ServerException(ServerErrorEnum.INTERNAL_ERROR); } return baseJob; } }
注意:
在controller层直接注入Scheduler即可,如下:
@Autowired private Scheduler scheduler;
然后方法中直接调用
//注册调度任务 scheduler.scheduleJob(jobDetail, trigger); //启动任务 scheduler.start();
或者注入声明的
@Autowired private SchedulerFactoryBean schedulerFactoryBean;
然后方法中先执行schedulerFactoryBean.getScheduler()获得scheduler
//获得调度容器 Scheduler scheduler = schedulerFactoryBean.getScheduler(); //注册调度任务 scheduler.scheduleJob(jobDetail, trigger); //启动任务 scheduler.start();
但是,一定要注意,千万不要自己直接去new StdSchedulerFactory(),默认的schedulerFactoryBean或注入的scheduler都是被StdScheduler,
new StdSchedulerFactory()获得的Scheduler将无法被Spring管理,如下代码:
// 实例化Quartz默认的调度器工厂SchedulerFactory SchedulerFactory sf = new StdSchedulerFactory(); // 获得调度容器 Scheduler sched = sf.getScheduler();
创建的任务区别如下:
===================================华丽的分割线======================================
如果不需要让Spring管理quartz生成的job,则每个job作为普通的Bean对象,可以直接通过applicationContext对象直接getBean
代码如下
import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; /** * Description: applicationContext工具类 * <p/> * User: lishaohua * Date: 2017/11/16 13:48 */ @Component public class ApplicationContextUtil implements ApplicationContextAware { /** * 上下文对象实例 */ private static ApplicationContext appContext; /** * Spring自动注入applicationContext对象 * -- 因此该Bean必须@Component被Spring scan发现 * * @param applicationContext * @throws BeansException */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { appContext = applicationContext; } /** * 获得applicationContext * @return */ public static ApplicationContext getAppContext() { return appContext; } /** * 根据name获取Bean * * @param name * @return */ public static Object getBean(String name) { return getAppContext().getBean(name); } /** * 根据class获取Bean * * @param clazz * @param <T> * @return */ public static <T> T getBean(Class<T> clazz) { return getAppContext().getBean(clazz); } /** * 根据name、class获得Bean * * @param name * @param clazz * @param <T> * @return */ public static <T> T getBean(String name, Class<T> clazz) { return getAppContext().getBean(name, clazz); } }
通过DB查询已添加的任务,查询SQL如下:
select TO_CHAR(t.next_fire_time / (1000 * 60 * 60 * 24) + TO_DATE('1970-01-01 08:00:00', 'YYYY-MM-DD HH24:MI:SS'), 'YYYY-MM-DD HH24:MI:SS') next_fire_datetime, t.* from qrtz_triggers t;
查询效果如图
参考:
如何获取SpringBoot项目的applicationContext对象
参考文章:
https://www.cnblogs.com/javanoob/p/springboot_schedule.html
https://www.cnblogs.com/softidea/p/6073495.html
http://blog.csdn.net/magic_best/article/details/50158125
http://blog.csdn.net/iycynna_123/article/details/52993542
http://www.jianshu.com/p/b460171c57ea