Фреймворк Quartz: решаем вопрос запуска задач | OTUS

Фреймворк Quartz: решаем вопрос запуска задач

Spring_Deep_10.2-5020-071634.png

Представим достаточно популярную задачу – утилизацию вычислительных ресурсов в дни с минимальной нагрузкой. Допустим, у нас есть какой-то сайт, с которым обычно работают по будням. И есть тяжёлые фоновые задачи, которые хотим запускать в выходные, чтобы не пересекаться с клиентами.

Но тут возникает небольшая проблема – выходные в РФ совсем не ограничиваются субботой и воскресеньем. Да и суббота и воскресенье не всегда могут быть выходными.

Попробуем решить эту задачу с помощью Quartz – одной из самых навороченных библиотек для запуска задач по расписанию и, конечно, Spring.

Естественно, мы напишем приложение на Spring Boot. Создадим его с помощью Spring Initializr.

pom.xml:

<?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>

Надо найти одновременно простое, полное и поддерживаемое API для получения списка выходных дней. Воспользуемся не самым простым для интеграции (это XML) http://xmlcalendar.ru/, но и не самым сложным с точки зрения полноты данных.

Данное API возвращает XML следующего вида:

src/test/resources/calendar.xml:

<?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>

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

День выходной == если это понедельник… пятница и он присутствует в праздничном календаре с типом t="1", а если это суббота или воскресенье, то день должен отсутствовать в праздничном календаре с типами t="2" или t="3".

Напишем с помощью Jackson маппер данного XML. Итак, для начала настроим Jackson:

pom.xml:

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

src/main/java/ru/otus/springquartzexample/config/ApplicationConfig.java:

@Configuration
public class ApplicationConfig {

    @Bean
    XmlMapper xmlMapper() {
        return new XmlMapper();
    }
}

Напишем классы для элементов:

src/main/java/ru/otus/springquartzexample/xmlcalendar/CalendarElement.java:

@Data
@JacksonXmlRootElement(localName = "calendar")
@JsonIgnoreProperties(ignoreUnknown = true)
public class CalendarElement {

    @JacksonXmlElementWrapper(localName = "days")
    private List<DayElement> days;
}

src/main/ru/otus/springquartzexample/xmlcalendar/DayElement.java:

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

Да, не удивляйтесь, что здесь присутствуют аннотации для Json. Всё-таки, это Jackson.

Чтобы убедиться, что мы всё сделали правильно, напишем небольшой тест:

src/main/java/ru/otus/springquartzexample/xmlcalendar/CalendarElementTest.java:

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

Ну и напишем клиента, который получает соответствующий календарь:

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

И куда без теста:

@DisplayName("Класс XmlCalendarClient")
@SpringBootTest
class XmlCalendarClientTest {

    @Autowired
    private XmlCalendarClient client;

    @DisplayName("должен возвращать данные за 2019 год")
    @Test
    void shouldReturn2019() throws Exception {
        val calendar = client.read(2019);
        assertThat(calendar).isNotNull();
    }
}

Теперь пришло время написать нашу бизнес-логику. Для приличия создадим интерфейс сервиса:

src/main/java/ru/otus/springquartzexample/service/RussianHolidaysService.java:

public interface RussianHolidaysService {

    boolean isHoliday(LocalDate date);
}

Но неприлично реализуем его прямо в клиенте:

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

Ну и куда без тестов:

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

Здесь мы не будем рассматривать особенности кэширования вызовов методов, хотя со Spring можно сделать кэширование очень просто.

Пришло время разобраться c Quartz

Начиная со Spring Boot 2.0, для него существует отдельный стартер:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

Главная сущность, которую мы хотели:

src/main/java/ru/otus/springquartzexample/quartz/VeryHardJob.java:

@Slf4j
public class VeryHardJob implements Job {

    @Override
    public void execute(JobExecutionContext context) {
        log.info("Very very very hard job...");
    }
}

Quartz — очень сложный фреймворк. Он хранит выполненные работы в БД и для того, чтобы его настроить, необходимо создать множество бинов, один из которых главный – Quartz Sheduler.

Spring Boot спасает нас от подобной необходимости, имеет настроенные бины, включая хранение в памяти данных работ.

Поэтому мы просто настроим только несколько бинов, а благодаря автоконфигурации они будут использованы.

src/main/java/ru/otus/springquartzexample/quartz/RussianHolidaysQuartzCalendar.java:

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

Ну и сами бины:

@Bean
JobDetail veryHardJob() {
    return JobBuilder.newJob(VeryHardJob.class)
            .withIdentity("veryHardJob")
            .storeDurably()
            .build();
}

@Bean
Trigger jobTrigger(){
    return TriggerBuilder.newTrigger().forJob(veryHardJob())
            .withIdentity("veryHardJobTrigger")
            .startNow()
            .build();
}

Запустив это в будний день, мы увидим:

2019-11-15 09:57:01.315  INFO 18580 --- [eduler_Worker-1] r.o.s.quartz.VeryHardJob                 : Very very very hard job...

Подробности реализации этой задачи вы можете посмотреть здесь.

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться
Популярное
Сегодня тут пусто