الفصل 4: المهام المجدولة وغير المتزامنة
أهداف الفصل
- تكوين المهام المجدولة باستخدام @Scheduled
- إتقان تعبيرات Cron
- تنفيذ طرق غير متزامنة باستخدام @Async
- تكوين مجموعات مؤشرات الترابط
- إدارة الأخطاء والمراقبة
1. التكوين الأساسي
1.1 تفعيل الجدولة
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 تكوين مجموعة مؤشرات الترابط
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 -> {
// تسجيل الأخطاء
System.err.println("Scheduled task error: " + t.getMessage());
});
return scheduler;
}
}
2. @Scheduled - المهام المجدولة
2.1 معدل ثابت
تنفيذ بفاصل ثابت منذ بداية التنفيذ الأخير.
@Component
public class ScheduledTasks {
private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
// كل 5 ثواني
@Scheduled(fixedRate = 5000)
public void reportCurrentTime() {
log.info("Current time: {}", LocalDateTime.now());
}
// مع تأخير ابتدائي 10 ثواني
@Scheduled(fixedRate = 5000, initialDelay = 10000)
public void taskWithInitialDelay() {
log.info("Task with initial delay");
}
// استخدام الخصائص
@Scheduled(fixedRateString = "${task.fixed-rate:5000}")
public void configurableTask() {
log.info("Configurable task");
}
}
2.2 تأخير ثابت
تنفيذ بفاصل ثابت منذ نهاية التنفيذ الأخير.
@Component
public class ScheduledTasks {
// 3 ثواني بعد نهاية التنفيذ السابق
@Scheduled(fixedDelay = 3000)
public void processQueue() {
log.info("Processing queue...");
// محاكاة المعالجة
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("Queue processed");
}
}
2.3 تعبيرات Cron
@Component
public class ScheduledTasks {
// كل يوم عند منتصف الليل
@Scheduled(cron = "0 0 0 * * *")
public void dailyCleanup() {
log.info("Running daily cleanup");
}
// كل ساعة، في الدقيقة 0
@Scheduled(cron = "0 0 * * * *")
public void hourlyReport() {
log.info("Generating hourly report");
}
// كل يوم اثنين عند الساعة 9
@Scheduled(cron = "0 0 9 * * MON")
public void weeklyReport() {
log.info("Generating weekly report");
}
// اليوم الأول من كل شهر عند الساعة 6
@Scheduled(cron = "0 0 6 1 * *")
public void monthlyReport() {
log.info("Generating monthly report");
}
// كل 15 دقيقة خلال ساعات العمل
@Scheduled(cron = "0 */15 9-17 * * MON-FRI")
public void businessHoursTask() {
log.info("Business hours task");
}
// مع المنطقة الزمنية
@Scheduled(cron = "0 0 9 * * *", zone = "America/Montreal")
public void taskWithTimezone() {
log.info("Task at 9 AM Montreal time");
}
}
2.4 صيغة Cron
┌───────────── ثانية (0-59)
│ ┌───────────── دقيقة (0-59)
│ │ ┌───────────── ساعة (0-23)
│ │ │ ┌───────────── يوم من الشهر (1-31)
│ │ │ │ ┌───────────── شهر (1-12 أو JAN-DEC)
│ │ │ │ │ ┌───────────── يوم من الأسبوع (0-7 أو SUN-SAT)
│ │ │ │ │ │
* * * * * *
| التعبير | الوصف |
|---|---|
* | جميع القيم |
*/15 | كل 15 |
0-23 | نطاق القيم |
1,15 | قائمة القيم |
? | غير محدد (اليوم) |
L | الأخير (يوم من الشهر) |
W | أقرب يوم عمل |
# | اليوم النوني من الأسبوع (2#1 = أول يوم اثنين) |
2.5 أمثلة Cron شائعة
| التعبير | الوصف |
|---|---|
0 0 * * * * | كل ساعة |
0 */10 * * * * | كل 10 دقائق |
0 0 8-18 * * * | كل ساعة من 8 صباحًا إلى 6 مساءً |
0 0 0 * * * | كل يوم عند منتصف الليل |
0 0 0 * * SUN | كل يوم أحد عند منتصف الليل |
0 0 0 1 * * | اليوم الأول من الشهر |
0 0 0 L * * | اليوم الأخير من الشهر |
0 0 0 ? * 6L | آخر جمعة من الشهر |
3. @Async - طرق غير متزامنة
3.1 التكوين
@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 طرق غير متزامنة
@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());
// محاكاة الإرسال
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("Email sent to {}", to);
}
// مع إرجاع قيمة
@Async
public CompletableFuture<Boolean> sendEmailWithResult(String to, String subject) {
log.info("Sending email to {}", to);
try {
// محاكاة
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);
}
}
// مع منفذ محدد
@Async("emailExecutor")
public void sendWithSpecificExecutor(String to) {
// استخدام bean "emailExecutor"
}
}
3.3 الاستخدام
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
private final EmailService emailService;
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
// إنشاء الطلب (متزامن)
Order order = orderService.createOrder(request);
// إرسال البريد الإلكتروني (غير متزامن - لا يحجب)
emailService.sendEmailAsync(
request.getEmail(),
"Order Confirmation",
"Your order #" + order.getId() + " has been created"
);
// العودة فورًا
return ResponseEntity.ok(order);
}
@PostMapping("/bulk-email")
public ResponseEntity<String> sendBulkEmails(@RequestBody List<String> emails) {
// إرسال عدة رسائل بريد إلكتروني بالتوازي
List<CompletableFuture<Boolean>> futures = emails.stream()
.map(email -> emailService.sendEmailWithResult(email, "Newsletter"))
.collect(Collectors.toList());
// انتظار جميع النتائج
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. حالات الاستخدام الشائعة
4.1 تنظيف البيانات المنتهية
@Component
public class CleanupTasks {
private final SessionRepository sessionRepository;
private final TempFileService tempFileService;
// تنظيف الجلسات المنتهية كل ساعة
@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);
}
// تنظيف الملفات المؤقتة كل يوم عند الساعة 3
@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 توليد التقارير
@Component
public class ReportTasks {
private final ReportService reportService;
private final EmailService emailService;
// تقرير يومي عند الساعة 6
@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");
}
// تقرير أسبوعي يوم الاثنين عند الساعة 7
@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 مزامنة البيانات
@Component
public class SyncTasks {
private final ExternalApiService externalApi;
private final ProductRepository productRepository;
// مزامنة الأسعار كل 15 دقيقة
@Scheduled(fixedRate = 900000) // 15 دقيقة
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 فحص الصحة
@Component
public class HealthCheckTasks {
private final List<ExternalService> externalServices;
private final AlertService alertService;
// فحص الخدمات الخارجية كل دقيقة
@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. إدارة الأخطاء
5.1 Try-Catch في المهام
@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);
// لا تعيد الرمي لتجنب إيقاف الجدولة
}
}
}
5.2 معالج الأخطاء العام
@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);
// إرسال تنبيه، مقاييس، إلخ.
alertService.sendAlert("Scheduled task failed: " + t.getMessage());
});
scheduler.initialize();
taskRegistrar.setTaskScheduler(scheduler);
}
}
6. التحكم الشرطي
6.1 تفعيل/تعطيل بالتكوين
@Component
@ConditionalOnProperty(name = "app.scheduling.enabled", havingValue = "true")
public class ScheduledTasks {
// هذه المهام لا تُنفذ إلا إذا كان app.scheduling.enabled=true
}
6.2 التحكم بواسطة ملف التعريف
@Component
@Profile("!test") // ليس في بيئة الاختبار
public class ProductionScheduledTasks {
@Scheduled(cron = "0 0 0 * * *")
public void productionOnlyTask() {
// تُنفذ فقط في الإنتاج
}
}
6.3 انتخاب القائد (العنقود)
@Component
public class ClusterAwareTask {
private final LockProvider lockProvider;
@Scheduled(cron = "0 0 * * * *")
@SchedulerLock(name = "hourlyTask", lockAtLeastFor = "PT5M", lockAtMostFor = "PT30M")
public void hourlyTask() {
// تُنفذ هذه المهمة بواسطة مثيل واحد فقط في العنقود
log.info("Running hourly task on this instance");
}
}
7. الملخص
التعليقات التوضيحية
| التعليق التوضيحي | الاستخدام |
|---|---|
@EnableScheduling | تفعيل الجدولة |
@EnableAsync | تفعيل الطرق غير المتزامنة |
@Scheduled | وضع علامة على طريقة مجدولة |
@Async | وضع علامة على طريقة غير متزامنة |
خيارات @Scheduled
| الخيار | الوصف |
|---|---|
fixedRate | الفاصل منذ البداية |
fixedDelay | الفاصل منذ النهاية |
cron | تعبير Cron |
initialDelay | التأخير الابتدائي |
zone | المنطقة الزمنية لـ Cron |
أفضل الممارسات
- دائمًا إدارة الاستثناءات في المهام
- استخدام مجموعات مؤشرات الترابط المخصصة
- مراقبة تنفيذ المهام
- تجنب المهام الطويلة جدًا
- استخدام القفل في بيئة العنقود
- تسجيل بداية ونهاية المهام