Getting your program code to the point that it satisfies the functional requirements provided is a milestone for developers, one that hopefully brings satisfaction and a sense of accomplishment. If that code must be executed on a schedule perhaps for multiple uses with custom schedules and configurable parameters, this can mean a whole new set of problems.
We’re going to compare how we would write a job in Quartz and one in Obsidian that would satisfy the above requirements. We’ll use the example scenario of a recurring report. In this scenario, the report has the following dynamic criteria: it is emailed to a specified user, the report format can be selected, either PDF or Excel, and of course the execution frequency varies by user.
The following will be the sample class we’ll use to satisfy these requirements.
public class MyReportClass { public void emailReport(String emailAddress, String reportFormat) { …generate report in desired format …email report to user } }
For the purpose of this exercise, we will leave this class alone and write a wrapper class for scheduling, allowing for its continued use in non-scheduled contexts.
Let’s start with Obsidian. All Obsidian jobs start with implementing a single interface: SchedulableJob. Our Obsidian job class will look something like this:
import com.carfey.ops.job.Context; import com.carfey.ops.job.SchedulableJob; import com.carfey.ops.job.param.Configuration; import com.carfey.ops.job.param.Parameter; import com.carfey.ops.job.param.Type; @Configuration(knownParameters={ @Parameter(name= MyScheduledReportClass.EMAIL, type=Type.STRING, required=true), @Parameter(name= MyScheduledReportClass.REPORT_FORMAT, type=Type.STRING, defaultValue="PDF", required=true) }) public class MyScheduledReportClass implements SchedulableJob { public static final String EMAIL = "email"; public static final String REPORT_FORMAT = "reportFormat"; public void execute(Context context) throws Exception { String email = context.getConfig().getString(EMAIL); String reportFormat = context.getConfig().getString(REPORT_FORMAT); new MyReportClass().emailReport(email, reportFormat); } }
You’ll notice we can annotate the class with the required parameters. This ensures that when this job is scheduled for execution, the email and reportFormat parameters will always be available. Obsidian will not allow the job to be configured without these values and will also ensure their type. But we wouldn’t mind going a step further. We’d like to validate the reportFormat is valid. How can we do so before the job is run?
We can change our class to implement ConfigValidatingJob and implement the necessary method.
Now our class looks like this:
import com.carfey.ops.job.ConfigValidatingJob; import com.carfey.ops.job.Context; import com.carfey.ops.job.config.JobConfig; import com.carfey.ops.job.param.Configuration; import com.carfey.ops.job.param.Parameter; import com.carfey.ops.job.param.Type; import com.carfey.ops.parameter.ParameterException; import com.carfey.suite.action.ValidationException; @Configuration(knownParameters={ @Parameter(name= MyScheduledReportClass.EMAIL, type=Type.STRING, required=true), @Parameter(name= MyScheduledReportClass.REPORT_FORMAT, type=Type.STRING, defaultValue="PDF", required=true) }) public class MyScheduledReportClass implements ConfigValidatingJob { public static final String EMAIL = "email"; public static final String REPORT_FORMAT = "reportFormat"; public void execute(Context context) throws Exception { String email = context.getConfig().getString(EMAIL); String reportFormat = context.getConfig().getString(REPORT_FORMAT); new MyReportClass().emailReport(email, reportFormat); } public void validateConfig(JobConfig config) throws ValidationException, ParameterException { String reportFormat = config.getString(REPORT_FORMAT); if (!"PDF".equalsIgnoreCase(reportFormat) && !"EXCEL".equalsIgnoreCase(reportFormat)) { throw new ValidationException("Report format must be either PDF or EXCEL"); } } }
That’s it! Our job will now only accept being scheduled with an email address specified and a valid report format specified. You could easily extend this to other types of custom validation, such as ensuring the email address is valid or perhaps that is in an allowable domain.
Now for Quartz. Let’s first of all identify some differences. Quartz doesn’t provide any mechanisms for ensuring parameters are specified or are valid before runtime. And since Quartz doesn’t provide an execution context, the best you can do when you write your own code to do so is validate the parameters on startup. Our sample below will follow the easiest approach in Quartz, to simply fail the job at runtime if the report format is invalid.
import org.quartz.Job; import org.quartz.JobDataMap; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; public class MyScheduledReportClass implements Job { public static final String EMAIL = "email"; public static final String REPORT_FORMAT = "reportFormat"; public void execute(JobExecutionContext context) throws JobExecutionException { JobDataMap data = context.getMergedJobDataMap(); String email = data.getString(EMAIL); String reportFormat = data.getString(REPORT_FORMAT); if (!"PDF".equalsIgnoreCase(reportFormat) && !"EXCEL".equalsIgnoreCase(reportFormat)) { throw new JobExecutionException("Report format must be either PDF or EXCEL"); } new MyReportClass().emailReport(email, reportFormat); } }
You may be thinking that the classes seem fairly comparable and I would agree. But with the Obsidian job, there’s nothing else that needs to be done. Since setting the runtime schedule and specifying parameters tend to be fluid, those are not done in code or even in static configuration. Using Obsidian’s UI or REST API, you specify the schedule and parameters for each instance or version of the job that is needed.
Obsidian always provides an execution context that can be standalone or be embedded as a part of an existing execution context.
Quartz never provides an execution context. Unless you are deploying in a servlet container, you always need to initialize the scheduling environment. Even when using a servlet container, you must help Quartz along. That means that With Quartz, you’ve only a portion of the code and/or configuration you’ll need.
Initialize the scheduler:
SchedulerFactory sf = new StdSchedulerFactory(); Scheduler scheduler = sf.getScheduler(); scheduler.start();
No administration console and no REST API means code and/or config to schedule and parameterize your job.
JobDetail job = newJob(MyScheduledReportClass.class).withIdentity("joe's report", "group1").usingJobData(MyScheduledReportClass.EMAIL, "joe@****.com").usingJobData(MyScheduledReportClass.REPORT_FORMAT, "PDF").build(); Trigger trigger = newTrigger().withIdentity("trigger1", "group1").startNow().withSchedule(dailyAtHourAndMinute(1, 30)).build(); scheduler.scheduleJob(job, trigger);
Now this may not seem too bad, but now imagine that Joe says he wants the report in Excel, not PDF. Are you really going to say that it requires code changes, followed by a build, followed by testing, acceptance, and promoting a new release?
True, some of the above can be moved to configuration files. While that may avoid a build cycle, it does present its own set of issues. You still have to push new configuration files, restart the jvm process and deal with potential mistakes in the new configuration files that could potentially derail all scheduling.
This also doesn’t get into the issues surrounding misfires, Job Concurrency and execution exception handling and recoverability discussed here.
What do you think? Share your experiences using Quartz for scheduling in your java projects by leaving a comment. We’d like to hear from you.