JavaRush /Java блог /Random UA /Додаємо Spring Scheduler - "Java-проект від А до Я"
Roman Beekeeper
35 рівень

Додаємо Spring Scheduler - "Java-проект від А до Я"

Стаття з групи Random UA
Привіт усім, мої любі друзі. У попередній статті ми підготували клієнт до роботи з JavaRush API для статей. Тепер можна писати логіку для роботи нашої джоби, яка виконуватиметься кожні 15 хвабон. Ось так, як показано на цій схемі "Java-проект від А до Я": Додаємо Spring Scheduler - 1.
  1. Знаходить у всіх групах, які є у нашій БД, нові статті, що вийшли після попереднього виконання.

    У цій схемі вказано меншу кількість груп лише з активними користувачами. На той момент мені це здалося логічним, але зараз я розумію, що незалежно від того, чи є активні користувачі, підписані на конкретну групу чи ні, все одно потрібно тримати актуальним останню статтю, що бот обробив. Може виникнути ситуація, коли новому користувачеві прийде одразу вся кількість статей, що вийшла з моменту деактивації цієї групи. А це не очікувана поведінка, і щоб її уникнути, потрібно тримати актуальними й ті групи з нашої БД, що на даний момент не мають активних користувачів.
  2. Якщо є нові статті, сформувати повідомлення для всіх користувачів, які активно підписані на цю групу. Якщо нових статей немає, то просто завершуємо роботу.

До речі, я вже згадував у своєму ТГ-каналі, бот вже працює та надсилає нові статті за підписками. Почнемо писати FindNewArtcileService . У ньому відбуватиметься вся робота з пошуку та відправлення повідомлень, а джоба лише запускатиме метод цього сервісу:

FindNewArticleService:

package com.github.codegymcommunity.jrtb.service;

/**
* Service for finding new articles.
*/
public interface FindNewArticleService {

   /**
    * Find new articles and notify subscribers about it.
    */
   void findNewArticles();
}
Дуже простий, правда? У цьому його й суть, а вся складність буде у реалізації:
package com.github.codegymcommunity.jrtb.service;

import com.github.codegymcommunity.jrtb.codegymclient.CodeGymPostClient;
import com.github.codegymcommunity.jrtb.codegymclient.dto.PostInfo;
import com.github.codegymcommunity.jrtb.repository.entity.GroupSub;
import com.github.codegymcommunity.jrtb.repository.entity.TelegramUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class FindNewArticleServiceImpl implements FindNewArticleService {

   public static final String CODEGYM_WEB_POST_FORMAT = "https://javarush.com/groups/posts/%s";

   private final GroupSubService groupSubService;
   private final CodeGymPostClient codeGymPostClient;
   private final SendBotMessageService sendMessageService;

   @Autowired
   public FindNewArticleServiceImpl(GroupSubService groupSubService,
                                    CodeGymPostClient codeGymPostClient,
                                    SendBotMessageService sendMessageService) {
       this.groupSubService = groupSubService;
       this.codeGymPostClient = codeGymPostClient;
       this.sendMessageService = sendMessageService;
   }


   @Override
   public void findNewArticles() {
       groupSubService.findAll().forEach(gSub -> {
           List<PostInfo> newPosts = codeGymPostClient.findNewPosts(gSub.getId(), gSub.getLastArticleId());

           setNewLastArticleId(gSub, newPosts);

           notifySubscribersAboutNewArticles(gSub, newPosts);
       });
   }

   private void notifySubscribersAboutNewArticles(GroupSub gSub, List<PostInfo> newPosts) {
       Collections.reverse(newPosts);
       List<String> messagesWithNewArticles = newPosts.stream()
               .map(post -> String.format("✨Вышла новая статья <b>%s</b> в группе <b>%s</b>.✨\n\n" +
                               "<b>Описание:</b> %s\n\n" +
                               "<b>Ссылка:</b> %s\n",
                       post.getTitle(), gSub.getTitle(), post.getDescription(), getPostUrl(post.getKey())))
               .collect(Collectors.toList());

       gSub.getUsers().stream()
               .filter(TelegramUser::isActive)
               .forEach(it -> sendMessageService.sendMessage(it.getChatId(), messagesWithNewArticles));
   }

   private void setNewLastArticleId(GroupSub gSub, List<PostInfo> newPosts) {
       newPosts.stream().mapToInt(PostInfo::getId).max()
               .ifPresent(id -> {
                   gSub.setLastArticleId(id);
                   groupSubService.save(gSub);
               });
   }

   private String getPostUrl(String key) {
       return String.format(CODEGYM_WEB_POST_FORMAT, key);
   }
}
Тут розберемося з усім по порядку:
  1. За допомогою groupService ми знаходимо всі групи, які є в базі даних.

  2. Потім розбігаємось по всіх групах і для кожної викликаємо створений у минулій статті клієнт - codeGymPostClient.findNewPosts .

  3. Далі за допомогою методу setNewArticleId ми оновлюємо ID шник нашої останньої нової статті, щоб наша база даних знала, що ми вже опрацювали нові.

  4. І за допомогою того, що GroupSub має колекцію користувачів, пробігаємо по активних і надсилаємо повідомлення про нові статті.

Яке там повідомлення зараз обговорювати не будемо, для нас це не дуже важливо. Головне, що метод працює. Логіка пошуку нових статей та відправлення повідомлень готова, тому можна перейти до створення джоби.

Створюємо FindNewArticleJob

Ми вже говорабо про те, що таке SpringScheduler, але ще раз повторимо швидко: це механізм у Spring фреймворку для створення фонового процесу, який буде виконуватися в певний час, що задається нами. Що потрібно для цього? Перший етап - додати інструкцію @EnableScheduling до нашого вхідного класу для спрингу:
package com.github.codegymcommunity.jrtb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
public class JavarushTelegramBotApplication {

   public static void main(String[] args) {
       SpringApplication.run(JavarushTelegramBotApplication.class, args);
   }

}
Другий етап - створити клас, додати його до ApplicationContext і створити в ньому метод, який запускатиметься періодично. Створюємо пакет job на одному рівні з repository, service тощо і там створюємо клас FindNewArticleJob :
package com.github.codegymcommunity.jrtb.job;

import com.github.codegymcommunity.jrtb.service.FindNewArticleService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.ZoneOffset;

/**
* Job for finding new articles.
*/
@Slf4j
@Component
public class FindNewArticlesJob {

   private final FindNewArticleService findNewArticleService;

   @Autowired
   public FindNewArticlesJob(FindNewArticleService findNewArticleService) {
       this.findNewArticleService = findNewArticleService;
   }

   @Scheduled(fixedRateString = "${bot.recountNewArticleFixedRate}")
   public void findNewArticles() {
       LocalDateTime start = LocalDateTime.now();

       log.info("Find new article job started.");

       findNewArticleService.findNewArticles();

       LocalDateTime end = LocalDateTime.now();

       log.info("Find new articles job finished. Took seconds: {}",
               end.toEpochSecond(ZoneOffset.UTC) - start.toEpochSecond(ZoneOffset.UTC));
   }
}
Щоб додати цей клас до Application Context , я використав інструкцію @Component . А щоб метод усередині класу знав, що йому потрібно запускатися періодично, я додав до методу інструкцію: @Scheduled(fixedRateString = "${bot.recountNewArticleFixedRate}") . А ось яке значення буде – його ми задаємо вже в application.properties файлі:
bot.recountNewArticleFixedRate = 900000
Тут значення вказано у мілісекундах. Це буде 15 хвабон. У цьому методі все просто: я для себе в логах додав супер просту метрику для підрахунку пошуку нових статей, щоб хоч приблизно уявляти, наскільки швидко працює.

Тестуємо новий функціонал

Тепер тестуватимемо на нашому тестовому боті. Але як? Не буду ж я видаляти щоразу статті, щоб показати, що сповіщення прийшли? Ні звичайно. Просто правитимемо дані в БД і запускатимемо додаток. Тестуватиму я на своєму тестовому рівні. Для цього підпишемося на якусь групу. Коли підписку буде оформлено, групі буде поставлено актуальне ID останньої статті. Ходімо в основу і змінимо значення на дві статті тому. У результаті очікуємо, що буде стільки статей, на скільки раніше поставимо останнійматеріал . "Java-проект від А до Я": Додаємо Spring Scheduler - 2Далі йдемо на сайт, сортуємо статті в групі Java-проекти — спочатку нові — і заходимо до третьої статті зі списку: "Java-проект від А до Я": Додаємо Spring Scheduler - 3Зайдемо в нижню статтю та з адресаного рядка отримаємо article Id — 3313: "Java-проект від А до Я": Додаємо Spring Scheduler - 4Далі йдемо в MySQL Workbench і змінюємо значенняlastArticleId на 3313. Подивимося, що така група є в базі: "Java-проект від А до Я": Додаємо Spring Scheduler - 5І для неї виконаємо команду: "Java-проект від А до Я": Додаємо Spring Scheduler - 6І все тепер потрібно почекати до наступного запуску джоби з пошуку нових статей. Очікуємо, що прийде два повідомлення про нову статтю із групи Java-проекти. Як кажуть, результат не забарився: "Java-проект від А до Я": Додаємо Spring Scheduler - 7Виходить, що бот відпрацював так, як ми і очікували.

Закінчення

Як завжди — оновлюємо версію в pom.xml і додаємо запис в RELEASE_NOTES, щоб історія роботи збереглася і завжди можна було повернутися і зрозуміти, що змінилося. Тому інкрементуємо одну одиницю версію:
<version>0.7.0-SNAPSHOT</version>
І оновлюємо RELEASE_NOTES:
## 0.7.0-SNAPSHOT * JRTB-4: розширена здатність до нових повідомлень про нові елементи * JRTB-8: розширена здатність до набору в активному телеграмі користувача * JRTB-9: розширена здатність до набору активного користувача і/або натиснути його.
Тепер можна створювати пулл-реквест і заливати нові зміни. Ось пулл-реквест із усіма змінами за дві частини: STEP_8. Що далі? Вже здавалося б усе готове і, як кажуть у нас, може виходити у продакшен, але є ще деякі речі, які хочеться зробити. Наприклад, налаштувати роботу адмінів у бота, додати їх та додати можливість задавати їх. Також перед закінченням добре б пройтися кодом і подивитися, чи немає речей, які можна відрефакторити. Я вже бачу розсинхрон в назві article/post. Насамкінець зробимо ретроспективу того, що ми планували і що отримали. І що хочеться зробити у майбутньому. Зараз поділюся з вами досить сирою ідеєю, яка може побачити світ: зробити springboot starter, який мав би всю функціональність по роботі з телеграм-ботом і пошуком статей. Це дасть змогу уніфікувати підхід та використовувати його для інших телеграм-ботів. Таким чином, цей проект стане більш доступним для інших і зможе принести користь більшій кількості людей. Це одна з ідей. Інша ідея — йти вглиб розробки нотифікацій. Але про це ми поговоримо дещо пізніше. Всім дякую за увагу, з вас як завжди: лайк - передплата - дзвіночок , зірку нашому проекту , коментар та оцінити статтю! Дякую всім за прочитання.

Список всіх матеріалів серії на початку цієї статті.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ