Java: 11
SpringBoot: 2.7.5

Часть 1: Узнайте о Quartz Scheduler
Часть 2: Реализация планировщика с Quartz Scheduler с помощью SpringBoot
часть 3:
Реализуйте отправку оповещений в определенное время в пользователь QuartzScheduler

В последней части, части 3, я реализую отправку уведомлений в определенное время для каждого пользователя, что и было конечной целью

Проблема началась с отправки уведомлений в любое время для каждого пользователя.
Подумайте об этом, я скажу, что есть пользователь, который хочет получать уведомления в 2 часа ночи, и пользователь, который хочет получать уведомления в 3:30 утра.

Если вы используете традиционный планировщик, вам нужно найти пользователя, которому нужно отправлять уведомление каждую минуту (Если нет, прокомментируйте)
Однако, если пользователей много, это не может быть обрабатывается таким образом

  • Что, если работа не закончится через минуту?
  • Что делать, если работа не удалась?

Столкнувшись с этими проблемами, я пришел к использованию Quartz Scheduler
В примере использовать Rest для создания заданий, Но также могу реализовать их через CronJob. если вам нужно

Обзор

в этой части я создам две конечные точки

  • Создать задание
    (POST) /jobs/{groupName}/{jobName}/{minutes}
  • Выбрать активированную вакансию
    (GET) /jobs

прочтите пример, вы можете изменить его достаточно для своего проекта
Обратитесь к Часть 1 и Часть 2для базовой настройки и принципов работы

Создать запрос

Во-первых, я реализую запрос, создающий задание
Все запросы заданий расширяются и реализуются BaseJobRequest
Задание с таким именем не может существовать в одной и той же группе
-›если это сделать, возникает ошибка, но я реализовал ее для перезаписи

@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;
    }
}

Теперь реализуйте SampleJobRequest,
который включает время начала, fromUserId и поток контента
: content доставляется userId в 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);
        }
    }
}

реализовать действие, которое запускается при запуске триггера
см. часть 2 для DI (внедрение зависимостей) и использовать Component
(требуется 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"));

    }
}

реализовать QuartzHandler, чтобы помочь вам создавать задания, создавать триггеры и проверять списки заданий
Инициализировать планировщик при запуске

@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);
    }
}

Теперь я собираюсь реализовать две конечные точки, о которых упоминал ранее
Как упоминалось ранее, если задание с тем же именем создается в той же группе, оно будет перезаписано (удобно для смены места работы)

@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;
}

вы можете увидеть код в моем gitHub