0 added
0 removed
Original
2026-01-01
Modified
2026-03-10
1
<p>Представим достаточно популярную задачу - утилизацию вычислительных ресурсов в дни с минимальной нагрузкой. Допустим, у нас есть какой-то сайт, с которым обычно работают по будням. И есть тяжёлые фоновые задачи, которые хотим запускать в выходные, чтобы не пересекаться с клиентами.</p>
1
<p>Представим достаточно популярную задачу - утилизацию вычислительных ресурсов в дни с минимальной нагрузкой. Допустим, у нас есть какой-то сайт, с которым обычно работают по будням. И есть тяжёлые фоновые задачи, которые хотим запускать в выходные, чтобы не пересекаться с клиентами.</p>
2
<p>Но тут возникает небольшая проблема - выходные в РФ совсем не ограничиваются субботой и воскресеньем. Да и суббота и воскресенье не всегда могут быть выходными.</p>
2
<p>Но тут возникает небольшая проблема - выходные в РФ совсем не ограничиваются субботой и воскресеньем. Да и суббота и воскресенье не всегда могут быть выходными.</p>
3
<p>Попробуем решить эту задачу с помощью<a>Quartz</a>- одной из самых навороченных библиотек для запуска задач по расписанию и, конечно, Spring.</p>
3
<p>Попробуем решить эту задачу с помощью<a>Quartz</a>- одной из самых навороченных библиотек для запуска задач по расписанию и, конечно, Spring.</p>
4
<p>Естественно, мы напишем приложение на<strong>Spring Boot</strong>. Создадим его с помощью Spring Initializr.</p>
4
<p>Естественно, мы напишем приложение на<strong>Spring Boot</strong>. Создадим его с помощью Spring Initializr.</p>
5
<p><strong>pom.xml</strong>:</p>
5
<p><strong>pom.xml</strong>:</p>
6
<?xml version="1.0" encoding="UTF-8"?> <project> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.1.RELEASE</version> <relativePath/> </parent> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> </dependencies> <!-- и ещё немного --> </project><p>Надо найти одновременно простое, полное и поддерживаемое API для получения списка выходных дней. Воспользуемся не самым простым для интеграции (это XML)<a>http://xmlcalendar.ru/</a>, но и не самым сложным с точки зрения полноты данных.</p>
6
<?xml version="1.0" encoding="UTF-8"?> <project> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.1.RELEASE</version> <relativePath/> </parent> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> </dependencies> <!-- и ещё немного --> </project><p>Надо найти одновременно простое, полное и поддерживаемое API для получения списка выходных дней. Воспользуемся не самым простым для интеграции (это XML)<a>http://xmlcalendar.ru/</a>, но и не самым сложным с точки зрения полноты данных.</p>
7
<p>Данное API возвращает XML следующего вида:</p>
7
<p>Данное API возвращает XML следующего вида:</p>
8
<p><strong>src/test/resources/calendar.xml</strong>:</p>
8
<p><strong>src/test/resources/calendar.xml</strong>:</p>
9
<?xml version="1.0" encoding="UTF-8"?> <calendar year="2018" lang="ru" date="2017.10.22"> <holidays> <!-- ещё немножко XML --> <holiday id="6" title="День Победы" /> </holidays> <days> <!-- ещё немножко XML --> <day d="05.09" t="1" h="6" /> <!-- t="1" - выходной, t="2" - сокращённый, t="3" - рабочий, суббота и воскрсенье - выходные по умолчанию. да, формат даты ММ.ДД --> <day d="06.09" t="2" /> </days> </calendar><p>Немножко разобравшись с форматом ответа, получается следующий алгоритм определения выходного дня:</p>
9
<?xml version="1.0" encoding="UTF-8"?> <calendar year="2018" lang="ru" date="2017.10.22"> <holidays> <!-- ещё немножко XML --> <holiday id="6" title="День Победы" /> </holidays> <days> <!-- ещё немножко XML --> <day d="05.09" t="1" h="6" /> <!-- t="1" - выходной, t="2" - сокращённый, t="3" - рабочий, суббота и воскрсенье - выходные по умолчанию. да, формат даты ММ.ДД --> <day d="06.09" t="2" /> </days> </calendar><p>Немножко разобравшись с форматом ответа, получается следующий алгоритм определения выходного дня:</p>
10
День выходной == если это понедельник… пятница и он присутствует в праздничном календаре с типом t="1", а если это суббота или воскресенье, то день должен отсутствовать в праздничном календаре с типами t="2" или t="3".<p>Напишем с помощью Jackson маппер данного XML. Итак, для начала настроим Jackson:</p>
10
День выходной == если это понедельник… пятница и он присутствует в праздничном календаре с типом t="1", а если это суббота или воскресенье, то день должен отсутствовать в праздничном календаре с типами t="2" или t="3".<p>Напишем с помощью Jackson маппер данного XML. Итак, для начала настроим Jackson:</p>
11
<p><strong>pom.xml</strong>:</p>
11
<p><strong>pom.xml</strong>:</p>
12
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency><p><strong>src/main/java/ru/otus/springquartzexample/config/ApplicationConfig.java</strong>:</p>
12
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency><p><strong>src/main/java/ru/otus/springquartzexample/config/ApplicationConfig.java</strong>:</p>
13
@Configuration public class ApplicationConfig { @Bean XmlMapper xmlMapper() { return new XmlMapper(); } }<p>Напишем классы для элементов:</p>
13
@Configuration public class ApplicationConfig { @Bean XmlMapper xmlMapper() { return new XmlMapper(); } }<p>Напишем классы для элементов:</p>
14
<p><strong>src/main/java/ru/otus/springquartzexample/xmlcalendar/CalendarElement.java</strong>:</p>
14
<p><strong>src/main/java/ru/otus/springquartzexample/xmlcalendar/CalendarElement.java</strong>:</p>
15
@Data @JacksonXmlRootElement(localName = "calendar") @JsonIgnoreProperties(ignoreUnknown = true) public class CalendarElement { @JacksonXmlElementWrapper(localName = "days") private List<DayElement> days; }<p><strong>src/main/ru/otus/springquartzexample/xmlcalendar/DayElement.java</strong>:</p>
15
@Data @JacksonXmlRootElement(localName = "calendar") @JsonIgnoreProperties(ignoreUnknown = true) public class CalendarElement { @JacksonXmlElementWrapper(localName = "days") private List<DayElement> days; }<p><strong>src/main/ru/otus/springquartzexample/xmlcalendar/DayElement.java</strong>:</p>
16
@Data @JacksonXmlRootElement(localName = "day") @JsonIgnoreProperties(ignoreUnknown = true) public class DayElement { @JacksonXmlProperty(isAttribute = true, localName = "d") private String date; @JacksonXmlProperty(isAttribute = true, localName = "t") private int type; }<p>Да, не удивляйтесь, что здесь присутствуют аннотации для Json. Всё-таки, это Jackson.</p>
16
@Data @JacksonXmlRootElement(localName = "day") @JsonIgnoreProperties(ignoreUnknown = true) public class DayElement { @JacksonXmlProperty(isAttribute = true, localName = "d") private String date; @JacksonXmlProperty(isAttribute = true, localName = "t") private int type; }<p>Да, не удивляйтесь, что здесь присутствуют аннотации для Json. Всё-таки, это Jackson.</p>
17
<p>Чтобы убедиться, что мы всё сделали правильно, напишем небольшой тест:</p>
17
<p>Чтобы убедиться, что мы всё сделали правильно, напишем небольшой тест:</p>
18
<p><strong>src/main/java/ru/otus/springquartzexample/xmlcalendar/CalendarElementTest.java</strong>:</p>
18
<p><strong>src/main/java/ru/otus/springquartzexample/xmlcalendar/CalendarElementTest.java</strong>:</p>
19
@DisplayName("Класс CalendarElement") @SpringBootTest class CalendarElementTest { @Autowired private XmlMapper xmlMapper; @DisplayName("должен десериализовываться из XML") @Test void shouldDeserializeFromXml() throws Exception { @Cleanup val resource = getClass().getResourceAsStream("/calendar.xml"); val calendar = xmlMapper.readValue(resource, CalendarElement.class); assertThat(calendar.getDays().get(0).getDate()).isEqualTo("01.01"); } }<p>Ну и напишем клиента, который получает соответствующий календарь:</p>
19
@DisplayName("Класс CalendarElement") @SpringBootTest class CalendarElementTest { @Autowired private XmlMapper xmlMapper; @DisplayName("должен десериализовываться из XML") @Test void shouldDeserializeFromXml() throws Exception { @Cleanup val resource = getClass().getResourceAsStream("/calendar.xml"); val calendar = xmlMapper.readValue(resource, CalendarElement.class); assertThat(calendar.getDays().get(0).getDate()).isEqualTo("01.01"); } }<p>Ну и напишем клиента, который получает соответствующий календарь:</p>
20
@RequiredArgsConstructor @Service public class XmlCalendarClient { private final XmlMapper xmlMapper; public CalendarElement read(int year) throws Exception { val url = new URL("http://xmlcalendar.ru/data/ru/" + year + "/calendar.xml"); return xmlMapper.readValue(url, CalendarElement.class); } }<p><strong>И куда без теста</strong>:</p>
20
@RequiredArgsConstructor @Service public class XmlCalendarClient { private final XmlMapper xmlMapper; public CalendarElement read(int year) throws Exception { val url = new URL("http://xmlcalendar.ru/data/ru/" + year + "/calendar.xml"); return xmlMapper.readValue(url, CalendarElement.class); } }<p><strong>И куда без теста</strong>:</p>
21
@DisplayName("Класс XmlCalendarClient") @SpringBootTest class XmlCalendarClientTest { @Autowired private XmlCalendarClient client; @DisplayName("должен возвращать данные за 2019 год") @Test void shouldReturn2019() throws Exception { val calendar = client.read(2019); assertThat(calendar).isNotNull(); } }<p>Теперь пришло время написать нашу<strong>бизнес-логику</strong>. Для приличия создадим интерфейс сервиса:</p>
21
@DisplayName("Класс XmlCalendarClient") @SpringBootTest class XmlCalendarClientTest { @Autowired private XmlCalendarClient client; @DisplayName("должен возвращать данные за 2019 год") @Test void shouldReturn2019() throws Exception { val calendar = client.read(2019); assertThat(calendar).isNotNull(); } }<p>Теперь пришло время написать нашу<strong>бизнес-логику</strong>. Для приличия создадим интерфейс сервиса:</p>
22
<p><strong>src/main/java/ru/otus/springquartzexample/service/RussianHolidaysService.java</strong>:</p>
22
<p><strong>src/main/java/ru/otus/springquartzexample/service/RussianHolidaysService.java</strong>:</p>
23
public interface RussianHolidaysService { boolean isHoliday(LocalDate date); }<p>Но неприлично реализуем его прямо в клиенте:</p>
23
public interface RussianHolidaysService { boolean isHoliday(LocalDate date); }<p>Но неприлично реализуем его прямо в клиенте:</p>
24
@Override public boolean isHoliday(LocalDate date) throws Exception { val year = date.getYear(); val calendar = read(year); val dayToFind = date.format(DateTimeFormatter.ofPattern("MM.dd")); val isSaturdayOrSunday = date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY; val fromCalendar = calendar.getDays().stream() .filter(day -> dayToFind.equals(day.getDate())) .findAny(); return isSaturdayOrSunday ? fromCalendar.isEmpty() : fromCalendar.isPresent() && fromCalendar.map(DayElement::isHoliday).get(); }<p><strong>Ну и куда без тестов</strong>:</p>
24
@Override public boolean isHoliday(LocalDate date) throws Exception { val year = date.getYear(); val calendar = read(year); val dayToFind = date.format(DateTimeFormatter.ofPattern("MM.dd")); val isSaturdayOrSunday = date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY; val fromCalendar = calendar.getDays().stream() .filter(day -> dayToFind.equals(day.getDate())) .findAny(); return isSaturdayOrSunday ? fromCalendar.isEmpty() : fromCalendar.isPresent() && fromCalendar.map(DayElement::isHoliday).get(); }<p><strong>Ну и куда без тестов</strong>:</p>
25
@DisplayName("должен сказать, что 8-ое марта 2019 - выходной") @Test void shouldSayThat20190308IsHoliday() throws Exception { assertTrue(client.isHoliday(LocalDate.of(2019, 3, 8))); } @DisplayName("должен сказать, что 9-ое июня 2018 был рабочий") @Test void shouldSayThat20180609IsHoliday() throws Exception { assertFalse(client.isHoliday(LocalDate.of(2018, 6, 9))); }<p>Здесь мы не будем рассматривать особенности кэширования вызовов методов, хотя со Spring можно сделать кэширование очень просто.</p>
25
@DisplayName("должен сказать, что 8-ое марта 2019 - выходной") @Test void shouldSayThat20190308IsHoliday() throws Exception { assertTrue(client.isHoliday(LocalDate.of(2019, 3, 8))); } @DisplayName("должен сказать, что 9-ое июня 2018 был рабочий") @Test void shouldSayThat20180609IsHoliday() throws Exception { assertFalse(client.isHoliday(LocalDate.of(2018, 6, 9))); }<p>Здесь мы не будем рассматривать особенности кэширования вызовов методов, хотя со Spring можно сделать кэширование очень просто.</p>
26
<h2>Пришло время разобраться c Quartz</h2>
26
<h2>Пришло время разобраться c Quartz</h2>
27
<p>Начиная со Spring Boot 2.0, для него существует отдельный стартер:</p>
27
<p>Начиная со Spring Boot 2.0, для него существует отдельный стартер:</p>
28
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency><p>Главная сущность, которую мы хотели:</p>
28
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency><p>Главная сущность, которую мы хотели:</p>
29
<p><strong>src/main/java/ru/otus/springquartzexample/quartz/VeryHardJob.java</strong>:</p>
29
<p><strong>src/main/java/ru/otus/springquartzexample/quartz/VeryHardJob.java</strong>:</p>
30
@Slf4j public class VeryHardJob implements Job { @Override public void execute(JobExecutionContext context) { log.info("Very very very hard job..."); } }<p><strong>Quartz</strong>- очень сложный фреймворк. Он хранит выполненные работы в БД и для того, чтобы его настроить, необходимо создать множество бинов, один из которых главный -<strong>Quartz Sheduler</strong>.</p>
30
@Slf4j public class VeryHardJob implements Job { @Override public void execute(JobExecutionContext context) { log.info("Very very very hard job..."); } }<p><strong>Quartz</strong>- очень сложный фреймворк. Он хранит выполненные работы в БД и для того, чтобы его настроить, необходимо создать множество бинов, один из которых главный -<strong>Quartz Sheduler</strong>.</p>
31
<p>Spring Boot спасает нас от подобной необходимости, имеет настроенные бины, включая хранение в памяти данных работ.</p>
31
<p>Spring Boot спасает нас от подобной необходимости, имеет настроенные бины, включая хранение в памяти данных работ.</p>
32
<p>Поэтому мы просто настроим только несколько бинов, а благодаря автоконфигурации они будут использованы.</p>
32
<p>Поэтому мы просто настроим только несколько бинов, а благодаря автоконфигурации они будут использованы.</p>
33
<p><strong>src/main/java/ru/otus/springquartzexample/quartz/RussianHolidaysQuartzCalendar.java</strong>:</p>
33
<p><strong>src/main/java/ru/otus/springquartzexample/quartz/RussianHolidaysQuartzCalendar.java</strong>:</p>
34
@RequiredArgsConstructor @Component public class RussianHolidaysQuartzCalendar implements Calendar { private final RussianHolidaysService russianHolidaysService; @Override public boolean isTimeIncluded(long timestamp) { try { val date = LocalDateTime.ofEpochSecond(timestamp, 0, ZoneOffset.UTC).toLocalDate(); return russianHolidaysService.isHoliday(date); } catch (Exception ex) { return false; } } @Override public long getNextIncludedTime(long timestamp) { LocalDate date = LocalDateTime.ofEpochSecond(timestamp, 0, ZoneOffset.UTC).toLocalDate(); try { while (!russianHolidaysService.isHoliday(date)) { date = date.plusDays(1); } return 0; } catch (Exception ex) { return date.plusDays(1).atStartOfDay().toEpochSecond(ZoneOffset.UTC); } } }<p>Ну и сами бины:</p>
34
@RequiredArgsConstructor @Component public class RussianHolidaysQuartzCalendar implements Calendar { private final RussianHolidaysService russianHolidaysService; @Override public boolean isTimeIncluded(long timestamp) { try { val date = LocalDateTime.ofEpochSecond(timestamp, 0, ZoneOffset.UTC).toLocalDate(); return russianHolidaysService.isHoliday(date); } catch (Exception ex) { return false; } } @Override public long getNextIncludedTime(long timestamp) { LocalDate date = LocalDateTime.ofEpochSecond(timestamp, 0, ZoneOffset.UTC).toLocalDate(); try { while (!russianHolidaysService.isHoliday(date)) { date = date.plusDays(1); } return 0; } catch (Exception ex) { return date.plusDays(1).atStartOfDay().toEpochSecond(ZoneOffset.UTC); } } }<p>Ну и сами бины:</p>
35
@Bean JobDetail veryHardJob() { return JobBuilder.newJob(VeryHardJob.class) .withIdentity("veryHardJob") .storeDurably() .build(); } @Bean Trigger jobTrigger(){ return TriggerBuilder.newTrigger().forJob(veryHardJob()) .withIdentity("veryHardJobTrigger") .startNow() .build(); }<p>Запустив это в будний день, мы увидим:</p>
35
@Bean JobDetail veryHardJob() { return JobBuilder.newJob(VeryHardJob.class) .withIdentity("veryHardJob") .storeDurably() .build(); } @Bean Trigger jobTrigger(){ return TriggerBuilder.newTrigger().forJob(veryHardJob()) .withIdentity("veryHardJobTrigger") .startNow() .build(); }<p>Запустив это в будний день, мы увидим:</p>
36
2019-11-15 09:57:01.315 INFO 18580 --- [eduler_Worker-1] r.o.s.quartz.VeryHardJob : Very very very hard job...<p>Подробности реализации этой задачи вы можете посмотреть<a>здесь</a>.</p>
36
2019-11-15 09:57:01.315 INFO 18580 --- [eduler_Worker-1] r.o.s.quartz.VeryHardJob : Very very very hard job...<p>Подробности реализации этой задачи вы можете посмотреть<a>здесь</a>.</p>
37
37