diff --git a/build.gradle.kts b/build.gradle.kts index 6cbf907..0041300 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,8 @@ plugins { id("jacoco") id("signing") id("maven-publish") + // cannot read variable in plugins + id("org.gradlex.extra-java-module-info") version "1.4.1" } jacoco { diff --git a/gradle.properties b/gradle.properties index 8ae5d29..1afd25f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,6 +4,8 @@ project.version=1.0.2-SNAPSHOT project.url=https://github.com/arjenzhou/bm-kit gradle.wrapper.version=8.2.1 + jcoco.version=0.8.9 junit-bom.version=5.9.1 -slf4j.version=2.0.7 \ No newline at end of file +slf4j.version=2.0.7 +quertz.version=2.3.2 \ No newline at end of file diff --git a/lazy/src/main/java/com/arjenzhou/kit/lazy/UnsetException.java b/lazy/src/main/java/com/arjenzhou/kit/lazy/UnsetException.java index 42701ac..ac0e847 100644 --- a/lazy/src/main/java/com/arjenzhou/kit/lazy/UnsetException.java +++ b/lazy/src/main/java/com/arjenzhou/kit/lazy/UnsetException.java @@ -1,7 +1,7 @@ package com.arjenzhou.kit.lazy; /** - * Threw when evaluation virtual Lazy node + * Thrown when evaluation virtual Lazy node * * @author bm-kit@arjenzhou.com * @since 2023/7/8 diff --git a/scheduler/.gitignore b/scheduler/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/scheduler/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/scheduler/build.gradle.kts b/scheduler/build.gradle.kts new file mode 100644 index 0000000..f8c6b59 --- /dev/null +++ b/scheduler/build.gradle.kts @@ -0,0 +1,21 @@ +dependencies { + implementation("org.quartz-scheduler:quartz:${project.property("quertz.version")}") +} + +plugins { + id("org.gradlex.extra-java-module-info") +} + +extraJavaModuleInfo { + // quartz has not supported JPMS yet + module("quartz-${project.property("quertz.version")}.jar", "org.quertz.scheduler.quartz", "${project.property("quertz.version")}") { + requires("org.slf4j") + requires("java.desktop") + requires("java.rmi") + exportAllPackages() + } + // quartz related dependencies, whose versions are depend on quartz, not global used. + module("HikariCP-java7-2.4.13.jar", "hikari", "2.4.13") + module("c3p0-0.9.5.4.jar", "c3p0", "0.9.5.4") + module("mchange-commons-java-0.2.15.jar", "mchange.commons.java", "0.2.15") +} \ No newline at end of file diff --git a/scheduler/src/main/java/com/arjenzhou/kit/scheduler/SchedulerRunner.java b/scheduler/src/main/java/com/arjenzhou/kit/scheduler/SchedulerRunner.java new file mode 100644 index 0000000..f2e8f2b --- /dev/null +++ b/scheduler/src/main/java/com/arjenzhou/kit/scheduler/SchedulerRunner.java @@ -0,0 +1,31 @@ +package com.arjenzhou.kit.scheduler; + +import com.arjenzhou.kit.scheduler.job.internal.JobRunner; +import org.quartz.*; +import org.quartz.impl.StdSchedulerFactory; + +/** + * portal to run scheduler jobs + * + * @author bm-kit@arjenzhou.com + * @since 2023/7/21 + */ +public class SchedulerRunner { + /** + * run all scheduler jobs + * + * @throws SchedulerException when scheduler failed to start + */ + public static void run() throws SchedulerException { + SchedulerFactory factory = new StdSchedulerFactory(); + Scheduler scheduler = factory.getScheduler(); + JobDetail jobRunner = JobBuilder.newJob(JobRunner.class).build(); + Trigger trigger = TriggerBuilder.newTrigger() + .startNow() + // In every second, check whether each job can run or not, till forever + .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1).repeatForever()) + .build(); + scheduler.scheduleJob(jobRunner, trigger); + scheduler.start(); + } +} diff --git a/scheduler/src/main/java/com/arjenzhou/kit/scheduler/job/InMemorySchedulerJob.java b/scheduler/src/main/java/com/arjenzhou/kit/scheduler/job/InMemorySchedulerJob.java new file mode 100644 index 0000000..0b576f3 --- /dev/null +++ b/scheduler/src/main/java/com/arjenzhou/kit/scheduler/job/InMemorySchedulerJob.java @@ -0,0 +1,32 @@ +package com.arjenzhou.kit.scheduler.job; + +import java.util.Date; + +/** + * Scheduler Job whose context stored in memory, thus cannot being restored from any persistence storage. + * + * @author bm-kit@arjenzhou.com + * @since 2023/7/23 + */ +public abstract class InMemorySchedulerJob implements ScheduledJob { + /** + * the last time this job completed. + */ + private Date lastRunTime = new Date(0L); + + /** + * execute the job + */ + public abstract void execute(); + + @Override + public void start() { + execute(); + lastRunTime = new Date(); + } + + @Override + public Date getLastRunTime() { + return lastRunTime; + } +} diff --git a/scheduler/src/main/java/com/arjenzhou/kit/scheduler/job/Scheduled.java b/scheduler/src/main/java/com/arjenzhou/kit/scheduler/job/Scheduled.java new file mode 100644 index 0000000..aff376c --- /dev/null +++ b/scheduler/src/main/java/com/arjenzhou/kit/scheduler/job/Scheduled.java @@ -0,0 +1,19 @@ +package com.arjenzhou.kit.scheduler.job; + +import java.lang.annotation.*; + +/** + * Indicates that the Scheduled Job when to start. + * + * @author bm-kit@arjenzhou.com + * @since 2023/7/21 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Scheduled { + /** + * the quartz cron expression + */ + String cronExpression(); +} diff --git a/scheduler/src/main/java/com/arjenzhou/kit/scheduler/job/ScheduledJob.java b/scheduler/src/main/java/com/arjenzhou/kit/scheduler/job/ScheduledJob.java new file mode 100644 index 0000000..d52b7da --- /dev/null +++ b/scheduler/src/main/java/com/arjenzhou/kit/scheduler/job/ScheduledJob.java @@ -0,0 +1,22 @@ +package com.arjenzhou.kit.scheduler.job; + +import java.util.Date; + +/** + * Job to run at specific condition. + * All subclass should be provided to this class, and declared in META-INF/services/com.arjenzhou.kit.scheduler.job.ScheduledJob + * + * @author bm-kit@arjenzhou.com + * @since 2023/7/21 + */ +public interface ScheduledJob { + /** + * how the ScheduledJob perform action + */ + void start(); + + /** + * @return the latest time the job completed + */ + Date getLastRunTime(); +} diff --git a/scheduler/src/main/java/com/arjenzhou/kit/scheduler/job/internal/JobHolder.java b/scheduler/src/main/java/com/arjenzhou/kit/scheduler/job/internal/JobHolder.java new file mode 100644 index 0000000..b8353b1 --- /dev/null +++ b/scheduler/src/main/java/com/arjenzhou/kit/scheduler/job/internal/JobHolder.java @@ -0,0 +1,40 @@ +package com.arjenzhou.kit.scheduler.job.internal; + +import com.arjenzhou.kit.scheduler.job.ScheduledJob; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.ServiceLoader; +import java.util.stream.Collectors; + +/** + * Container which holds all instances that implemented ScheduledJob + * + * @author bm-kit@arjenzhou.com + * @see ScheduledJob + * @since 2023/7/23 + */ +public class JobHolder { + private static final Logger LOG = LoggerFactory.getLogger("JOB FACTORY"); + /** + * All jobs instances + */ + private static final List SCHEDULED_JOBS; + + static { + ServiceLoader jobServiceLoader = ServiceLoader.load(ScheduledJob.class); + SCHEDULED_JOBS = jobServiceLoader.stream().map(ServiceLoader.Provider::get).toList(); + String loadedJobs = SCHEDULED_JOBS.stream().map(j -> j.getClass().getTypeName()).collect(Collectors.joining("\n")); + LOG.info("load " + SCHEDULED_JOBS.size() + " jobs: \n" + loadedJobs); + LOG.info("check whether your service is declared in /META-INF/serivces/com.arjenzhou.kit.scheduler.job.ScheduledJob, " + + "or provides with com.arjenzhou.kit.scheduler.job.ScheduledJob or not."); + } + + /** + * @return all scheduled jobs + */ + public static List getAllJobs() { + return SCHEDULED_JOBS; + } +} diff --git a/scheduler/src/main/java/com/arjenzhou/kit/scheduler/job/internal/JobRunner.java b/scheduler/src/main/java/com/arjenzhou/kit/scheduler/job/internal/JobRunner.java new file mode 100644 index 0000000..3bb842f --- /dev/null +++ b/scheduler/src/main/java/com/arjenzhou/kit/scheduler/job/internal/JobRunner.java @@ -0,0 +1,48 @@ +package com.arjenzhou.kit.scheduler.job.internal; + +import com.arjenzhou.kit.scheduler.job.Scheduled; +import org.quartz.CronExpression; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.ParseException; +import java.util.Date; + +/** + * runner to run all scheduled jobs + * + * @author bm-kit@arjenzhou.com + * @since 2023/7/21 + */ +public class JobRunner implements Job { + private static final Logger LOG = LoggerFactory.getLogger("JOB RUNNER"); + + /** + * for JobBuilder to invoke + */ + public JobRunner() { + } + + @Override + public void execute(JobExecutionContext context) { + Date now = new Date(); + JobHolder.getAllJobs().forEach(scheduledJob -> { + Scheduled scheduled = scheduledJob.getClass().getAnnotation(Scheduled.class); + String expression = scheduled.cronExpression(); + try { + CronExpression cronExpression = new CronExpression(expression); + Date lastRunTime = scheduledJob.getLastRunTime(); + Date nextValidTimeAfterLastRun = cronExpression.getNextValidTimeAfter(lastRunTime); + // it is not the time to start this job + if (nextValidTimeAfterLastRun.after(now)) { + return; + } + scheduledJob.start(); + LOG.info("Job " + scheduledJob.getClass().getTypeName() + " started"); + } catch (ParseException ignored) { + } + }); + } +} diff --git a/scheduler/src/main/java/module-info.java b/scheduler/src/main/java/module-info.java new file mode 100644 index 0000000..c6536ae --- /dev/null +++ b/scheduler/src/main/java/module-info.java @@ -0,0 +1,15 @@ +/** + * @author bm-kit@arjenzhou.com + * @since 2023/7/23 + */ +module bm.kit.scheduler.main { + requires transitive org.quertz.scheduler.quartz; + requires org.slf4j; + + exports com.arjenzhou.kit.scheduler; + exports com.arjenzhou.kit.scheduler.job; + exports com.arjenzhou.kit.scheduler.job.internal to org.quertz.scheduler.quartz; + + // all jobs should be provided by this, for SPI reflection + uses com.arjenzhou.kit.scheduler.job.ScheduledJob; +} \ No newline at end of file diff --git a/scheduler/src/test/java/module-info.java b/scheduler/src/test/java/module-info.java new file mode 100644 index 0000000..dc245fc --- /dev/null +++ b/scheduler/src/test/java/module-info.java @@ -0,0 +1,16 @@ +import com.arjenzhou.kit.scheduler.job.ScheduledJob; +import test.com.arjenzhou.kit.scheduler.TestJob; + +/** + * @author bm-kit@arjenzhou.com + * @since 2023/7/23 + */ +module bm.kit.scheduler.test { + exports test.com.arjenzhou.kit.scheduler; + + requires bm.kit.scheduler.main; + requires org.slf4j; + requires org.junit.jupiter.api; + + provides ScheduledJob with TestJob; +} \ No newline at end of file diff --git a/scheduler/src/test/java/test/com/arjenzhou/kit/scheduler/SchedulerTest.java b/scheduler/src/test/java/test/com/arjenzhou/kit/scheduler/SchedulerTest.java new file mode 100644 index 0000000..42c212c --- /dev/null +++ b/scheduler/src/test/java/test/com/arjenzhou/kit/scheduler/SchedulerTest.java @@ -0,0 +1,19 @@ +package test.com.arjenzhou.kit.scheduler; + +import com.arjenzhou.kit.scheduler.SchedulerRunner; +import org.junit.jupiter.api.Test; +import org.quartz.SchedulerException; + +import java.util.concurrent.TimeUnit; + +/** + * @author bm-kit@arjenzhou.com + * @since 2023/7/21 + */ +public class SchedulerTest { + @Test + public void test() throws SchedulerException, InterruptedException { + SchedulerRunner.run(); + TimeUnit.SECONDS.sleep(15); + } +} diff --git a/scheduler/src/test/java/test/com/arjenzhou/kit/scheduler/TestJob.java b/scheduler/src/test/java/test/com/arjenzhou/kit/scheduler/TestJob.java new file mode 100644 index 0000000..f35ce50 --- /dev/null +++ b/scheduler/src/test/java/test/com/arjenzhou/kit/scheduler/TestJob.java @@ -0,0 +1,24 @@ +package test.com.arjenzhou.kit.scheduler; + +import com.arjenzhou.kit.scheduler.job.InMemorySchedulerJob; +import com.arjenzhou.kit.scheduler.job.Scheduled; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; + +/** + * Test Job which runs in every 5 seconds, and prints current system time. + * + * @author bm-kit@arjenzhou.com + * @since 2023/7/23 + */ +@Scheduled(cronExpression = "*/5 * * * * ?") +public class TestJob extends InMemorySchedulerJob { + private static final Logger LOG = LoggerFactory.getLogger(TestJob.class); + + @Override + public void execute() { + LOG.info(new Date().toString()); + } +} diff --git a/scheduler/src/test/resources/META-INF/services/com.arjenzhou.kit.scheduler.job.ScheduledJob b/scheduler/src/test/resources/META-INF/services/com.arjenzhou.kit.scheduler.job.ScheduledJob new file mode 100644 index 0000000..4202a0d --- /dev/null +++ b/scheduler/src/test/resources/META-INF/services/com.arjenzhou.kit.scheduler.job.ScheduledJob @@ -0,0 +1 @@ +test.com.arjenzhou.kit.scheduler.TestJob \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 5557f21..2a56e78 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,5 @@ rootProject.name = "bm-kit" include("statemachine") include("base") -include("lazy") \ No newline at end of file +include("lazy") +include("scheduler") \ No newline at end of file