Способ вызова по дате/времени

Я ищу современный способ выполнить данный метод в заданную дату/время (в частности, ZonedDateTime).

Я знаю о классе Timer и библиотеке Quartz, как показано здесь (обсуждения включают полные решения):

Но эти потоки довольно старые и с тех пор не используют новые функции Java и элементы библиотеки. В частности, было бы очень удобно заполучить любой объект Future, так как они предоставляют простой механизм для их отмены.

Поэтому, пожалуйста, не предлагайте решения, связанные с Timer или Quartz. Кроме того, я хотел бы иметь ванильное решение, не использующее какие-либо внешние библиотеки. Но не стесняйтесь также предлагать их ради вопросов и ответов.


person Zabuzard    schedule 06.03.2019    source источник


Ответы (1)


ScheduledExecutorService

Вы можете использовать ScheduledExecutorService (documentation), который доступен начиная с Java 5. Он даст ScheduledFuture (документация), которую можно использовать для отслеживания выполнения, а также для его отмены.

В частности, метод:

ScheduledFuture<?> schedule​(Runnable command, long delay, TimeUnit unit)

который

Отправляет одноразовую задачу, которая становится доступной после заданной задержки.

Но вы также можете изучить другие методы, в зависимости от фактического варианта использования (scheduleAtFixedRate и версии, принимающие Callable вместо Runnable).

Начиная с Java 8 (потоки, лямбда-выражения, ...) этот класс становится еще более удобным благодаря наличию простых методов преобразования между старым TimeUnit и более новым ChronoUnit (для вашего ZonedDateTime), а также возможности предоставления Runnable command как лямбда или ссылка на метод (потому что это FunctionalInterface).


Пример

Давайте посмотрим на пример, выполняющий то, что вы просите:

// Somewhere before the method, as field for example
// Use other pool sizes if desired
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

public static ScheduledFuture<?> scheduleFor(Runnable runnable, ZonedDateTime when) {
    Instant now = Instant.now();
    // Use a different resolution if desired
    long secondsUntil = ChronoUnit.SECONDS.between(now, when.toInstant());

    return scheduler.schedule(runnable, secondsUntil, TimeUnit.of(ChronoUnit.SECONDS));
}

Вызов прост:

ZonedDateTime when = ...
ScheduledFuture<?> job = scheduleFor(YourClass::yourMethod, when);

Затем вы можете использовать job для наблюдения за выполнением, а также отменить его, если хотите. Пример:

if (!job.isCancelled()) {
    job.cancel(false);
}

Примечания

Вы можете заменить параметр ZonedDateTime в методе на Temporal, тогда он также принимает другие форматы даты/времени.

Не забудьте выключить ScheduledExecutorService, когда закончите. В противном случае у вас будет запущен поток, даже если ваша основная программа уже завершена.

scheduler.shutdown();

Обратите внимание, что мы используем Instant вместо ZonedDateTime, поскольку информация о зоне не имеет для нас значения, если разница во времени вычисляется правильно. Instant всегда представляет время в формате UTC, без каких-либо странных явлений, таких как DST. (Хотя это не имеет особого значения для этого приложения, оно просто чище).

person Zabuzard    schedule 06.03.2019
comment
Хороший ответ. Для ясности я предлагаю, чтобы внутри вашего метода scheduleFor вы преобразовали его в использование Instant, а не ZonedDateTime, вызвав toInstant. Хотя в этом контексте это работает, показ ZonedDateTime.now() без явного часового пояса подает плохой пример, чтобы показать людям, что они изучают обработку даты и времени. Незнание часового пояса является распространенным источником проблем при программировании. Instant всегда указывается в формате UTC, поэтому его использование устраняет эту двусмысленность. - person Basil Bourque; 06.03.2019
comment
@BasilBourque Спасибо за комментарий, но у меня есть вопрос. Я думал, что ChronoUnit#between знает о часовом поясе, установленном в ZonedDateTime, и, таким образом, будет правильно сравнивать его, независимо от того, какой пояс установлен в now() (это зона системы по умолчанию). т.е. это не имело бы значения, результат between всегда был бы правильным. Так ли это, или я ошибаюсь здесь? - person Zabuzard; 06.03.2019
comment
Я считаю, что ваш код верен правильно и дает правильные результаты. Как я уже упоминал в своем комментарии, в контексте вашего кода ваше использование ZonedDateTime будет успешно работать. Я беспокоюсь о том, что другие люди изучают обработку даты и времени, видят ZonedDateTime.now(), а затем продолжают вызывать этот метод в своей собственной работе, sans необязательный аргумент зоны, не понимая последствий текущего времени JVM по умолчанию. zone неявно применяется во время выполнения. Кроме того, использование Instant в вашем методе дает понять, что мы считаем секунды без какой-либо конкретной связи с проблемами часового пояса. - person Basil Bourque; 06.03.2019