本例使用quartz持久化数据库的方式,结合JPA,简单写几个web接口,演示集群状态下定时任务在页面的增删改查,验证同一定时任务同一时间执行的唯一性,避免多实例集群部署导致同一个定时任务执行多次。
springboot版本如下,自动引用的quartz版本为2.3.0,数据库使用mysql
<parent>
<groupId>org
.springframework
.boot
</groupId
>
<artifactId>spring
-boot
-starter
-parent
</artifactId
>
<version>2.1.0.RELEASE
</version
>
<relativePath
/> <!-- lookup parent from repository
-->
</parent
>
1、下载sql文件
1.1、下载quartz对应版本的sql文件
官方网址:http://www.quartz-scheduler.org/downloads/
1.2、导入sql文件到数据库
quartz-2.3.0-distribution\quartz-2.3.0-SNAPSHOT\src\org\quartz\impl\jdbcjobstore 下载后解压文件,通过以上路径找到对应的sql文件,导入数据库 导入成功可以看到新增如下表
1.3、各表代表意义
2、pom依赖
<dependencies>
<dependency>
<groupId>org
.springframework
.boot
</groupId
>
<artifactId>spring
-boot
-starter
-web
</artifactId
>
</dependency
>
<dependency>
<groupId>org
.springframework
.boot
</groupId
>
<artifactId>spring
-boot
-starter
</artifactId
>
</dependency
>
<dependency>
<groupId>org
.projectlombok
</groupId
>
<artifactId>lombok
</artifactId
>
<optional>true</optional
>
</dependency
>
<dependency>
<groupId>org
.springframework
.boot
</groupId
>
<artifactId>spring
-boot
-starter
-quartz
</artifactId
>
</dependency
>
<dependency>
<groupId>org
.Springframework
.boot
</groupId
>
<artifactId>spring
-boot
-starter
-data
-jpa
</artifactId
>
</dependency
>
<!--mysql链接依赖
-->
<dependency>
<groupId>mysql
</groupId
>
<artifactId>mysql
-connector
-java
</artifactId
>
</dependency
>
<dependency>
<groupId>org
.springframework
.boot
</groupId
>
<artifactId>spring
-boot
-starter
-jdbc
</artifactId
>
<exclusions>
<exclusion>
<groupId>org
.apache
.tomcat
</groupId
>
<artifactId>tomcat
-jdbc
</artifactId
>
</exclusion
>
</exclusions
>
</dependency
>
<dependency>
<groupId>com
.zaxxer
</groupId
>
<artifactId>HikariCP
-java7
</artifactId
>
<version>2.4.13</version
>
</dependency
>
<dependency>
<groupId>org
.springframework
.boot
</groupId
>
<artifactId>spring
-boot
-starter
-test
</artifactId
>
<scope>test
</scope
>
</dependency
>
<dependency>
<groupId>org
.springframework
.boot
</groupId
>
<artifactId>spring
-boot
-starter
-data
-jpa
</artifactId
>
</dependency
>
</dependencies
>
<build>
<plugins>
<plugin>
<groupId>org
.springframework
.boot
</groupId
>
<artifactId>spring
-boot
-maven
-plugin
</artifactId
>
</plugin
>
</plugins
>
</build
>
3、添加配置文件
3.1、application.yml
server
:
port
: 8081
logging
:
file
: logback
.xml
spring
:
dataSource
:
url
: jdbc
:mysql
://localhost
:3306/quartz
?characterEncoding
=utf
-8&autoReconnect
=true&allowMultiQueries
=true&useSSL
=true&zeroDateTimeBehavior
=convertToNull
&serverTimezone
=UTC
username
: root
password
: ******
driver
-class-name
: com
.mysql
.cj
.jdbc
.Driver
type
: com
.zaxxer
.hikari
.HikariDataSource
hikari
:
minimum
-idle
: 5
maximum
-pool
-size
: 15
idle
-timeout
: 30000
pool
-name
: DatebookHikariCP
max
-lifetime
: 1800000
connection
-timeout
: 30000
connection
-test
-query
: 'SELECT 1'
jpa
:
properties
:
hibernate
:
hbm2ddl
:
auto
: validate
dialect
: org
.hibernate
.dialect
.MySQL5InnoDBDialect
format_sql
: true
show
-sql
: true
3.2、quartz.properties
#quartz集群配置
#调度标识名 集群中每一个实例都必须使用相同的名称
org
.quartz
.scheduler
.instanceName
=DefaultQuartzScheduler
#ID设置为自动获取 每一个必须不同
org
.quartz
.scheduler
.instanceId
=AUTO
org
.quartz
.scheduler
.makeSchedulerThreadDaemon
=true
#线程池的实现类(一般使用SimpleThreadPool即可满足需求)
org
.quartz
.threadPool
.class=org
.quartz
.simpl
.SimpleThreadPool
#指定在线程池里面创建的线程是否是守护线程
org
.quartz
.threadPool
.makeThreadsDaemons
=true
#指定线程数,至少为
1(无默认值)
org
.quartz
.threadPool
.threadCount
:20
#设置线程的优先级(最大为java
.lang
.Thread
.MAX_PRIORITY
10,最小为Thread
.MIN_PRIORITY
1,默认为
5)
org
.quartz
.threadPool
.threadPriority
:5
#数据保存方式为数据库持久化
org
.quartz
.jobStore
.class=org
.quartz
.impl
.jdbcjobstore
.JobStoreTX
#数据库代理类,一般org
.quartz
.impl
.jdbcjobstore
.StdJDBCDelegate可以满足大部分数据库
org
.quartz
.jobStore
.driverDelegateClass
=org
.quartz
.impl
.jdbcjobstore
.StdJDBCDelegate
#表的前缀,默认QRTZ_
org
.quartz
.jobStore
.tablePrefix
=QRTZ_
#是否加入集群
org
.quartz
.jobStore
.isClustered
=true
# 信息保存时间 默认值
60秒
org
.quartz
.jobStore
.misfireThreshold
=25000
3.3、日志配置文件logback.xml
<?xml version
="1.0" encoding
="UTF-8"?>
<configuration scan
="true" scanPeriod
="30 seconds">
<property name
="LOG_HOME" value
="/data/button/springboot" />
<!-- 控制台输出
-->
<appender name
="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出
,%d
:日期
;%thread
:线程名
;%-5level:级别
,从左显示
5个字符宽度
;%msg
:日志消息
;%n
:换行符
-->
<pattern>%d
{yyyy
-MM
-dd HH
:mm
:ss
.SSS
} [%thread
] %-5level
%logger
{50} -%msg
%n
</pattern
>
</encoder
>
</appender
>
<!-- 按照每天生成日志文件
-->
<appender name
="FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>$
{LOG_HOME
}/springboot_button
.log
</file
>
<rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名
-->
<FileNamePattern>
$
{LOG_HOME
}/springboot_button
.log
.%d
{yyyy
-MM
-dd
}.%i
.log
</FileNamePattern
>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- 日志文件最大size
-->
<maxFileSize>100MB
</maxFileSize
>
</timeBasedFileNamingAndTriggeringPolicy
>
</rollingPolicy
>
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出
,%d
:日期
;%thread
:线程名
;%-5level:级别
,从左显示
5个字符宽度
;%msg
:日志消息
;%n
:换行符
-->
<pattern>%d
{HH
:mm
:ss
.SSS
} [%thread
] %-5level
%logger
{50} -%msg
%n
</pattern
>
</encoder
>
</appender
>
<!-- 日志输出级别
-->
<root level
="ERROR">
<appender
-ref ref
="STDOUT" />
<appender
-ref ref
="FILE" />
</root
>
</configuration
>
4、代码
4.1、添加quartz配置类
类QuartzConfigure
import org
.quartz
.Trigger
;
import org
.quartz
.spi
.JobFactory
;
import org
.springframework
.beans
.factory
.annotation
.Autowired
;
import org
.springframework
.beans
.factory
.annotation
.Qualifier
;
import org
.springframework
.beans
.factory
.config
.PropertiesFactoryBean
;
import org
.springframework
.context
.ApplicationContext
;
import org
.springframework
.context
.annotation
.Bean
;
import org
.springframework
.context
.annotation
.Configuration
;
import org
.springframework
.core
.io
.ClassPathResource
;
import org
.springframework
.scheduling
.quartz
.SchedulerFactoryBean
;
import javax
.sql
.DataSource
;
import java
.io
.IOException
;
import java
.util
.Properties
;
@Configuration
public class QuartzConfigure {
private static final String QUARTZ_CONFIG
= "/quartz.properties";
@Autowired
@Qualifier(value
= "dataSource")
private DataSource dataSource
;
@Bean
public Properties
quartzProperties() throws IOException
{
PropertiesFactoryBean propertiesFactoryBean
= new PropertiesFactoryBean();
propertiesFactoryBean
.setLocation(new ClassPathResource(QUARTZ_CONFIG
));
propertiesFactoryBean
.afterPropertiesSet();
return propertiesFactoryBean
.getObject();
}
@Bean
public JobFactory
buttonJobFactory(ApplicationContext applicationContext
) {
AutoWiredSpringBeanToJobFactory jobFactory
= new AutoWiredSpringBeanToJobFactory();
jobFactory
.setApplicationContext(applicationContext
);
return jobFactory
;
}
@Bean
public SchedulerFactoryBean
schedulerFactoryBean(JobFactory jobFactory
, Trigger
... cronJobTrigger
) throws IOException
{
SchedulerFactoryBean factory
= new SchedulerFactoryBean();
factory
.setJobFactory(jobFactory
);
factory
.setOverwriteExistingJobs(true);
factory
.setAutoStartup(true);
factory
.setQuartzProperties(quartzProperties());
factory
.setTriggers(cronJobTrigger
);
factory
.setDataSource(dataSource
);
return factory
;
}
}
类AutoWiredSpringBeanToJobFactory
import org
.quartz
.spi
.TriggerFiredBundle
;
import org
.springframework
.beans
.factory
.config
.AutowireCapableBeanFactory
;
import org
.springframework
.context
.ApplicationContext
;
import org
.springframework
.context
.ApplicationContextAware
;
import org
.springframework
.scheduling
.quartz
.SpringBeanJobFactory
;
public class AutoWiredSpringBeanToJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
private transient AutowireCapableBeanFactory beanFactory
;
@Override
public void setApplicationContext(final ApplicationContext context
) {
beanFactory
= context
.getAutowireCapableBeanFactory();
}
@Override
protected Object
createJobInstance(final TriggerFiredBundle bundle
) throws Exception
{
final Object job
= super.createJobInstance(bundle
);
beanFactory
.autowireBean(job
);
return job
;
}
}
4.2、model类
import lombok
.AllArgsConstructor
;
import lombok
.Data
;
import lombok
.NoArgsConstructor
;
import javax
.persistence
.Entity
;
import javax
.persistence
.GeneratedValue
;
import javax
.persistence
.Id
;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ScheduleInfo {
@Id
@GeneratedValue
private Long id
;
private String jobDetailName
;
private String jobDetailGroup
;
private String triggerName
;
private String triggerGroup
;
private Integer executeCount
;
public ScheduleInfo(String jobDetailName
,String jobDetailGroup
,String triggerName
,
String triggerGroup
,Integer executeCount
){
this.jobDetailName
= jobDetailName
;
this.jobDetailGroup
= jobDetailGroup
;
this.triggerName
= triggerName
;
this.triggerGroup
= triggerGroup
;
this.executeCount
= executeCount
;
}
}
import lombok
.AllArgsConstructor
;
import lombok
.Data
;
import lombok
.NoArgsConstructor
;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Response<T> {
Integer code
;
String message
;
T result
;
public Response
(String message
,T result
){
this.code
= 200;
this.message
= message
;
this.result
= result
;
}
}
4.3、数据库操作类
import com
.example
.quartzdemo
.model
.ScheduleInfo
;
import org
.springframework
.data
.jpa
.repository
.JpaRepository
;
public interface QuartzInfoRepository extends JpaRepository<ScheduleInfo,Long> {
ScheduleInfo
findByJobDetailName(String jobDetailName
);
void deleteByJobDetailName(String jobDetailName
);
}
4.4、quartz增删改查类
import com
.example
.quartzdemo
.dao
.QuartzInfoRepository
;
import com
.example
.quartzdemo
.model
.ScheduleInfo
;
import org
.quartz
.CronScheduleBuilder
;
import org
.quartz
.CronTrigger
;
import org
.quartz
.JobBuilder
;
import org
.quartz
.JobDataMap
;
import org
.quartz
.JobDetail
;
import org
.quartz
.JobKey
;
import org
.quartz
.Scheduler
;
import org
.quartz
.SchedulerException
;
import org
.quartz
.Trigger
;
import org
.quartz
.TriggerBuilder
;
import org
.quartz
.TriggerKey
;
import org
.quartz
.impl
.matchers
.GroupMatcher
;
import org
.springframework
.beans
.factory
.annotation
.Autowired
;
import org
.springframework
.scheduling
.quartz
.SchedulerFactoryBean
;
import org
.springframework
.stereotype
.Component
;
import java
.util
.ArrayList
;
import java
.util
.HashMap
;
import java
.util
.List
;
import java
.util
.Set
;
@Component
public class QuartzManager {
public static final String JOB_GROUP_NAME
= "cron_group";
public static final String TRIGGER_GROUP_NAME
= "cron_trigger";
public static final String DEFAULT_GROUP_NAME
= "DEFAULT";
@Autowired
private SchedulerFactoryBean schedulerFactory
;
@Autowired
QuartzInfoRepository quartzInfoRepository
;
public Scheduler
getScheduled(){
return schedulerFactory
.getScheduler();
}
public void addJob(String jobName
, Class
jobClazz , String cron
, HashMap
<String,String> params
){
try {
Scheduler scheduler
= getScheduled();
JobDetail jobDetail
= JobBuilder
.newJob(jobClazz
).withIdentity(jobName
, JOB_GROUP_NAME
).build();
JobDataMap jobDataMap
= jobDetail
.getJobDataMap();
if(params
!= null
){
for (String key
:params
.keySet()){
jobDataMap
.put(key
, params
.get(key
));
}
}
TriggerBuilder
<Trigger> triggerBuilder
= TriggerBuilder
.newTrigger();
triggerBuilder
.withIdentity(jobName
, TRIGGER_GROUP_NAME
);
triggerBuilder
.startNow();
triggerBuilder
.withSchedule(CronScheduleBuilder
.cronSchedule(cron
));
CronTrigger trigger
= (CronTrigger
) triggerBuilder
.build();
scheduler
.scheduleJob(jobDetail
, trigger
);
if (!scheduler
.isShutdown()) {
scheduler
.start();
}
quartzInfoRepository
.save(new ScheduleInfo(jobName
,JOB_GROUP_NAME
,
jobName
,TRIGGER_GROUP_NAME
,0));
} catch (SchedulerException e
) {
e
.printStackTrace();
}
}
public void removeJob(String jobName
) {
try {
Scheduler scheduler
= getScheduled();
TriggerKey triggerKey
= TriggerKey
.triggerKey(jobName
, TRIGGER_GROUP_NAME
);
scheduler
.pauseTrigger(triggerKey
);
scheduler
.unscheduleJob(triggerKey
);
scheduler
.deleteJob(JobKey
.jobKey(jobName
, JOB_GROUP_NAME
));
System
.out
.println("删除定时任务"+jobName
);
ScheduleInfo quartzInfo
= quartzInfoRepository
.findByJobDetailName(jobName
);
if(quartzInfo
!= null
){
quartzInfoRepository
.deleteById(quartzInfo
.getId());
}
} catch (SchedulerException e
) {
e
.printStackTrace();
}
}
public void removeDefaultJob(String jobName
) {
try {
Scheduler scheduler
= getScheduled();
TriggerKey triggerKey
= TriggerKey
.triggerKey(jobName
, DEFAULT_GROUP_NAME
);
scheduler
.pauseTrigger(triggerKey
);
scheduler
.unscheduleJob(triggerKey
);
scheduler
.deleteJob(JobKey
.jobKey(jobName
, DEFAULT_GROUP_NAME
));
} catch (SchedulerException e
) {
e
.printStackTrace();
}
}
public void startJobs() {
try {
Scheduler scheduler
= getScheduled();
scheduler
.start();
} catch (SchedulerException e
) {
e
.printStackTrace();
}
}
public void shutdownJobs() {
try {
Scheduler scheduler
= getScheduled();
if (!scheduler
.isShutdown()) {
scheduler
.shutdown();
}
} catch (SchedulerException e
) {
e
.printStackTrace();
}
}
public List
<String> getAllJob(){
List
<String> jobs
= new ArrayList<>();
try {
Set
<JobKey> jobKeys
= getScheduled().getJobKeys(GroupMatcher
.anyGroup());
for(JobKey jobKey
:jobKeys
){
jobs
.add(jobKey
.getName());
}
} catch (SchedulerException e
) {
e
.printStackTrace();
}
return jobs
;
}
}
4.5、定义Job
import com
.example
.quartzdemo
.dao
.QuartzInfoRepository
;
import com
.example
.quartzdemo
.model
.ScheduleInfo
;
import org
.quartz
.Job
;
import org
.quartz
.JobDataMap
;
import org
.quartz
.JobExecutionContext
;
import org
.quartz
.JobExecutionException
;
import org
.springframework
.beans
.factory
.annotation
.Autowired
;
import org
.springframework
.stereotype
.Component
;
import java
.text
.SimpleDateFormat
;
import java
.util
.Date
;
import java
.util
.HashMap
;
@Component
public class WebJob implements Job {
@Autowired
QuartzInfoRepository quartzInfoRepository
;
@Override
public void execute(JobExecutionContext context
) throws JobExecutionException
{
JobDataMap dataMap
= context
.getMergedJobDataMap();
HashMap
<String,String> map
= new HashMap<>();
for(Object o
:dataMap
.keySet()){
map
.put((String
)o
,dataMap
.getString((String
)o
));
}
String jobName
= context
.getJobDetail().getKey().getName();
ScheduleInfo quartzInfo
= quartzInfoRepository
.findByJobDetailName(jobName
);
if(quartzInfo
!= null
){
quartzInfo
.setExecuteCount(quartzInfo
.getExecuteCount()+1);
SimpleDateFormat df
= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String nowDate
= df
.format(new Date());
System
.out
.println(nowDate
+ " my web job prams: "+map
.toString()+quartzInfo
.getExecuteCount());
quartzInfoRepository
.save(quartzInfo
);
}
}
}
4.6、controller接口类
import com
.example
.quartzdemo
.job
.WebJob
;
import com
.example
.quartzdemo
.model
.Response
;
import com
.example
.quartzdemo
.quartzManager
.QuartzManager
;
import org
.springframework
.beans
.factory
.annotation
.Autowired
;
import org
.springframework
.web
.bind
.annotation
.GetMapping
;
import org
.springframework
.web
.bind
.annotation
.RequestMapping
;
import org
.springframework
.web
.bind
.annotation
.RestController
;
import java
.util
.HashMap
;
@RestController
@RequestMapping("/quartz")
public class QuartzController {
@Autowired
QuartzManager quartzManager
;
@GetMapping("/addJob")
public Response
addJob(String jobName
) {
String cron
= "0/3 * * * * ?";
HashMap
<String, String> map
= new HashMap<>();
map
.put("jobName", jobName
);
quartzManager
.addJob(jobName
, WebJob
.class, cron
, map
);
return new Response("job添加成功", "");
}
@GetMapping("/deleteJob")
public Response
deleteJob(String jobName
) {
quartzManager
.removeJob(jobName
);
return new Response("job删除成功", "");
}
@GetMapping("/removeDefaultJob")
public Response
removeDefaultJob(String jobName
) {
quartzManager
.removeDefaultJob(jobName
);
return new Response("job删除成功", "");
}
@GetMapping("/getAllJob")
public Response
getAllJob(String jobName
) {
return new Response("成功获取所有job", quartzManager
.getAllJob());
}
}
5、启动项目看看效果
复制2个项目出来,三个项目分别改一下端口为8081、8082、8083,依次启动项目 我们查一下所有定时任务发现为空 http://localhost:8081/quartz/getAllJob 添加一个定时任务 http://localhost:8081/quartz/addJob?jobName=JsonTom888 我们发现定时任务正常运行,输出如下结果,这个时候另外两个服务并没有任何输出,这个时候我们并不能确定定时任务集群状态满足了我们的要求。 继续添加一个定时任务 http://localhost:8081/quartz/addJob?jobName=Json888 我们发现定时任务正常启动,通过数字我们能够确认每次任务只在一个实例上运行了一次,但是运气不佳,还是只在同一个实例上运行 我们尝试删除一个定时任务看看 http://localhost:8081/quartz/addJob?jobName=Json888 这时我们发现同时在三个实例上输出了如下内容,说明quartz自动进行了负载均衡策略,通过每一行的时间和后面的数字,我们发现同一定时任务同一时间只在一个实例上运行了一次,符合我们集群状态下的预想。 总结:我们可以添加更多的定时任务看看效果,每次结果在每个实例上输出的可能不一样,但是我们能看出同一定时任务同一时间只在一个实例上运行一次,符合我们集群状态下的预想。
项目代码地址 https://github.com/JsonTom888/scheduledAndQuartz/tree/master/quartzcluster