Creating Component-Based Jobs with Quartz Scheduler

edit

Creating Component-Based Jobs with Quartz Scheduler

Execute CFML components (CFCs) as scheduled tasks with Quartz Scheduler.

For general overview, see Quartz Scheduler.

Advantages over URL-based jobs:

  • Full CFML capabilities
  • Object-oriented design
  • Dependency injection for configuration
  • Testable, reusable components

Component Mappings

Lucee uses component mappings to locate your components.

Default Component Mappings

Default mapping configuration:

{
  "componentMappings": [
    {
      "physical": "{lucee-config}/components/",
      "virtual": "/7c0791ef8c6ceb3efef56e85a04ae393",
      "archive": "",
      "primary": "physical",
      "inspectTemplate": "always"
    }
  ]
}

Similar to Java classpath.

Configuring Custom Mappings

Extend mappings by:

  1. Editing lucee-server/context/.CFConfig.json
  2. Using Lucee Administrator > Archives & Resources > Component Mappings

Creating a Component-Based Job

Step 1: Create the Component

Create a CFC with an execute() method. Optionally include init() to receive configuration parameters.

// path: {lucee-config}/components/jobs/DatabaseCleanupJob.cfc
component {

// Properties property name="tableName" type="string"; property name="retentionDays" type="numeric"; property name="logName" type="string" default="scheduler";
// Constructor - receives job parameters public void function init( required string tableName, numeric retentionDays=30, string logName="scheduler" ) { variables.tableName = arguments.tableName; variables.retentionDays = arguments.retentionDays; variables.logName = arguments.logName;
log log=variables.logName type="info" text="DatabaseCleanupJob initialized for table: #variables.tableName#"; }
// Required execute method - called when the job runs public void function execute() { try { log log=variables.logName type="info" text="Starting cleanup for table: #variables.tableName#";
// Sample cleanup logic var cutoffDate = dateAdd("d", -variables.retentionDays, now()); var result = queryExecute( "DELETE FROM #variables.tableName# WHERE created_date < :cutoffDate", {cutoffDate: {value: cutoffDate, cfsqltype: "CF_SQL_TIMESTAMP"}}, {datasource: "myDatasource"} );
log log=variables.logName type="info" text="Cleanup complete. Removed #result.recordCount# records from #variables.tableName#"; } catch(any e) { log log=variables.logName type="error" text="Error in DatabaseCleanupJob: #e.message#" exception=e; rethrow; } } }

Step 2: Place the Component in a Mapped Location

  • Save in default directory: {lucee-config}/components/jobs/DatabaseCleanupJob.cfc
  • Or create a custom mapping to your component's location

Step 3: Configure the Job

Add to Quartz Scheduler configuration:

{
  "jobs": [
    {
      "label": "Database Cleanup - User Logs",
      "component": "jobs.DatabaseCleanupJob",
      "cron": "0 0 3 * * ?",  // Run at 3 AM daily
      "pause": false,
      "mode": "transient",
      "tableName": "user_logs",
      "retentionDays": 90
    }
  ]
}

Component Modes

  • Transient (default): New instance per execution. Use when no state needed between runs.
  • Singleton: Single reused instance. Use for stateful jobs or expensive initialization.

Singleton example:

{
  "label": "Incremental Data Processor",
  "component": "jobs.DataProcessor",
  "cron": "0 */15 * * * ?",  // Every 15 minutes
  "mode": "singleton",
  "batchSize": 100
}

Creating a Job Listener

Job listeners monitor job execution events - useful for logging, notifications, or job coordination.

Step 1: Create the Listener Component

// path: {lucee-config}/components/listeners/JobMonitorListener.cfc
component {

// Properties property name="name" type="string"; property name="stream" type="string"; property name="logFile" type="string";
// Constructor - receives listener parameters public void function init(struct listenerData) { variables.name = "JobMonitorListener"; variables.stream = listenerData.stream ?: "err"; variables.logFile = listenerData.logFile ?: "";
// Initialize any resources if (len(variables.logFile)) { // Ensure log directory exists var logDir = getDirectoryFromPath(variables.logFile); if (!directoryExists(logDir)) { directoryCreate(logDir); } } }
// Required method - returns the name of the listener public string function getName() { return variables.name; }
// Called before a job executes public void function jobToBeExecuted(jobExecutionContext) { var jobDetail = jobExecutionContext.getJobDetail(); var jobDataMap = jobDetail.getJobDataMap(); var jobName = jobDataMap.get("label") ?: jobDetail.getKey().toString();
var message = "#now()# - Job starting: #jobName#"; writeToLog(message); }
// Called after a job executes public void function jobWasExecuted(jobExecutionContext, jobException) { var jobDetail = jobExecutionContext.getJobDetail(); var jobDataMap = jobDetail.getJobDataMap(); var jobName = jobDataMap.get("label") ?: jobDetail.getKey().toString();
if (isNull(jobException)) { var message = "#now()# - Job completed successfully: #jobName#"; } else { var message = "#now()# - Job failed: #jobName# - Error: #jobException.getMessage()#"; }
writeToLog(message); }
// Called when a job is vetoed public void function jobExecutionVetoed(jobExecutionContext) { var jobDetail = jobExecutionContext.getJobDetail(); var jobDataMap = jobDetail.getJobDataMap(); var jobName = jobDataMap.get("label") ?: jobDetail.getKey().toString();
var message = "#now()# - Job execution vetoed: #jobName#"; writeToLog(message); }
// Helper function to write to log private void function writeToLog(required string message) { // Write to console if (variables.stream == "out") { systemOutput(message, true, true); } else { systemOutput(message, true, false); }
// Write to log file if configured if (len(variables.logFile)) { fileAppend(variables.logFile, message & chr(13) & chr(10)); } } }

Step 2: Place the Listener in a Mapped Location

Save in a mapped location, e.g. {lucee-config}/components/listeners/JobMonitorListener.cfc

Step 3: Configure the Listener

{
  "listeners": [
    {
      "component": "listeners.JobMonitorListener",
      "stream": "err",
      "logFile": "{lucee-config}/logs/quartz-jobs.log"
    }
  ]
}

Best Practices

  1. Organize Components: Use clear structure and namespaces (e.g., myapp.jobs.DataCleanup)
  2. Handle Exceptions: Always implement error handling in execute(), log detailed errors
  3. Keep Jobs Focused: Single responsibility per job, use helper components for complex operations
  4. Use Dependency Injection: Pass config values through job configuration, avoid hardcoding
  5. Include Logging: Track execution, use listeners for centralized monitoring

See also