BaseCalendar
专门用于屏蔽一个时间区间,使 Trigger 在这个区间中不被触发

AnnualCalendar
排除每一年中指定的一天或多少天,精度是天
CronCalendar
使用表达式排除某些时间段不执行,精度取决于 Cron 表达式,最大精度到秒
DailyCalendar
指定的时间范围内的每一天不执行,指定每天的时间段,格式是 HH: MM [: SS[mmm]],即最大精度可以到毫秒
HolidayCalendar
排除节假日,精度到天
MonthlyCalendar
排除月份中的数天,可选值为 1-31,精度是天
WeeklyCalendar
排除星期中的一天或多天,可选值比如为:java.util.Calendar.SUNDAY,精度是天
基本使用
public class CalendarSchedule {
public static void main(String[] args) throws SchedulerException {
//创建一个 JobDetail 的实例,将该实例与 HelloJob 绑定
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
.withIdentity("test")
.build();
AnnualCalendar holidays = new AnnualCalendar();
// 排除今天的时间 2017 年 11 月 27 日(月份是从 0~11 的)
GregorianCalendar nationalDay = new GregorianCalendar(2017, 10, 27);
// 排除的日期,如果为 false 则为包含
holidays.setDayExcluded(nationalDay,true);
Trigger simpleTrigger = TriggerBuilder.newTrigger()
.withIdentity("testTrigger")
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(1)
.repeatForever()) //每一秒执行一次 job
.modifiedByCalendar("holidays") //将我们设置好的 Calander 与 trigger 绑定
.build();
//创建 Scheduler 实例
StdSchedulerFactory stdSchedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = stdSchedulerFactory.getScheduler();
//向 Scheduler 注册日历
scheduler.addCalendar("holidays",holidays,false,false);
//让 trigger 应用指定的日历规则
scheduler.scheduleJob(jobDetail,simpleTrigger);
scheduler.start();
}
}
定时任务增删改查
参考 Scheduler 源码中的方法
通过配置文件定义每个任务的属性,项目启动后将每个任务加载到内存中,将其中一个任务用来监听配置文件的修改,当其修改后动态刷新任务
#定时任务
quartz:
jobs:
- name: myName #(随便取任务名)
group: collect
cron: 0 0/5 * * * ? *
jobClass: com.gamer.me.quartz.jobs.MyJob #(自己的定时任务的执行类,也就是你写业务代码的类)
desc: 我的任务
@Data
public class SchedulerJob {
private String name;
private String group;
private String cron;
private String jobClass;
private String desc;
}
@Data
@Component
@ConfigurationProperties(prefix = "quartz")
public class SchedulerJobs {
private List<SchedulerJob> jobs;
}
@Slf4j
@Component
public class SchedulerManager {
@Autowired
private Scheduler scheduler;
/**
* 激活任务
* @param schedulerJob
*/
public void activeJob(SchedulerJob schedulerJob){
JobKey jobKey = JobKey.jobKey(schedulerJob.getName(), schedulerJob.getGroup());
try {
if (scheduler.checkExists(jobKey) && !MonitorCronJob.JOB_NAME.equals(schedulerJob.getName())) {
updateJob(schedulerJob);
}else {
createJob(schedulerJob);
}
} catch (SchedulerException e) {
logger.error("activeJob {}", e);
}
}
/**
* 创建任务并加入调度
* @param schedulerJob
*/
public void createJob(SchedulerJob schedulerJob){
JobKey jobKey = JobKey.jobKey(schedulerJob.getName(), schedulerJob.getGroup());
try {
if (scheduler.checkExists(jobKey)) {
return;
}
Class<?> clazz = Class.forName(schedulerJob.getJobClass());
JobDetail jobDetail = getJobDetail(schedulerJob, (Class<Job>) clazz);
Trigger cronTrigger = getCronTrigger(schedulerJob);
//加入调度器
scheduler.scheduleJob(jobDetail, cronTrigger);
} catch (ClassNotFoundException | SchedulerException e) {
logger.error("createJob {}", e);
}
}
/**
* 更新任务触发器
* @param schedulerJob
*/
public void updateJob(SchedulerJob schedulerJob){
TriggerKey triggerKey = TriggerKey.triggerKey(schedulerJob.getName(), schedulerJob.getGroup());
try {
Trigger trigger = scheduler.getTrigger(triggerKey);
if (trigger == null) {
return;
}
JobKey jobKey = trigger.getJobKey();
//查询 cron
String oldCron = ((CronTrigger)trigger).getCronExpression();
//没有变化则返回
if (oldCron.equals(schedulerJob.getCron())){
return;
}
Trigger cronTrigger = getCronTrigger(schedulerJob);
//加入调度器
scheduler.rescheduleJob(triggerKey, cronTrigger);
} catch (SchedulerException e) {
logger.error("updateJob {}", e);
}
}
/**
* 删除任务
*/
public void deleteJobs(List<JobKey> jobKeys) {
try {
scheduler.deleteJobs(jobKeys);
} catch (SchedulerException e) {
logger.error("deleteJobs {}", e);
}
}
/**
* 创建任务
* @param schedulerJob
* @param clazz
* @return
*/
private JobDetail getJobDetail(SchedulerJob schedulerJob, Class<Job> clazz) {
return JobBuilder.newJob()
.ofType(clazz)
.withIdentity(schedulerJob.getName(), schedulerJob.getGroup())
.withDescription(schedulerJob.getDesc())
.build();
}
/**
* 创建触发器
* @param schedulerJob
* @return
*/
private Trigger getCronTrigger(SchedulerJob schedulerJob) {
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(schedulerJob.getCron());
if (!MonitorCronJob.JOB_NAME.equals(schedulerJob.getName())){
//任务错过执行策略,以错过的第一个频率时间立刻开始执行,重做错过的所有频率周期后,当下一次触发频率发生时间大于当前时间后,再按照正常的 Cron 频率依次执行
cronScheduleBuilder.withMisfireHandlingInstructionIgnoreMisfires();
}
return TriggerBuilder.newTrigger()
.withIdentity(schedulerJob.getName(), schedulerJob.getGroup())
.withDescription(schedulerJob.getDesc())
.withSchedule(cronScheduleBuilder)
.build();
}
}
监控其他定时任务的总任务 MonitorCronJob(用于监控 cron 的更新)
@Slf4j
public class MonitorCronJob implements Job {
public static final String JOB_NAME = "monitor_cron";
public static final String GROUP_NAME = "monitor";
public static final String CRON = "0 0/10 * * * ?";
public static final String DESC = "监控cron更新";
@Autowired
private SchedulerManager schedulerManager;
@Autowired
private SchedulerJobs schedulerJobs;
@Autowired
private ContextRefresher contextRefresher;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//重新加载配置
contextRefresher.refresh();
Set<JobKey> oldJobKeys = null;
try {
oldJobKeys = jobExecutionContext.getScheduler().getJobKeys(GroupMatcher.anyJobGroup());
} catch (SchedulerException e) {
logger.error("MonitorCronJob {}", e);
}
List<String> newJobKeys = new ArrayList<>();
for (SchedulerJob job : schedulerJobs.getJobs()) {
//过滤掉 monitor_cron 任务
if (job.getName().equals(JOB_NAME)) {
continue;
}
newJobKeys.add(job.getName());
logger.info("job【{}】,cron【{}】", job.getName(), job.getCron());
schedulerManager.activeJob(job);
}
if (oldJobKeys == null) {
return;
}
//删除没有配置的任务
List<JobKey> shouldDeleteJobKeys = oldJobKeys.stream()
.filter(jobKey -> !JOB_NAME.equals(jobKey.getName()) && !newJobKeys.contains(jobKey.getName()))
.collect(Collectors.toList());
logger.info("delete jobs {}", shouldDeleteJobKeys);
schedulerManager.deleteJobs(shouldDeleteJobKeys);
}
}
@Component
public class Initialization implements ApplicationRunner {
@Autowired
private SchedulerManager schedulerManager;
@Override
public void run(ApplicationArguments args) throws Exception {
SchedulerJob schedulerJob = new SchedulerJob();
schedulerJob.setName(MonitorCronJob.JOB_NAME);
schedulerJob.setGroup(MonitorCronJob.GROUP_NAME);
schedulerJob.setCron(MonitorCronJob.CRON);
schedulerJob.setDesc(MonitorCronJob.DESC);
schedulerJob.setJobClass(MonitorCronJob.class.getName());
schedulerManager.activeJob(schedulerJob);
}
}
SchedulerRepository 详解
/**
* 保持对调度器实例的引用-确保唯一性,以及防止垃圾收集,并允许“全球”查找-所有在一个类加载器空间。
* 调度程序库,采用单例模式存储任务调度 Schedule
*/
public class SchedulerRepository {
private HashMap<String, Scheduler> schedulers;
private static SchedulerRepository inst;
private SchedulerRepository() {
schedulers = new HashMap<String, Scheduler>();
}
public static synchronized SchedulerRepository getInstance() {
if (inst == null) {
inst = new SchedulerRepository();
}
return inst;
}
/**
* 绑定新的任务调度器
*/
public synchronized void bind(Scheduler sched) throws SchedulerException {
if ((Scheduler) schedulers.get(sched.getSchedulerName()) != null) {
throw new SchedulerException("Scheduler with name '"
+ sched.getSchedulerName() + "' already exists.");
}
schedulers.put(sched.getSchedulerName(), sched);
}
/**
* 移除新的任务调度器
*/
public synchronized boolean remove(String schedName) {
return (schedulers.remove(schedName) != null);
}
/**
* 查询新的任务调度器
*/
public synchronized Scheduler lookup(String schedName) {
return schedulers.get(schedName);
}
/**
* 查询所有任务调度器
*/
public synchronized Collection<Scheduler> lookupAll() {
return java.util.Collections.unmodifiableCollection(schedulers.values());
}
}
StdSchedulerFactory 详解
SchedulerFacotory 是一个接口,它有两个实现:
-
StdSchedulerFacotory 通过配置文件来设置 Scheduler 的各项参数 -
DirectSchedulerFactory 主要通过硬编码来设置 Scheduler 的各项参数
构造方法
public StdSchedulerFactory() {
}
public StdSchedulerFactory(Properties props) throws SchedulerException {
initialize(props);
}
public StdSchedulerFactory(String fileName) throws SchedulerException {
initialize(fileName);
}
getScheduler()
/**
* 加载 quartz.properties,初始化 StdScheduler
*/
public Scheduler getScheduler() throws SchedulerException {
// 加载 quartz.properties
if (cfg == null) {
initialize();
}
// 单例的 SchedulerRepository 实例
SchedulerRepository schedRep = SchedulerRepository.getInstance();
// 从缓存中查询获取 Schedule 任务,任务名称从配置中获取,若无指定,则默认指定 QuartzScheduler
Scheduler sched = schedRep.lookup(getSchedulerName());
// 判断若存在且已停止运行,则从缓存中移除
if (sched != null) {
if (sched.isShutdown()) {
schedRep.remove(getSchedulerName());
} else {
return sched;
}
}
// 初始化 StdScheduler
sched = instantiate();
return sched;
}
getScheduler()会推迟实例化,当调用其方法时才会去初始化 Quartz 的配置
initialize()
/**
* 根据配置文件初始化
*/
public void initialize() throws SchedulerException {
// 如果已经存在,直接返回
if (cfg != null) {
return;
}
if (initException != null) {
throw initException;
}
// 从环境变量 PROPERTIES_FILE = org.quartz.properties 是否存在指定读取的配置文件
String requestedFile = System.getProperty(PROPERTIES_FILE);
// 不主动设置,默认设置为 quartz.properties
String propFileName = requestedFile != null ? requestedFile: "quartz.properties";
File propFile = new File(propFileName);
Properties props = new Properties();
InputStream in = null;
// 读取配置文件内容,如果都不存在依次读取 quartz.properties、/quartz.properties、org/quartz/quartz.properties
try {
if (propFile.exists()) {
try {
if (requestedFile != null) {
propSrc = "specified file: '" + requestedFile + "'";
} else {
propSrc = "default file in current working dir: 'quartz.properties'";
}
in = new BufferedInputStream(new FileInputStream(propFileName));
props.load(in);
} catch (IOException ioe) {
initException = new SchedulerException("Properties file: '"
+ propFileName + "' could not be read.", ioe);
throw initException;
}
} else if (requestedFile != null) {
in = Thread.currentThread().getContextClassLoader().getResourceAsStream(requestedFile);
if(in == null) {
initException = new SchedulerException("Properties file: '"
+ requestedFile + "' could not be found.");
throw initException;
}
propSrc = "specified file: '" + requestedFile + "' in the class resource path.";
in = new BufferedInputStream(in);
try {
props.load(in);
} catch (IOException ioe) {
initException = new SchedulerException("Properties file: '"
+ requestedFile + "' could not be read.", ioe);
throw initException;
}
} else {
propSrc = "default resource file in Quartz package: 'quartz.properties'";
ClassLoader cl = getClass().getClassLoader();
if(cl == null)
cl = findClassloader();
if(cl == null)
throw new SchedulerConfigException("Unable to find a class loader on the current thread or class.");
in = cl.getResourceAsStream("quartz.properties");
if (in == null) {
in = cl.getResourceAsStream("/quartz.properties");
}
if (in == null) {
in = cl.getResourceAsStream("org/quartz/quartz.properties");
}
if (in == null) {
initException = new SchedulerException(
"Default quartz.properties not found in class path");
throw initException;
}
try {
props.load(in);
} catch (IOException ioe) {
initException = new SchedulerException(
"Resource properties file: 'org/quartz/quartz.properties' "
+ "could not be read from the classpath.", ioe);
throw initException;
}
}
} finally {
if(in != null) {
try { in.close(); } catch(IOException ignore) { /* ignore */ }
}
}
//赋值
initialize(overrideWithSysProps(props));
}
/**
* 添加系统配置,如果跟之前的配置相同则覆盖,以系统配置为主
*/
private Properties overrideWithSysProps(Properties props) {
Properties sysProps = null;
try {
sysProps = System.getProperties();
} catch (AccessControlException e) {
getLog().warn(
"Skipping overriding quartz properties with System properties " +
"during initialization because of an AccessControlException. " +
"This is likely due to not having read/write access for " +
"java.util.PropertyPermission as required by java.lang.System.getProperties(). " +
"To resolve this warning, either add this permission to your policy file or " +
"use a non-default version of initialize().",
e);
}
if (sysProps != null) {
props.putAll(sysProps);
}
return props;
}
public void initialize(Properties props) throws SchedulerException {
if (propSrc == null) {
propSrc = "an externally provided properties instance.";
}
this.cfg = new PropertiesParser(props);
}
通过查看源码可知,quartz 的配置文件加载流程如下:
-
查看是否有系统变量指定了配置文件路径 -
在当前项目下面查找名为 quartz.properties 的文件 -
通过类加载器加载当前 classpath 路径下的 quartz.properties、/quartz.properties、org/quartz/quartz.properties 的资源文件
这里需要注意的是,一旦自己指定了 quartz.properties 文件,那么 quartz 默认提供的配置将不会被加载,因此也就不会生效
Quartz 默认配置文件
#实例名称
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
#远程方法调用
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
#线程个数
org.quartz.threadPool.threadCount: 10
#线程优先级
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
#失火的阈值
org.quartz.jobStore.misfireThreshold: 60000
#使用内存方式存储
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
instantiate()
public class StdSchedulerFactory implements SchedulerFactory {
public static final String PROPERTIES_FILE = "org.quartz.properties";
public static final String PROP_SCHED_INSTANCE_NAME = "org.quartz.scheduler.instanceName";
public static final String PROP_SCHED_INSTANCE_ID = "org.quartz.scheduler.instanceId";
public static final String PROP_SCHED_INSTANCE_ID_GENERATOR_PREFIX = "org.quartz.scheduler.instanceIdGenerator";
public static final String PROP_SCHED_INSTANCE_ID_GENERATOR_CLASS =
PROP_SCHED_INSTANCE_ID_GENERATOR_PREFIX + ".class";
public static final String PROP_SCHED_THREAD_NAME = "org.quartz.scheduler.threadName";
public static final String PROP_SCHED_BATCH_TIME_WINDOW = "org.quartz.scheduler.batchTriggerAcquisitionFireAheadTimeWindow";
public static final String PROP_SCHED_MAX_BATCH_SIZE = "org.quartz.scheduler.batchTriggerAcquisitionMaxCount";
public static final String PROP_SCHED_JMX_EXPORT = "org.quartz.scheduler.jmx.export";
public static final String PROP_SCHED_JMX_OBJECT_NAME = "org.quartz.scheduler.jmx.objectName";
public static final String PROP_SCHED_JMX_PROXY = "org.quartz.scheduler.jmx.proxy";
public static final String PROP_SCHED_JMX_PROXY_CLASS = "org.quartz.scheduler.jmx.proxy.class";
public static final String PROP_SCHED_RMI_EXPORT = "org.quartz.scheduler.rmi.export";
public static final String PROP_SCHED_RMI_PROXY = "org.quartz.scheduler.rmi.proxy";
public static final String PROP_SCHED_RMI_HOST = "org.quartz.scheduler.rmi.registryHost";
public static final String PROP_SCHED_RMI_PORT = "org.quartz.scheduler.rmi.registryPort";
public static final String PROP_SCHED_RMI_SERVER_PORT = "org.quartz.scheduler.rmi.serverPort";
public static final String PROP_SCHED_RMI_CREATE_REGISTRY = "org.quartz.scheduler.rmi.createRegistry";
public static final String PROP_SCHED_RMI_BIND_NAME = "org.quartz.scheduler.rmi.bindName";
public static final String PROP_SCHED_WRAP_JOB_IN_USER_TX = "org.quartz.scheduler.wrapJobExecutionInUserTransaction";
public static final String PROP_SCHED_USER_TX_URL = "org.quartz.scheduler.userTransactionURL";
public static final String PROP_SCHED_IDLE_WAIT_TIME = "org.quartz.scheduler.idleWaitTime";
public static final String PROP_SCHED_DB_FAILURE_RETRY_INTERVAL = "org.quartz.scheduler.dbFailureRetryInterval";
public static final String PROP_SCHED_MAKE_SCHEDULER_THREAD_DAEMON = "org.quartz.scheduler.makeSchedulerThreadDaemon";
public static final String PROP_SCHED_SCHEDULER_THREADS_INHERIT_CONTEXT_CLASS_LOADER_OF_INITIALIZING_THREAD = "org.quartz.scheduler.threadsInheritContextClassLoaderOfInitializer";
public static final String PROP_SCHED_CLASS_LOAD_HELPER_CLASS = "org.quartz.scheduler.classLoadHelper.class";
public static final String PROP_SCHED_JOB_FACTORY_CLASS = "org.quartz.scheduler.jobFactory.class";
public static final String PROP_SCHED_JOB_FACTORY_PREFIX = "org.quartz.scheduler.jobFactory";
public static final String PROP_SCHED_INTERRUPT_JOBS_ON_SHUTDOWN = "org.quartz.scheduler.interruptJobsOnShutdown";
public static final String PROP_SCHED_INTERRUPT_JOBS_ON_SHUTDOWN_WITH_WAIT = "org.quartz.scheduler.interruptJobsOnShutdownWithWait";
public static final String PROP_SCHED_CONTEXT_PREFIX = "org.quartz.context.key";
public static final String PROP_THREAD_POOL_PREFIX = "org.quartz.threadPool";
public static final String PROP_THREAD_POOL_CLASS = "org.quartz.threadPool.class";
public static final String PROP_JOB_STORE_PREFIX = "org.quartz.jobStore";
public static final String PROP_JOB_STORE_LOCK_HANDLER_PREFIX = PROP_JOB_STORE_PREFIX + ".lockHandler";
public static final String PROP_JOB_STORE_LOCK_HANDLER_CLASS = PROP_JOB_STORE_LOCK_HANDLER_PREFIX + ".class";
public static final String PROP_TABLE_PREFIX = "tablePrefix";
public static final String PROP_SCHED_NAME = "schedName";
public static final String PROP_JOB_STORE_CLASS = "org.quartz.jobStore.class";
public static final String PROP_JOB_STORE_USE_PROP = "org.quartz.jobStore.useProperties";
public static final String PROP_DATASOURCE_PREFIX = "org.quartz.dataSource";
public static final String PROP_CONNECTION_PROVIDER_CLASS = "connectionProvider.class";
/**
* @deprecated Replaced with {@link PoolingConnectionProvider#DB_DRIVER}
*/
@Deprecated
public static final String PROP_DATASOURCE_DRIVER = "driver";
/**
* @deprecated Replaced with {@link PoolingConnectionProvider#DB_URL}
*/
@Deprecated
public static final String PROP_DATASOURCE_URL = "URL";
/**
* @deprecated Replaced with {@link PoolingConnectionProvider#DB_USER}
*/
@Deprecated
public static final String PROP_DATASOURCE_USER = "user";
/**
* @deprecated Replaced with {@link PoolingConnectionProvider#DB_PASSWORD}
*/
@Deprecated
public static final String PROP_DATASOURCE_PASSWORD = "password";
/**
* @deprecated Replaced with {@link PoolingConnectionProvider#DB_MAX_CONNECTIONS}
*/
@Deprecated
public static final String PROP_DATASOURCE_MAX_CONNECTIONS = "maxConnections";
/**
* @deprecated Replaced with {@link PoolingConnectionProvider#DB_VALIDATION_QUERY}
*/
@Deprecated
public static final String PROP_DATASOURCE_VALIDATION_QUERY = "validationQuery";
public static final String PROP_DATASOURCE_JNDI_URL = "jndiURL";
public static final String PROP_DATASOURCE_JNDI_ALWAYS_LOOKUP = "jndiAlwaysLookup";
public static final String PROP_DATASOURCE_JNDI_INITIAL = "java.naming.factory.initial";
public static final String PROP_DATASOURCE_JNDI_PROVDER = "java.naming.provider.url";
public static final String PROP_DATASOURCE_JNDI_PRINCIPAL = "java.naming.security.principal";
public static final String PROP_DATASOURCE_JNDI_CREDENTIALS = "java.naming.security.credentials";
public static final String PROP_PLUGIN_PREFIX = "org.quartz.plugin";
public static final String PROP_PLUGIN_CLASS = "class";
public static final String PROP_JOB_LISTENER_PREFIX = "org.quartz.jobListener";
public static final String PROP_TRIGGER_LISTENER_PREFIX = "org.quartz.triggerListener";
public static final String PROP_LISTENER_CLASS = "class";
public static final String DEFAULT_INSTANCE_ID = "NON_CLUSTERED";
public static final String AUTO_GENERATE_INSTANCE_ID = "AUTO";
public static final String PROP_THREAD_EXECUTOR = "org.quartz.threadExecutor";
public static final String PROP_THREAD_EXECUTOR_CLASS = "org.quartz.threadExecutor.class";
public static final String SYSTEM_PROPERTY_AS_INSTANCE_ID = "SYS_PROP";
public static final String MANAGEMENT_REST_SERVICE_ENABLED = "org.quartz.managementRESTService.enabled";
public static final String MANAGEMENT_REST_SERVICE_HOST_PORT = "org.quartz.managementRESTService.bind";
}
instantiate()方法主要就是根据之前加载好的配置文件来创建出 scheduler 需要用到的一些对象。
主要对象:
-
Job:业务逻辑类需要时间的接口 -
JobStore:主要用于存储 Job 和 Trigger -
JobFactory:job 实例化工厂 -
ThreadPool:主要用于分配任务给具体的线程进行执行 -
QuartzSchedulerThread:管理者线程 -
WorkThread:工作者线程 -
DBConnectionManager:数据库连接管理器 -
ThreadExecutor:线程执行器 -
SchedulerPlugin:调度器插件 -
JobListener:任务监听器 -
Trigger:触发器 -
Schedule:时间表 -
TriggerListerne:触发器监听器 -
JobRunShellFactory:jobRunShell 的工厂 -
JobRunShell:实现了 Runnale 接口,会调用对应实例化的 job 的 execute 方法
原文始发于微信公众号(爱编程的小生):Quartz(3-Scheduler)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/314652.html