任务需求:
银行日终定时跑批,开始思路是使用最简单的方式Timer任务调度线程去跑,但是有个问题,Timer调度的话,不能够精准的时间,因为牵扯到集群的问题,所以最后考虑到了quartz集群
Quartz版本:
2.3.1
Quartz 核心元素:
Quartz任务调度的核心元素为:Scheduler——任务调度器、Trigger——触发器、Job——任务。其中trigger和job是任务调度的元数据,scheduler是实际执行调度的控制器。
Trigger是用于定义调度时间的元素,即按照什么时间规则去执行任务。Quartz中主要提供了四种类型的trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和NthIncludedDayTrigger。这四种trigger可以满足企业应用中的绝大部分需求。
Job用于表示被调度的任务。主要有两种类型的job:无状态的(stateless)和有状态的(stateful)。对于同一个trigger来说,有状态的job不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job主要有两种属性:volatility和durability,其中volatility表示任务是否被持久化到数据库存储,而durability表示在没有trigger关联的时候任务是否被保留。两者都是在值为true的时候任务被持久化或保留。一个job可以被多个trigger关联,但是一个trigger只能关联一个job。
Scheduler由scheduler工厂创建:
DirectSchedulerFactory或者StdSchedulerFactory。看很多资料都是基于这两个去做的,本文是直接从工厂接口SchedulerFactory去做。
Scheduler主要有三种:
RemoteMBeanScheduler,RemoteScheduler和StdScheduler。也是基于接口去直接做不适用他们的3个实现。
正文:
结构:
工厂:QuartzJobFactory
package cn.lsr.quartz.factory; 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: quartz工厂 * @Package: lsr-microservice * @author: Hacker_lsr@126.com **/ @Component public class QuartzJobFactory extends AdaptableJobFactory { /** * 这个对象Spring会帮我们自动注入进来,也属于Spring技术范畴. */ @Autowired private AutowireCapableBeanFactory capableBeanFactory; @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { //调用父类的方法 Object jobInstance = super.createJobInstance(bundle); //进行注入,这属于Spring的技术,不清楚的可以查看Spring的API. capableBeanFactory.autowireBean(jobInstance); return jobInstance; } }
定时任务管理器:QuartzConfigManager
package cn.lsr.quartz.manager; import cn.lsr.quartz.factory.QuartzJobFactory; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.core.io.ClassPathResource; import org.springframework.scheduling.quartz.SchedulerFactoryBean; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.io.IOException; /** * @Description: 分布式任务定时管理配置 * @Package: lsr-microservice * @author: Hacker_lsr@126.com **/ @Component public class QuartzConfigManager { /** * 默认数据源--若不使用,使用的是配置文件的数据源 */ @Autowired DataSource dataSource; // 配置文件路径 static final String QUARTZ_CONFIG = "/quartz.properties"; /** * 工厂 * @param myJobFactory * @return * @throws Exception */ @Bean public SchedulerFactoryBean schedulerFactoryBean(QuartzJobFactory myJobFactory) throws Exception { SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); //如果使用自定义的数据源就可以配置,如果是使用quartz扩展的不需要设置。 //schedulerFactoryBean.setDataSource(dataSource); //使job实例支持spring 容器管理 //用于Quartz集群,启动时更新已存在的Job schedulerFactoryBean.setOverwriteExistingJobs(true); schedulerFactoryBean.setJobFactory(myJobFactory); schedulerFactoryBean.setConfigLocation(new ClassPathResource(QUARTZ_CONFIG)); //可以手动加载quartz配置 //schedulerFactoryBean.setQuartzProperties(PropertiesUtils.readProperties(QUARTZ_CONFIG)); // 集群需要通过QuartzJobBean注入,需要设置上下文 schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContext"); // 延迟10s启动quartz schedulerFactoryBean.setStartupDelay(10); return schedulerFactoryBean; } /** * 调度器 * @param schedulerFactoryBean * @return * @throws IOException * @throws SchedulerException */ @Bean public Scheduler scheduler(SchedulerFactoryBean schedulerFactoryBean) throws IOException, SchedulerException { Scheduler scheduler = schedulerFactoryBean.getScheduler(); scheduler.start(); return scheduler; } }
quartz数据源扩展为德鲁伊的:DruidConnectionProvider
package cn.lsr.quartz.config; import com.alibaba.druid.pool.DruidDataSource; import org.quartz.utils.ConnectionProvider; import java.sql.Connection; import java.sql.SQLException; /** * @Description: quartz数据源扩展 * @Package: lsr-microservice * @author: Hacker_lsr@126.com **/ public class DruidConnectionProvider implements ConnectionProvider { //JDBC驱动 public String driver; //JDBC连接串 public String URL; //数据库用户名 public String user; //数据库用户密码 public String password; //数据库最大连接数 public int maxConnections; // 数据库SQL查询每次连接返回执行到连接池,以确保它仍然是有效的。 public String validationQuery; private boolean validateOnCheckout; private int idleConnectionValidationSeconds; public String maxCachedStatementsPerConnection; public static final int DEFAULT_DB_MAX_CONNECTIONS = 10; public static final int DEFAULT_DB_MAX_CACHED_STATEMENTS_PER_CONNECTION = 120; // Druid连接池 private DruidDataSource datasource; @Override public Connection getConnection() throws SQLException { return datasource.getConnection(); } @Override public void shutdown() throws SQLException { datasource.close(); } @Override public void initialize() throws SQLException { if (URL == null) { throw new SQLException("DBPool could not be created: DB URL cannot be null"); } if (driver == null) { throw new SQLException("DBPool driver could not be created: DB driver class name cannot be null!"); } if (this.maxConnections < 0) { throw new SQLException( "DBPool maxConnectins could not be created: Max connections must be greater than zero!"); } datasource = new DruidDataSource(); try { datasource.setDriverClassName(driver); } catch (Exception e) { e.printStackTrace(); } datasource.setUrl(this.URL); datasource.setUsername(this.user); datasource.setPassword(this.password); datasource.setMaxActive(this.maxConnections); datasource.setMinIdle(1); datasource.setMaxWait(0); datasource.setMaxPoolPreparedStatementPerConnectionSize(this.maxConnections); if (this.validationQuery != null) { datasource.setValidationQuery(this.validationQuery); if (!this.validateOnCheckout){ datasource.setTestOnReturn(true); }else{ datasource.setTestOnBorrow(true); } datasource.setValidationQueryTimeout(this.idleConnectionValidationSeconds); } } /* * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * * 提供get set方法 * * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ public String getDriver() { return driver; } public void setDriver(String driver) { this.driver = driver; } public String getURL() { return URL; } public void setURL(String URL) { this.URL = URL; } public String getUser() { return user; } public void setUser(String user) { this.user = user; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getMaxConnections() { return maxConnections; } public void setMaxConnections(int maxConnections) { this.maxConnections = maxConnections; } public String getMaxCachedStatementsPerConnection() { return maxCachedStatementsPerConnection; } public void setMaxCachedStatementsPerConnection(String maxCachedStatementsPerConnection) { this.maxCachedStatementsPerConnection = maxCachedStatementsPerConnection; } public String getValidationQuery() { return validationQuery; } public void setValidationQuery(String validationQuery) { this.validationQuery = validationQuery; } public boolean isValidateOnCheckout() { return validateOnCheckout; } public void setValidateOnCheckout(boolean validateOnCheckout) { this.validateOnCheckout = validateOnCheckout; } public int getIdleConnectionValidationSeconds() { return idleConnectionValidationSeconds; } public void setIdleConnectionValidationSeconds(int idleConnectionValidationSeconds) { this.idleConnectionValidationSeconds = idleConnectionValidationSeconds; } public DruidDataSource getDatasource() { return datasource; } public void setDatasource(DruidDataSource datasource) { this.datasource = datasource; } }
job任务接口:JobTaskService
package cn.lsr.quartz.service; import cn.lsr.quartz.entity.JobInfo; import org.quartz.SchedulerException; import java.util.List; /** * @Description: job任务接口 * @Package: lsr-microservice * @author: Hacker_lsr@126.com **/ public interface JobTaskService { /** * 查询所有的job * @return */ List<JobInfo> list(); /** * 新增job * @param info */ void addJob(JobInfo info); /** * 修改job * @param info */ void edit(JobInfo info); /** * 删除job * @param jobName * @param jobGroup */ void delete(String jobName, String jobGroup); /** * 暂停job * @param jobName * @param jobGroup */ void pause(String jobName, String jobGroup); /** * 重启job * @param jobName * @param jobGroup */ void resume(String jobName, String jobGroup); /** * 检查job * @param jobName * @param jobGroup * @return * @throws SchedulerException */ boolean checkExists(String jobName, String jobGroup)throws SchedulerException; }
job接口实现:JobTaskServiceImp
package cn.lsr.quartz.service.imp; import cn.lsr.quartz.entity.JobInfo; import cn.lsr.quartz.service.JobTaskService; import org.apache.commons.lang3.time.DateFormatUtils; import org.quartz.*; import org.quartz.impl.matchers.GroupMatcher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; /** * @Description: job任务实现 * @Package: lsr-microservice * @author: Hacker_lsr@126.com **/ @Service public class JobTaskServiceImp implements JobTaskService { private Logger logger = LoggerFactory.getLogger(JobTaskServiceImp.class); @Autowired(required = false) private Scheduler scheduler; /** * 所有任务列表 */ @Override public List<JobInfo> list() { List<JobInfo> list = new ArrayList<>(); try { for (String groupJob : scheduler.getJobGroupNames()) { for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.<JobKey>groupEquals(groupJob))) { List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey); for (Trigger trigger : triggers) { Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey()); JobDetail jobDetail = scheduler.getJobDetail(jobKey); String cronExpression = "", createTime = ""; if (trigger instanceof CronTrigger) { CronTrigger cronTrigger = (CronTrigger) trigger; cronExpression = cronTrigger.getCronExpression(); createTime = cronTrigger.getDescription(); } JobInfo info = new JobInfo(); info.setJobName(jobKey.getName()); info.setJobGroup(jobKey.getGroup()); info.setJobDescription(jobDetail.getDescription()); //获取参数 TODO jobDetail.getJobDataMap().toString(); info.setJobStatus(triggerState.name()); info.setCronExpression(cronExpression); info.setCreateTime(createTime); list.add(info); } } } } catch (SchedulerException e) { e.printStackTrace(); } return list; } /** * 保存定时任务 * * @param info */ @SuppressWarnings("unchecked") @Override public void addJob(JobInfo info) { String jobName = info.getJobName(), jobGroup = info.getJobGroup(), cronExpression = info.getCronExpression(), jobDescription = info.getJobDescription(), createTime = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"); try { if (checkExists(jobName, jobGroup)) { logger.info("add job fail, job already exist, jobGroup:{}, jobName:{}", jobGroup, jobName); } TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup); JobKey jobKey = JobKey.jobKey(jobName, jobGroup); CronScheduleBuilder schedBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing(); CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withDescription(createTime).withSchedule(schedBuilder).build(); Class<? extends Job> clazz = (Class<? extends Job>) Class.forName(jobName); JobDetail jobDetail = JobBuilder.newJob(clazz).withIdentity(jobKey).withDescription(jobDescription).build(); //可以传参 TODO jobDetail.getJobDataMap().put("data", jobDescription); scheduler.scheduleJob(jobDetail, trigger); } catch (SchedulerException | ClassNotFoundException e) { logger.error("类名不存在或执行表达式错误,exception:{}", e.getMessage()); } } /** * 修改定时任务 * * @param info */ @Override public void edit(JobInfo info) { String jobName = info.getJobName(), jobGroup = info.getJobGroup(), cronExpression = info.getCronExpression(), jobDescription = info.getJobDescription(), createTime = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"); try { if (!checkExists(jobName, jobGroup)) { logger.info("edit job fail, job is not exist, jobGroup:{}, jobName:{}", jobGroup, jobName); } TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup); JobKey jobKey = new JobKey(jobName, jobGroup); CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing(); CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withDescription(createTime).withSchedule(cronScheduleBuilder).build(); JobDetail jobDetail = scheduler.getJobDetail(jobKey); //可以传参 TODO jobDetail.getJobDataMap().put("data", jobDescription); jobDetail.getJobBuilder().withDescription(jobDescription); HashSet<Trigger> triggerSet = new HashSet<>(); triggerSet.add(cronTrigger); scheduler.scheduleJob(jobDetail, triggerSet, true); } catch (SchedulerException e) { logger.error("类名不存在或执行表达式错误,exception:{}", e.getMessage()); } } /** * 删除定时任务 * * @param jobName * @param jobGroup */ @Override public void delete(String jobName, String jobGroup) { TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup); try { if (checkExists(jobName, jobGroup)) { scheduler.pauseTrigger(triggerKey); scheduler.unscheduleJob(triggerKey); logger.info("delete job, triggerKey:{},jobGroup:{}, jobName:{}", triggerKey, jobGroup, jobName); } } catch (SchedulerException e) { logger.error(e.getMessage()); } } /** * 暂停定时任务 * * @param jobName * @param jobGroup */ @Override public void pause(String jobName, String jobGroup) { TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup); try { if (checkExists(jobName, jobGroup)) { scheduler.pauseTrigger(triggerKey); logger.info("pause job success, triggerKey:{},jobGroup:{}, jobName:{}", triggerKey, jobGroup, jobName); } } catch (SchedulerException e) { logger.error(e.getMessage()); } } /** * 重新开始任务 * * @param jobName * @param jobGroup */ @Override public void resume(String jobName, String jobGroup) { TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup); try { if (checkExists(jobName, jobGroup)) { scheduler.resumeTrigger(triggerKey); logger.info("resume job success,triggerKey:{},jobGroup:{}, jobName:{}", triggerKey, jobGroup, jobName); } } catch (SchedulerException e) { logger.error(e.getMessage()); } } /** * 验证是否存在 * * @param jobName * @param jobGroup * @throws SchedulerException */ @Override public boolean checkExists(String jobName, String jobGroup) throws SchedulerException { TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup); return scheduler.checkExists(triggerKey); } }
job入口:JobExecute
package cn.lsr.user.job; import org.quartz.DisallowConcurrentExecution; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.Map; /** * @Description: job执行入口 * @Package: lsr-microservice * @author: Hacker_lsr@126.com **/ @Component @DisallowConcurrentExecution public class JobExecute implements Job { private Logger logger = LoggerFactory.getLogger(JobExecute.class); @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { //获取参数 String s = (String) jobExecutionContext.getJobDetail().getJobDataMap().get("data"); logger.info("开始执行任务===================,参数:{}",s); } }
job实体:JobInfo
package cn.lsr.quartz.entity; import java.io.Serializable; /** * @Description: job配置类 * @Package: lsr-microservice * @author: Hacker_lsr@126.com **/ public class JobInfo implements Serializable { /**任务名称*/ private String jobName; /**任务分组*/ private String jobGroup; /**任务描述*/ private String jobDescription; /**任务状态*/ private String jobStatus; /**任务表达式*/ private String cronExpression; /**创建时间*/ private String createTime; public String getJobName() { return jobName; } public void setJobName(String jobName) { this.jobName = jobName; } public String getJobGroup() { return jobGroup; } public void setJobGroup(String jobGroup) { this.jobGroup = jobGroup; } public String getJobDescription() { return jobDescription; } public void setJobDescription(String jobDescription) { this.jobDescription = jobDescription; } public String getJobStatus() { return jobStatus; } public void setJobStatus(String jobStatus) { this.jobStatus = jobStatus; } public String getCronExpression() { return cronExpression; } public void setCronExpression(String cronExpression) { this.cronExpression = cronExpression; } public String getCreateTime() { return createTime; } public void setCreateTime(String createTime) { this.createTime = createTime; } }
sql:
CREATE TABLE `quartz_jobconfig` ( `jobName` varchar(190) NOT NULL COMMENT '任务类路径', `jobGroup` varchar(190) DEFAULT NULL COMMENT '分组', `jobDescription` varchar(500) COMMENT '任务描述', `jobStatus` varchar(32) DEFAULT NULL COMMENT 'job状态 (查询显示会用到的参数)', `jobcron` varchar(32) DEFAULT NULL COMMENT 'cron表达式', `createTime` varchar(32) NOT NULL DEFAULT '' COMMENT '创建时间', PRIMARY KEY (`jobName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
配置文件:quartz.properties
#quartz集群配置 # =========================================================================== # Configure Main Scheduler Properties 调度器属性 # =========================================================================== #调度标识名 集群中每一个实例都必须使用相同的名称 org.quartz.scheduler.instanceName=DefaultQuartzScheduler #ID设置为自动获取 每一个必须不同 org.quartz.scheduler.instanceid=AUTO #禁用quartz软件更新 org.quartz.scheduler.skipUpdateCheck=true #============================================================================ # Configure ThreadPool #============================================================================ #线程池的实现类(一般使用SimpleThreadPool即可满足几乎所有用户的需求) org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool #指定线程数,至少为1(无默认值)(一般设置为1-100直接的整数合适) org.quartz.threadPool.threadCount = 25 #设置线程的优先级(最大为java.lang.Thread.MAX_PRIORITY 10,最小为Thread.MIN_PRIORITY 1,默认为5) org.quartz.threadPool.threadPriority = 5 #============================================================================ # Configure JobStore #============================================================================ # 信息保存时间 默认值60秒 org.quartz.jobStore.misfireThreshold = 60000 #数据保存方式为数据库持久化 org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX #数据库代理类,一般org.quartz.impl.jdbcjobstore.StdJDBCDelegate可以满足大部分数据库 org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate #JobDataMaps是否都为String类型 org.quartz.jobStore.useProperties = false #数据库别名 随便取 org.quartz.jobStore.dataSource = myDS #表的前缀,默认QRTZ_ org.quartz.jobStore.tablePrefix = QRTZ_ #是否加入集群 org.quartz.jobStore.isClustered = true #调度实例失效的检查时间间隔 org.quartz.jobStore.clusterCheckinInterval = 20000 #============================================================================ # Configure Datasources #============================================================================ #自定义连接池 org.quartz.dataSource.myDS.connectionProvider.class=cn.lsr.quartz.config.DruidConnectionProvider #数据库引擎 org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver #数据库连接 org.quartz.dataSource.myDS.URL = jdbc:mysql://127.0.0.1:3306/lsr-microservice?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true #数据库用户 org.quartz.dataSource.myDS.user = root #数据库密码 org.quartz.dataSource.myDS.password = lishirui #允许最大连接 org.quartz.dataSource.myDS.maxConnections = 5 #验证查询sql,可以不设置 org.quartz.dataSource.myDS.validationQuery=select 0 from dual #注:如果嫌需要额外配置quart数据源很烦,也可以共用你项目配置的数据库链接,这样每次更换数据库连接,就不需要额外在修改。
测试:
/** * 测试 */ @GetMapping("/job/test") @ResponseBody public void test(){ JobInfo jobInfo = new JobInfo(); jobInfo.setJobName("cn.lsr.user.job.JobExecute"); //配置job入口的准确路径去执行exe..方法 jobInfo.setJobGroup("test"); jobInfo.setJobDescription("lishirui"); jobInfo.setCronExpression("0 */1 * * * ? "); jobTaskService.addJob(jobInfo); }
结果: