Chapter 4: Scheduled and Asynchronous Tasks
Chapter Objectives
- Configure scheduled tasks with @Scheduled
- Master Cron expressions
- Implement asynchronous methods with @Async
- Configure thread pools
- Manage errors and monitoring
1. Basic Configuration
1.1 Enable Scheduling
package com.example.scheduling;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableScheduling
@EnableAsync
public class SchedulingApplication {
public static void main(String[] args) {
SpringApplication.run(SchedulingApplication.class, args);
}
}
1.2 Thread Pool Configuration
package com.example.scheduling.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
@Configuration
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setTaskScheduler(taskScheduler());
}
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10);
scheduler.setThreadNamePrefix("scheduled-");
scheduler.setAwaitTerminationSeconds(60);
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setErrorHandler(t -> {
// Log errors
System.err.println("Scheduled task error: " + t.getMessage());
});
return scheduler;
}
}
2. @Scheduled - Scheduled Tasks
2.1 Fixed Rate
Executes at a fixed interval from the start of the last execution.
@Component
public class ScheduledTasks {
private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
// Every 5 seconds
@Scheduled(fixedRate = 5000)
public void reportCurrentTime() {
log.info("Current time: {}", LocalDateTime.now());
}
// With initial delay of 10 seconds
@Scheduled(fixedRate = 5000, initialDelay = 10000)
public void taskWithInitialDelay() {
log.info("Task with initial delay");
}
// Using properties
@Scheduled(fixedRateString = "${task.fixed-rate:5000}")
public void configurableTask() {
log.info("Configurable task");
}
}
2.2 Fixed Delay
Executes at a fixed interval from the end of the last execution.
@Component
public class ScheduledTasks {
// 3 seconds after the end of the previous execution
@Scheduled(fixedDelay = 3000)
public void processQueue() {
log.info("Processing queue...");
// Simulate processing
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("Queue processed");
}
}
2.3 Cron Expressions
@Component
public class ScheduledTasks {
// Every day at midnight
@Scheduled(cron = "0 0 0 * * *")
public void dailyCleanup() {
log.info("Running daily cleanup");
}
// Every hour, at minute 0
@Scheduled(cron = "0 0 * * * *")
public void hourlyReport() {
log.info("Generating hourly report");
}
// Every Monday at 9 AM
@Scheduled(cron = "0 0 9 * * MON")
public void weeklyReport() {
log.info("Generating weekly report");
}
// The first day of every month at 6 AM
@Scheduled(cron = "0 0 6 1 * *")
public void monthlyReport() {
log.info("Generating monthly report");
}
// Every 15 minutes during business hours
@Scheduled(cron = "0 */15 9-17 * * MON-FRI")
public void businessHoursTask() {
log.info("Business hours task");
}
// With timezone
@Scheduled(cron = "0 0 9 * * *", zone = "America/Montreal")
public void taskWithTimezone() {
log.info("Task at 9 AM Montreal time");
}
}
2.4 Cron Syntax
┌───────────── second (0-59)
│ ┌───────────── minute (0-59)
│ │ ┌───────────── hour (0-23)
│ │ │ ┌───────────── day of month (1-31)
│ │ │ │ ┌───────────── month (1-12 or JAN-DEC)
│ │ │ │ │ ┌───────────── day of week (0-7 or SUN-SAT)
│ │ │ │ │ │
* * * * * *
| Expression | Description |
|---|---|
* | All values |
*/15 | Every 15 |
0-23 | Range of values |
1,15 | List of values |
? | Not specified (day) |
L | Last (day of month) |
W | Nearest weekday |
# | Nth day of the week (2#1 = first Monday) |
2.5 Common Cron Examples
| Expression | Description |
|---|---|
0 0 * * * * | Every hour |
0 */10 * * * * | Every 10 minutes |
0 0 8-18 * * * | Every hour from 8 AM to 6 PM |
0 0 0 * * * | Every day at midnight |
0 0 0 * * SUN | Every Sunday at midnight |
0 0 0 1 * * | First day of the month |
0 0 0 L * * | Last day of the month |
0 0 0 ? * 6L | Last Friday of the month |
3. @Async - Asynchronous Methods
3.1 Configuration
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
@Bean(name = "taskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
System.err.println("Async error in method " + method.getName() + ": " + ex.getMessage());
};
}
}
3.2 Asynchronous Methods
@Service
public class EmailService {
private static final Logger log = LoggerFactory.getLogger(EmailService.class);
// Fire and forget
@Async
public void sendEmailAsync(String to, String subject, String body) {
log.info("Sending email to {} on thread {}", to, Thread.currentThread().getName());
// Simulate sending
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("Email sent to {}", to);
}
// With return value
@Async
public CompletableFuture<Boolean> sendEmailWithResult(String to, String subject) {
log.info("Sending email to {}", to);
try {
// Simulate
Thread.sleep(2000);
log.info("Email sent successfully to {}", to);
return CompletableFuture.completedFuture(true);
} catch (Exception e) {
log.error("Failed to send email to {}", to, e);
return CompletableFuture.completedFuture(false);
}
}
// With specific executor
@Async("emailExecutor")
public void sendWithSpecificExecutor(String to) {
// Uses the "emailExecutor" bean
}
}
3.3 Usage
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
private final EmailService emailService;
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
// Create order (synchronous)
Order order = orderService.createOrder(request);
// Send email (asynchronous - does not block)
emailService.sendEmailAsync(
request.getEmail(),
"Order Confirmation",
"Your order #" + order.getId() + " has been created"
);
// Returns immediately
return ResponseEntity.ok(order);
}
@PostMapping("/bulk-email")
public ResponseEntity<String> sendBulkEmails(@RequestBody List<String> emails) {
// Send multiple emails in parallel
List<CompletableFuture<Boolean>> futures = emails.stream()
.map(email -> emailService.sendEmailWithResult(email, "Newsletter"))
.collect(Collectors.toList());
// Wait for all results
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenRun(() -> {
long successCount = futures.stream()
.map(CompletableFuture::join)
.filter(Boolean::booleanValue)
.count();
log.info("Sent {} emails successfully", successCount);
});
return ResponseEntity.accepted().body("Emails are being sent");
}
}
4. Common Use Cases
4.1 Expired Data Cleanup
@Component
public class CleanupTasks {
private final SessionRepository sessionRepository;
private final TempFileService tempFileService;
// Clean up expired sessions every hour
@Scheduled(cron = "0 0 * * * *")
public void cleanupExpiredSessions() {
log.info("Starting session cleanup");
LocalDateTime expirationThreshold = LocalDateTime.now().minusHours(24);
int deleted = sessionRepository.deleteByLastAccessBefore(expirationThreshold);
log.info("Cleaned up {} expired sessions", deleted);
}
// Clean up temporary files every day at 3 AM
@Scheduled(cron = "0 0 3 * * *")
public void cleanupTempFiles() {
log.info("Starting temp files cleanup");
int deleted = tempFileService.deleteOlderThan(Duration.ofDays(7));
log.info("Deleted {} temp files", deleted);
}
}
4.2 Report Generation
@Component
public class ReportTasks {
private final ReportService reportService;
private final EmailService emailService;
// Daily report at 6 AM
@Scheduled(cron = "0 0 6 * * *")
public void generateDailyReport() {
log.info("Generating daily report");
DailyReport report = reportService.generateDailyReport(LocalDate.now().minusDays(1));
emailService.sendReportAsync("admin@company.com", report);
log.info("Daily report generated and sent");
}
// Weekly report on Monday at 7 AM
@Scheduled(cron = "0 0 7 * * MON")
public void generateWeeklyReport() {
log.info("Generating weekly report");
WeeklyReport report = reportService.generateWeeklyReport();
emailService.sendReportAsync("management@company.com", report);
}
}
4.3 Data Synchronization
@Component
public class SyncTasks {
private final ExternalApiService externalApi;
private final ProductRepository productRepository;
// Synchronize prices every 15 minutes
@Scheduled(fixedRate = 900000) // 15 minutes
public void syncPrices() {
log.info("Starting price sync");
try {
List<PriceUpdate> updates = externalApi.fetchPriceUpdates();
updates.forEach(update -> {
productRepository.updatePrice(update.getProductId(), update.getNewPrice());
});
log.info("Synced {} price updates", updates.size());
} catch (Exception e) {
log.error("Price sync failed", e);
}
}
}
4.4 Health Check
@Component
public class HealthCheckTasks {
private final List<ExternalService> externalServices;
private final AlertService alertService;
// Check external services every minute
@Scheduled(fixedRate = 60000)
public void checkExternalServices() {
externalServices.forEach(service -> {
boolean healthy = service.healthCheck();
if (!healthy) {
log.warn("Service {} is unhealthy", service.getName());
alertService.sendAlert("Service " + service.getName() + " is down");
}
});
}
}
5. Error Handling
5.1 Try-Catch in Tasks
@Component
public class RobustScheduledTask {
@Scheduled(fixedRate = 60000)
public void robustTask() {
try {
performTask();
} catch (Exception e) {
log.error("Task failed, will retry on next schedule", e);
// Do not re-throw to avoid stopping scheduling
}
}
}
5.2 Global Error Handler
@Configuration
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
scheduler.setErrorHandler(t -> {
log.error("Scheduled task error", t);
// Send alert, metrics, etc.
alertService.sendAlert("Scheduled task failed: " + t.getMessage());
});
scheduler.initialize();
taskRegistrar.setTaskScheduler(scheduler);
}
}
6. Conditional Control
6.1 Enable/Disable by Configuration
@Component
@ConditionalOnProperty(name = "app.scheduling.enabled", havingValue = "true")
public class ScheduledTasks {
// These tasks only run if app.scheduling.enabled=true
}
6.2 Control by Profile
@Component
@Profile("!test") // Not in test environment
public class ProductionScheduledTasks {
@Scheduled(cron = "0 0 0 * * *")
public void productionOnlyTask() {
// Only runs in production
}
}
6.3 Leader Election (Cluster)
@Component
public class ClusterAwareTask {
private final LockProvider lockProvider;
@Scheduled(cron = "0 0 * * * *")
@SchedulerLock(name = "hourlyTask", lockAtLeastFor = "PT5M", lockAtMostFor = "PT30M")
public void hourlyTask() {
// Only one instance in the cluster executes this task
log.info("Running hourly task on this instance");
}
}
7. Summary
Annotations
| Annotation | Usage |
|---|---|
@EnableScheduling | Enables scheduling |
@EnableAsync | Enables async methods |
@Scheduled | Marks a scheduled method |
@Async | Marks an asynchronous method |
@Scheduled Options
| Option | Description |
|---|---|
fixedRate | Interval from start |
fixedDelay | Interval from end |
cron | Cron expression |
initialDelay | Initial delay |
zone | Timezone for Cron |
Best Practices
- Always handle exceptions in tasks
- Use dedicated thread pools
- Monitor task execution
- Avoid overly long tasks
- Use locking in cluster environments
- Log task start and end