Springboot+quartz整合(多数据源)
谈谈
最近在公司需要做一个定时任务管理的小项目,心想采用Springboot和quartz能减少不少配置,迅速完成项目搭建和开发。以前做过SpringMVC中配置定时任务,都是在xml配置文件中配置一大堆bean来实现任务的配置与存储,所以这次也同样在pom文件中导入
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
但是经过一番折腾,由于知识的浅薄,通过配置文件配置quartz持久化到数据库失败了。
无意间看到一个叫 Spring-boot-starter-quartz 的包,立马去百度查,这才知道,原来Springboot早已经集成了quartz。于是从新导入新的quartz依赖,进行一番配置后,终于大功告成。
下面简单介绍一下我的配置方式。
还是老样子,如果发现有错误或则有待提升的地方,欢迎各位大佬指出来!
导入quartz依赖
既然用springboot做项目,那咱们还是老老实实导入spring家族的依赖吧。一切为了简化配置。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
初始化quartz持久化所需要的数据库表
官方提供了初始化数据库所需要的脚本,下载链接http://www.quartz-scheduler.org/downloads/files/quartz-2.2.3-distribution.tar.gz。不同数据库使用不同的脚本不一样,下载后通过解压路径/quartz-2.2.3/docs/dbTables/可以看到有很多类型的脚本,找到适合自己数据库的就行。我用的mysql,所以打开tables_mysql.sql,复制其中的内容到数据库工具执行一遍就ok。
运行完成后,可以看到数据库有以下表,通过表明容易理解其用途。
其中
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
这个配置需要根据不同数据库类型,设置到yml配置文件中
配置yml文件
这里有一部分涉及到多数据源配置,因为公司项目涉及到多种数据库和多个表的操作,所以我将quartz持久化所需要的表放到了新的数据库task下边。
spring:
datasource:
#数据源1
db1:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.137.53:3306/db1?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: mysqlpassword
type: com.alibaba.druid.pool.DruidDataSource
#数据源2
db2:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.137.53:3306/db2?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: mysqlpassword
type: com.alibaba.druid.pool.DruidDataSource
#quartz数据源
task:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.137.53:3306/task?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: mysqlpassword
type: com.alibaba.druid.pool.DruidDataSource
quartz:
properties:
org:
quartz:
scheduler:
instanceName: DemoScheduler
instanceId: AUTO
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate #跟数据库初始化脚本中配置保持一致
tablePrefix: QRTZ_
isClustered: true
clusterCheckinInterval: 10000
useProperties: false
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 20
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true
job-store-type: jdbc
overwrite-existing-jobs: true
auto-startup: true
quartz数据源配置
在yml配置文件中我们配置了三个数据源,一个是task,存储持久化quartz的数据表;其他两个当作是业务数据源,可能来自不同的数据库。
多数据源配置可以参考我之前的文章Springboot+Mybatis+MySql整合多数据源及其使用
新建DataSourceConfig类,配置三个数据源,并通过@QuartzDataSource告诉spring容器quartz使用的数据源是哪一个。
@Configuration
@MapperScan(basePackages = "com.czhjsl.dao")
public class DataSourceConfig {
@Bean(name = "dataSource1")
@Primary
@ConfigurationProperties("spring.datasource.db1")
public DruidDataSource dataSource1 () {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dataSource2")
@ConfigurationProperties("spring.datasource.db2")
public DruidDataSource dataSource2 () {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dataSourceTask")
@ConfigurationProperties("spring.datasource.task")
@QuartzDataSource
public DruidDataSource dataSourceTask () { return DruidDataSourceBuilder.create().build(); }
@Bean(name = "myRoutingDataSource")
public MyRoutingDataSource myRoutingDataSource (@Qualifier("dataSource1") DruidDataSource dataSource1,
@Qualifier("dataSource2") DruidDataSource dataSource2) {
Map<Object, Object> map = new HashMap<>();
map.put(DataSourceType.DB1, dataSource1);
map.put(DataSourceType.DB2, dataSource2);
MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
myRoutingDataSource.setTargetDataSources(map);
myRoutingDataSource.setDefaultTargetDataSource(dataSource1);
return myRoutingDataSource;
}
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory (@Qualifier("dataSource1") DruidDataSource dataSource1,
@Qualifier("dataSource2") DruidDataSource dataSource2) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(myRoutingDataSource(dataSource1,dataSource2));
// 设置mapper.xml的位置路径
Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*/*.xml");
factoryBean.setMapperLocations(resources);
return factoryBean.getObject();
}
@Bean
public PlatformTransactionManager transactionManager (@Qualifier("myRoutingDataSource")MyRoutingDataSource myRoutingDataSource){
return new DataSourceTransactionManager(myRoutingDataSource);
}
}
任务类实现
不同于以前,在spring-boot-starter-quartz中通过继承QuartzJobBean,覆写executeInternal方法,来实现任务类
@Slf4j
public class DemoJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//这里边写你的任务所要干的任何事情
JobDetail jobDetail = jobExecutionContext.getJobDetail();
log.info("------任务名:" + jobDetail.getKey().getName() + ",组名:" +
jobDetail.getKey().getGroup() + "------我是要执行的定时任务工作内容!");
}
任务“增删改查”
任务详情对象
@Data
public class JobInfo implements Serializable {
private static final long serialVersionUID = 8026140551673459050L;
private int id;
private String jobname;
private String jobgroup;
private String jobclassname;
private String triggername;
private String triggergroup;
private String cronexpression;
private String description;
private String prefiretime;
private String nextfiretime;
private String state;
}
业务操作service类
public interface IJobService {
//查询所有任务
public List<JobInfo> getAllJobs();
//恢复任务
public boolean resumeJob(String jobName,String jobGroup);
//停止任务
public boolean pauseJob(String jobName,String jobGroup);
//修改任务执行周期表达式
public boolean reScheduleJob(String jobName,String jobGroup,String cronExpression);
//删除任务
public boolean deleteJob(String jobName,String jobGroup);
//新增任务
public int addJob(JobInfo jobInfo);
//判断任务是否存在
public int isJobExist(JobKey jobKey);
}
@Service
@Slf4j
public class JobServiceImp implements IJobService {
//Springboot已经为我们自动装配了任务调度器Scheduler,
//无需额外配置便可以注入使用,由Springboot为我们管理调度器
@Autowired
private Scheduler scheduler;
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public int isJobExist(JobKey jobKey) {
int result = 1;
try {
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
if (jobDetail != null && triggers.size() > 0)
result = 1;
else if (jobDetail != null && triggers.size() == 0)
result = 0;
else
result = -1;
} catch (SchedulerException e) {
result = -1;
log.info("任务不存在!");
}
return result;
}
@Override
public List<JobInfo> getAllJobs() {
List<JobInfo> jobInfos = new ArrayList<>();
try {
List<String> groups = scheduler.getJobGroupNames();
int i = 0;
for(String group :groups) {
GroupMatcher<JobKey> groupMatcher = GroupMatcher.groupEquals(group);
Set<JobKey> jobKeys = scheduler.getJobKeys(groupMatcher);
for(JobKey jobKey:jobKeys) {
JobInfo jobInfo = new JobInfo();
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
jobInfo.setJobname(jobKey.getName());
jobInfo.setJobgroup(jobKey.getGroup());
jobInfo.setJobclassname(jobDetail.getJobClass().getName());
Trigger jobTrigger = scheduler.getTrigger(TriggerKey.triggerKey(jobKey.getName(),jobKey.getGroup()));
if (jobTrigger != null) {
Trigger.TriggerState tState = scheduler.getTriggerState(TriggerKey.triggerKey(jobKey.getName(),jobKey.getGroup()));
jobInfo.setTriggername(jobKey.getName());
jobInfo.setTriggergroup(jobKey.getGroup());
try {
CronTrigger cronTrigger = (CronTrigger)jobTrigger;
jobInfo.setCronexpression(cronTrigger.getCronExpression());
}catch (Exception e) {
log.info("不是CronTrigger");
}
if (jobTrigger.getNextFireTime() != null)
jobInfo.setNextfiretime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(jobTrigger.getNextFireTime()));
jobInfo.setDescription(jobDetail.getDescription());
jobInfo.setState(tState.name());
jobInfo.setId(i);
jobInfos.add(jobInfo);
i += 1;
} else {
jobInfo.setState("OVER");
jobInfo.setId(i);
jobInfos.add(jobInfo);
i += 1;
}
}
}
} catch (SchedulerException e) {
log.error(e.getMessage());
}
return jobInfos;
}
@Override
public boolean resumeJob(String jobName, String jobGroup) {
boolean result = true;
try {
scheduler.resumeJob(JobKey.jobKey(jobName,jobGroup));
} catch (SchedulerException e) {
result = false;
log.error(e.getMessage());
}
return result;
}
@Override
public boolean pauseJob(String jobName, String jobGroup) {
boolean result = true;
try {
scheduler.pauseJob(JobKey.jobKey(jobName,jobGroup));
} catch (SchedulerException e) {
result = false;
log.error(e.getMessage());
}
return result;
}
@Override
public boolean reScheduleJob(String jobName, String jobGroup, String cronExpression) {
//判断当前状态
boolean result = true;
try {
Trigger.TriggerState triggerState = scheduler.getTriggerState(TriggerKey.triggerKey(jobName,jobGroup));
CronTrigger cronTriggerOld = (CronTrigger)scheduler.getTrigger(TriggerKey.triggerKey(jobName,jobGroup));
if (!cronTriggerOld.getCronExpression().equals(cronExpression)){
CronTrigger cronTriggerNew = TriggerBuilder.newTrigger().withIdentity(jobName,jobGroup)
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
.build();
scheduler.rescheduleJob(TriggerKey.triggerKey(jobName,jobGroup),cronTriggerNew);
if (triggerState.name().equals("PAUSED"))
this.pauseJob(jobName,jobGroup);
}
} catch (SchedulerException e) {
result = false;
log.error(e.getMessage());
}
return result;
}
@Override
public boolean deleteJob(String jobName, String jobGroup) {
boolean result = true;
try {
List<? extends Trigger> triggers = scheduler.getTriggersOfJob(JobKey.jobKey(jobName,jobGroup));
if (triggers.size() > 0) {
if (!"PAUSED".equals(scheduler.getTriggerState(TriggerKey.triggerKey(jobName,jobGroup)).name()))
scheduler.pauseTrigger(TriggerKey.triggerKey(jobName,jobGroup));
scheduler.unscheduleJob(TriggerKey.triggerKey(jobName,jobGroup));
}
scheduler.deleteJob(JobKey.jobKey(jobName,jobGroup));
} catch (SchedulerException e) {
result = false;
log.error(e.getMessage());
}
return result;
}
@Override
public int addJob(JobInfo jobInfo) {
int result = 0;
int isJobExist = this.isJobExist(JobKey.jobKey(jobInfo.getJobname(),jobInfo.getJobgroup()));
if (isJobExist == 1) {
result = -1;
log.info("任务已经存在!");
} else {
try {
JobDetail jobDetail = null;
if (isJobExist == 0) {
jobDetail = scheduler.getJobDetail(JobKey.jobKey(jobInfo.getJobname(),jobInfo.getJobgroup()));
}else if (isJobExist == -1) {
jobDetail = JobBuilder.newJob(
(Class<? extends QuartzJobBean>)Class.forName(jobInfo.getJobclassname()))
.withIdentity(jobInfo.getJobname(),jobInfo.getJobgroup())
.withDescription(jobInfo.getDescription())
.storeDurably().build();
}
//如果jobInfo的cron表达式为空,则创建常规任务,反之创建周期任务
if (!StringUtils.isEmpty(jobInfo.getCronexpression())) {
CronTrigger cronTrigger = TriggerBuilder.newTrigger()
.withIdentity(jobInfo.getTriggername(),jobInfo.getTriggergroup())
.withSchedule(CronScheduleBuilder.cronSchedule(jobInfo.getCronexpression()))
.build();
scheduler.scheduleJob(jobDetail,cronTrigger);
} else {
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(jobInfo.getJobname(),jobInfo.getJobgroup())
.startAt(sdf.parse(jobInfo.getNextfiretime()))
.withSchedule(SimpleScheduleBuilder.simpleSchedule().
withRepeatCount(0))
.build();
scheduler.scheduleJob(jobDetail,trigger);
}
}catch (ClassNotFoundException e) {
result = 1;
log.error("任务对应的Class类不存在");
} catch (SchedulerException e) {
result = 2;
log.error("任务调度失败");
} catch (ParseException e) {
result = 3;
log.error("时间转换出错");
}
}
return result;
}
}
前端接口controller类
@Controller
@RequestMapping(value = "/task")
@Slf4j
public class JobController {
@Autowired
private IJobService jobService;
@GetMapping(value = "/ok")
@ResponseBody
public String sendok(){
return "ok";
}
@PostMapping(value = "/add")
@ResponseBody
public Response addJob(@RequestBody JobInfo jobInfo) {
Response response = null;
switch (jobService.addJob(jobInfo)) {
case -1 : response = new Response(500,"任务已存在!",false);break;
case 0 : response = new Response(200,"success",true);break;
case 1 : response = new Response(500,"没有该任务对应的 Java Class 类!",false);break;
case 2 : response = new Response(500,"添加任务失败!",false);break;
case 3 : response = new Response(500,"时间格式错误!",false);break;
}
return response;
}
@GetMapping(value = "/jobs")
@ResponseBody
public Response getAllJobs() {
List<JobInfo> jobInfos = jobService.getAllJobs();
return jobInfos.size() > 0 ? new Response(200,"success",jobInfos)
: new Response(500,"No job",false);
}
@PostMapping(value = "/pause")
@ResponseBody
public Response pauseJob(String name, String group) {
return jobService.pauseJob(name,group) ? new Response(200,"success",true)
: new Response(500,"errror",false);
}
@PostMapping(value = "/resume")
@ResponseBody
public Response resumeJob(String name, String group) {
return jobService.resumeJob(name,group) ? new Response(200,"success",true)
: new Response(500,"errror",false);
}
@PostMapping(value = "/reschedule")
@ResponseBody
public Response reScheduleJob(String name, String group, String cron) {
return jobService.reScheduleJob(name, group, cron) ? new Response(200,"success",true)
: new Response(500,"errror",false);
}
@PostMapping(value = "/delete")
@ResponseBody
public Response deleteJob(String name, String group) {
return jobService.deleteJob(name,group) ? new Response(200,"success",true)
: new Response(500,"errror",false);
}
//校验是否是合法cron表达式
@PostMapping(value = "/cron-check")
@ResponseBody
public Response checkCron(String cron) {
boolean valide = false;
try {
valide = CronExpression.isValidExpression(cron);
}catch (Exception e){
log.error(e.getMessage());
}
return valide ? new Response(200,"success",true)
: new Response(500,"cron表达式格式错误!",false);
}
}
测试
可以看到新增的任务已经开始执行了。新增方法如果有参数nextfiretime没有cronexpression,那么任务只会在nextfiretime的时候执行一次。
其它接口已在公司项目测试通过,如果还有疑问,请留言或私信~