Implement sending alerts at specific times per user with QuartzScheduler

Seonggil Jeong
5 min readNov 27, 2022

--

java : 11
springBoot : 2.7.5

Part 1 : Learn about the Quartz Scheduler
Part 2 :
Implementing Scheduler with Quartz Scheduler with SpringBoot
part 3 :
Implement sending alerts at specific times per user with QuartzScheduler

In the last part, Part 3, I will implement sending notifications at a specific time for each user, which was the final goal

The problem started with sending notifications at any time per user.
Think about it, I will say there’s a user who wants to be notified at 2am and a user who wants to be notified at 3:30am

If you use the traditional scheduler, you need to find a user who needs to send a notification every minute (If not, please comment)
However, if there are many users, it cannot be handled this way

  • What if the work doesn’t finish in a minute?
  • What if the work fails?

Faced with these problems, I came to use the Quartz Scheduler
In the example, use Rest to create jobs, But can also implement them through CronJob. if you need

Overview

in this Part, i will create two Endpoint

  • Create Job
    (POST) /jobs/{groupName}/{jobName}/{minutes}
  • Select Activated Job
    (GET) /jobs

read the example, you can change it enough for your project
Refer to Part1 and Part2 for basic configuration and operation principles

Create Request

First, I will implement a request that creates a Job
All JobRequests extend and implement BaseJobRequest
A job with the same name cannot exist in the same group
-> if make it, An error occurs, but I implemented it to be overwritten

@Getter
@Setter
public abstract class BaseJobRequest {
@NotNull(message = "name cannot be Null")
private String name;
@NotNull(message = "group cannot be Null")
private String group;
private String description;


@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BaseJobRequest that = (BaseJobRequest) o;
return name.equals(that.name) && group.equals(that.group);
}

@Override
public int hashCode() {
return Objects.hash(name, group);
}

public void setName(String name) {
this.name = name;
}

public void setGroup(String group) {
this.group = group;
}

public void setDescription(String description) {
this.description = description;
}
}

Now, implement SampleJobRequest,
which includes the start time, fromUserId And content
flow : content is delivered to userId at startTime

@Getter
public class SampleJobRequest extends BaseJobRequest {
private final UUID userId; // userId
private final LocalTime startTime; // Desired start time
private final String content;

private SampleJobRequest(Builder builder) {
super.setName(builder.name);
super.setGroup(builder.group);
super.setDescription(builder.description);
userId = builder.userId;
startTime = builder.startTime;
content = builder.content;
}

public static Builder builder() {
return new Builder();
}

public static final class Builder {
private @NotNull(message = "name cannot be Null") String name;
private @NotNull(message = "group cannot be Null") String group;
private String description;
private UUID userId;
private LocalTime startTime;
private String content;

private Builder() {
}

public static Builder builder() {
return new Builder();
}

public Builder name(@NotNull(message = "name cannot be Null") String val) {
name = val;
return this;
}

public Builder group(@NotNull(message = "group cannot be Null") String val) {
group = val;
return this;
}

public Builder description(String val) {
description = val;
return this;
}

public Builder userId(UUID val) {
userId = val;
return this;
}

public Builder startTime(LocalTime val) {
startTime = val;
return this;
}

public Builder content(String val) {
content = val;
return this;
}

public SampleJobRequest build() {
return new SampleJobRequest(this);
}
}
}

implement the action that runs when the trigger runs
See Part2 for DI(Dependency Injection) And use Component
(need AutoWiringSpringBeanJobFactory)

@Slf4j
@Component
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class SampleJob implements Job {


@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();

log.info("userId : " + jobDataMap.get("userId"));
log.info("content : " + jobDataMap.get("content"));

log.info("[send Message] " + jobDataMap.get("content") + " to " + jobDataMap.get("userId"));

}
}

implement QuartzHandler to help you create jobs, create triggers, and check job listings
Initialize the scheduler at startup

@Slf4j
@Configuration
@RequiredArgsConstructor
public class QuartzHandler {
private final Scheduler scheduler;

@PostConstruct
public void init() {
try {
// Initialization (DB)
scheduler.clear();
// add Listener
scheduler.getListenerManager()
.addJobListener(new JobsListener());
scheduler.getListenerManager()
.addTriggerListener(new TriggersListener());

} catch (Exception e) {
e.printStackTrace();
}
}

public <T extends Job> void addJob(Class<? extends Job> job, SampleJobRequest request, Map params)
throws SchedulerException {

final JobDetail jobDetail = buildJobDetail(job, request.getName(), request.getGroup(), request.getDescription(), params);
final Trigger trigger = buildTrigger(request.getName(), request.getGroup(), request.getStartTime());

registerJobInScheduler(jobDetail, trigger);
}

public List<JobResponse> findAllActivatedJob() {
List<JobResponse> result = new ArrayList<>();
try {
for (String groupName : scheduler.getJobGroupNames()) {
log.info("groupName : " + groupName);

for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName))) {
List<Trigger> trigger = (List<Trigger>) scheduler.getTriggersOfJob(jobKey);

result.add(JobResponse.builder()
.jobName(jobKey.getName())
.groupName(jobKey.getGroup())
.scheduleTime(trigger.get(0).getStartTime().toString()).build());
}
}
} catch (SchedulerException e) {
e.printStackTrace();
}

return result;
}

private void registerJobInScheduler(final JobDetail jobDetail, final Trigger trigger) throws SchedulerException {
if (scheduler.checkExists(jobDetail.getKey())) {
scheduler.deleteJob(jobDetail.getKey());
scheduler.scheduleJob(jobDetail, trigger);
} else {
scheduler.scheduleJob(jobDetail, trigger);
}

}


public <T extends Job> JobDetail buildJobDetail(
Class<? extends Job> job, final String jobName, final String group,
String jobDescription, Map<String, Object> params) {

JobDataMap jobDataMap = new JobDataMap();

if (params != null) {
jobDataMap.putAll(params);
}

return JobBuilder.newJob(job)
.withIdentity(jobName, group)
.withDescription(jobDescription)
.usingJobData(jobDataMap)
.build();
}

private Trigger buildTrigger(final String name, final String group, final LocalTime startTime) {
SimpleTriggerFactoryBean triggerFactory = new SimpleTriggerFactoryBean();

triggerFactory.setName(name);
triggerFactory.setGroup(group);
triggerFactory.setStartTime(localTimeToDate(startTime));
triggerFactory.setRepeatCount(0);
triggerFactory.setRepeatInterval(0);

triggerFactory.afterPropertiesSet();
return triggerFactory.getObject();

}

private Date localTimeToDate(final LocalTime startTime) {
Instant instant = startTime.atDate(LocalDate
.of(LocalDate.now().getYear(), LocalDate.now().getMonth(), LocalDate.now().getDayOfMonth()))
.atZone(ZoneId.systemDefault()).toInstant();

return Date.from(instant);
}
}

Now, I’m going to implement the two endpoints that I mentioned earlier
As mentioned earlier, if a job with the same name is created in the same group, it will be overwritten (Good for changing jobs)

@Slf4j
@RestController
@RequiredArgsConstructor
public class QuartzController {
private final QuartzHandler quartzHandler;


@PostMapping("/jobs/{group}/{name}/{minutes}")
public ResponseEntity<Void> createJob(@PathVariable final String group, @PathVariable final String name,
@PathVariable final Integer minutes) throws Exception {

quartzHandler.addJob(SampleJob.class, SampleJobRequest.builder()
.group(group)
.name(name)
.userId(UUID.randomUUID())
.content("[Send Message] at " + LocalDateTime.now())
.startTime(LocalTime.now().plusMinutes(minutes)).build());

return ResponseEntity.status(HttpStatus.CREATED).build();

}

@GetMapping("/jobs")
public ResponseEntity<List<JobResponse>>findAllJob() throws Exception {
return ResponseEntity.ok().body(quartzHandler.findAllActivatedJob());
}
}
@Builder
@Getter
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
public class JobResponse {
private final String jobName;
private final String groupName;
private final String scheduleTime;
}

you can see code in my gitHub

--

--