- Published on
Spring Boot İle Scheduler Yapısı Oluşturma II
- Authors
- Name
- abidino
- @abidinozd
Giriş
Herkese selamlar! Bu yazıda, Spring Boot
kullanarak scheduler yapısı oluşturma serimize devam ediyoruz. Bir önceki yazımda dinamik olarak scheduler yapısı nasıl kurulabilir bundan bahsetmiştik, şimdi ise projemizi biraz daha genişleterek yeni özellikler kazandıralım.
Yeni Özellikler
Önceki yazımızdaki projemizde bir API yazmıştık ve bu API'ye gelen istekte bize "cronExpression" ve "name" alanları geliyordu. Biz "cronExpression" a göre belirli aralıklarla bu "name" i output'a yazdırıyorduk.
Şimdi bu basit uygulamayı biraz daha kapsamlı bir hale getirelim. Bir job oluşturduğumuzda onu çalıştırmak, güncellemek ve durdurmak isteriz. Önceki uygulamamızda bunları karşılayan bir akış bulunmamaktaydı, şimdi bu eksiklikleri teker teker giderelim.
1. Çalışan Task'ın İptali
@Service
public class SchedulerService {
private final TaskScheduler taskScheduler;
public SchedulerService(TaskScheduler taskScheduler) {
this.taskScheduler = taskScheduler;
}
public void addTask(TaskDefinition taskDefinition) {
MyJob myJob = new MyJob(taskDefinition.getName());
CronTrigger cronTrigger = new CronTrigger(taskDefinition.getCronExpression());
taskScheduler.schedule(myJob, cronTrigger);
}
}
Önceki geliştirmemizde yukarıda görüldüğü gibi SchedulerService
sınıfı içerisinde bir task'ın çalışması için TaskScheduler
'ın schedule methodunu çağırıyorduk. Peki ya task'ı iptal etmek istersek ne yapacağız?
Aslında schedule
methodu bize bir response dönüyor.
ScheduledFuture<?> schedule = taskScheduler.schedule(myJob, cronTrigger)
Önceki yazımızda thread'lerle ilgili bahsettiğimiz konuda Spring
, çalışan task'ları yönetmek için ScheduledThreadPool
kullandığından bahsetmiştik. Java
'da bir işlem thread'lerle yönetildiğinde Future
adlı bir nesne döner. Future adından da anlaşılacağı gibi "gelecektir". Burada yapılan işlem henüz tamamlanmadı ama sana bir Future
obje dönüyorum, tamamlandığında yapılacak bir işlemin varsa bunu kullanabilirsin der Java. Bu nedenle TaskScheduler
da aslında çalışan task'ları işleme aldıktan sonra Future
objesinden kalıtılmış, task'lar için özelleştirilen ScheduledFuture
objesi döner.
Bu dönen ScheduledFuture
objesinin cancel methodu var, bu method sayesinde çalışan bir task'ın, çalışmasını durdurabiliriz.
Bir T anında bunu gerçekleştirebilmek için bu nesneyi bir yerde saklamalıyız. Bunu yapmak için bir map oluşturabiliriz, bu map'in key değeri task'ın id'si, value değeri ise o task'ın scheduleFuture objesi olacak şekilde kurgulayabiliriz. Bunun için öncelikle her task'a unique bir id tanımlamalıyız, bu nedenle TaskDefinition sınıfını güncelliyoruz.
public class TaskDefinition {
private String id;
private String cronExpression;
private String name;
public TaskDefinition(String cronExpression, String name) {
this.cronExpression = cronExpression;
this.name = name;
}
public void initialize() {
this.id = UUID.randomUUID().toString();
}
public String getId() {
return id;
}
public String getCronExpression() {
return cronExpression;
}
public String getName() {
return name;
}
}
Şimdi SchedulerService
'imizi güncelleyebiliriz, içerisine bir map ekliyoruz, ve addTask methodunda TaskScheduler
'dan dönen değeri ve task'ın id'sini bu map'e kaydediyoruz. TaskDefinition
'ın initialize methodunu çağırmamızın nedeni ise içerisindeki id değerinin atanması ve bundan sonra ekleyebileceğimiz bazı kontrollerin bu method içerisinde yapılmasını sağlamak.
@Service
public class SchedulerService {
private final TaskScheduler taskScheduler;
private final Map<String, ScheduledFuture<?>> tasksMap = new ConcurrentHashMap<>();
public SchedulerService(TaskScheduler taskScheduler) {
this.taskScheduler = taskScheduler;
}
public void addTask(TaskDefinition taskDefinition) {
taskDefinition.initialize();
MyJob myJob = new MyJob(taskDefinition.getId());
CronTrigger cronTrigger = new CronTrigger(taskDefinition.getCronExpression());
ScheduledFuture<?> schedule = taskScheduler.schedule(myJob, cronTrigger);
tasksMap.put(taskDefinition.getId(), schedule);
}
}
Şimdi map'imizi de eklediğimize göre artık task'ları iptal edebiliriz, bunu yapabilmek için SchedulerService
'in içerisine cancelTask
methodunu ekliyoruz.
public void cancelTask(String taskId) {
ScheduledFuture<?> scheduledFuture = tasksMap.get(taskId);
if (Objects.nonNull(scheduledFuture)) {
scheduledFuture.cancel(true);
}
}
cancel
methodu içerisine bir boolean ifade bekliyor, mayInterruptIfRunning
. Aslında bu ifade task devam ediyorsa, bu aşamada kesilmesini istiyor muyuz istemiyor muyuz sorusuna karşılık geliyor. Bu case'de kesilmesinde bir sakınca görmediğimiz için true olarak geçiyoruz.
Şimdi SchedulerController
'umuza yeni endpoint yazalım.
@RestController
@RequestMapping("/api/v1/scheduler")
public class SchedulerController {
private final SchedulerService schedulerService;
public SchedulerController(SchedulerService schedulerService) {
this.schedulerService = schedulerService;
}
@PostMapping
void addTask(@RequestBody TaskDefinition taskDefinition) {
schedulerService.addTask(taskDefinition);
}
@DeleteMapping("/{id}")
void cancelTask(@PathVariable String id){
schedulerService.cancelTask(id);
}
Kodumuzu test edelim.
curl --location 'localhost:8080/api/v1/scheduler' \
--header 'Content-Type: application/json' \
--data '{
"name":"abidino",
"cronExpression" : "*/5 * * * * *"
}'
Yukarıdaki curl isteğini attığımızda oluşturduğumuz task 5 saniyede bir console'a aşağıdaki gibi bir çıktı yazacaktır. Burada ekrana zaman ve task ID yazılmaktadır.
07:32:40 ==> 25e19736-3eac-4eae-b6fa-4ab23c4a539f running
07:32:45 ==> 25e19736-3eac-4eae-b6fa-4ab23c4a539f running
07:32:50 ==> 25e19736-3eac-4eae-b6fa-4ab23c4a539f running
Bu task ID'yi alıp yeni oluşturduğumuz cancelTask endpoint'imize aşağıdaki gibi istek attığımızda ise çalışan job'umuz iptal edildiği için artık console'a herhangi bir şey yazılmadığını gözlemleyebiliriz.
curl --location --request DELETE 'localhost:8080/api/v1/scheduler/25e19736-3eac-4eae-b6fa-4ab23c4a539f'
2. Çalışan Task'ın Güncellenmesi
Task'ın oluşturulması, iptal edilmesi konusunu işledik. Şimdi ise güncellenmesini nasıl yapabiliriz diye düşünelim. Bunun için aslında yapmamız gereken şey, eski task'ı silip, gelen isteğe göre yeni bir task oluşturmak.
SchedulerService
sınıfımıza updateTask
methodu ekliyoruz.
public void updateTask(String taskId, TaskDefinition taskDefinition) {
ScheduledFuture<?> scheduledFuture = tasksMap.get(taskId);
if (Objects.nonNull(scheduledFuture)) {
scheduledFuture.cancel(true);
addTask(taskDefinition);
return;
}
throw new IllegalArgumentException("Task not found with this id : " + taskId);
}
SchedulerController
sınıfımıza updateTask methodu ekliyoruz.
@PutMapping("/{id}")
void updateTask(@PathVariable String id, @RequestBody TaskDefinition taskDefinition){
schedulerService.updateTask(id, taskDefinition);
}
Test etmek amacıyla bir adet task ekleyip daha sonra ise bunu updateTask endpoint'imizi çağırabiliriz. Önce mevcutta var olan task'ı iptal edip sonra ise verilen değerlere göre yeni task oluşturacaktır.
Github reposu icin buraya 👀 bakabilirsiniz.
Sonuç
Yazıyı okuduğunuz için teşekkür ederim, umarım faydalı olmuştur. Bu yazıda Spring Boot'da dinamik scheduler yapısını biraz daha geliştirdik serinin devam yazılarında görüşmek ūzere.
Herkese iyi çalışmalar. ✌🏼