Привет всем, мои дорогие друзья.
В предыдущей статье мы подготовили клиент для работы с JavaRush API для статей. Теперь можно писать логику для работы нашей джобы, которая будет выполняться каждые 15 минут.
Вот ровно так, как показано на этой схеме:
Каждые 15 минут будет запускаться джоба (по нашему — просто метод в определенном классе), который выполняется на фоне работы основного приложения и делает следующее:
Далее идем на сайт, сортируем статьи в группе Java-проекты — сначала новые — и заходим в третью статью из списка:
Зайдем в нижнюю статью и из адресной строки получим article Id — 3313:
Далее идем в MySQL Workbench и меняем значение lastArticleId на 3313. Посмотрим, что такая группа есть в базе:
И для нее выполним команду:
И все, теперь нужно подождать до следующего запуска джобы по поиску новых статей. Ожидаем, что придет два сообщения о новой статье из группы Java-проекты. Как говорится, результат не заставил себя ждать:
Получается, что бот отработал так, как мы и ожидали.
лайк - подписка - колокольчик, звезду нашему проекту, комментарий и оценить статью!
Всем спасибо за прочтение.

Находит во всех группах, которые есть в нашей БД, новые статьи, вышедшие после предыдущего выполнения.
В этой схеме указано меньшее количество групп — только с активными пользователями. На тот момент мне это показалось логичным, но сейчас я понимаю, что независимо от того, есть активные пользователи, подписанные на конкретную группу или нет, все равно нужно держать актуальным последнюю статью, что бот обработал. Может возникнуть ситуация, когда новому пользователю придет сразу все количество статей, вышедшее с момента деактивации этой группы. А это не ожидаемое поведение, и чтобы его избежать, нужно держать актуальным и те группы из нашей БД, что на данный момент не имеют активных пользователей. Если новые статьи есть, сформировать сообщения для всех пользователей, которые активно подписаны на эту группу. Если новых статей нет, просто завершаем работу.
FindNewArticleService:
package com.github.javarushcommunity.jrtb.service;
/**
* Service for finding new articles.
*/
public interface FindNewArticleService {
/**
* Find new articles and notify subscribers about it.
*/
void findNewArticles();
}
Очень простой, правда? В этом его и суть, а вся сложность будет в реализации:
package com.github.javarushcommunity.jrtb.service;
import com.github.javarushcommunity.jrtb.javarushclient.JavaRushPostClient;
import com.github.javarushcommunity.jrtb.javarushclient.dto.PostInfo;
import com.github.javarushcommunity.jrtb.repository.entity.GroupSub;
import com.github.javarushcommunity.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 JAVARUSH_WEB_POST_FORMAT = "https://javarush.com/groups/posts/%s";
private final GroupSubService groupSubService;
private final JavaRushPostClient javaRushPostClient;
private final SendBotMessageService sendMessageService;
@Autowired
public FindNewArticleServiceImpl(GroupSubService groupSubService,
JavaRushPostClient javaRushPostClient,
SendBotMessageService sendMessageService) {
this.groupSubService = groupSubService;
this.javaRushPostClient = javaRushPostClient;
this.sendMessageService = sendMessageService;
}
@Override
public void findNewArticles() {
groupSubService.findAll().forEach(gSub -> {
List<PostInfo> newPosts = javaRushPostClient.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(JAVARUSH_WEB_POST_FORMAT, key);
}
}
Здесь разберемся со всем по порядку:При помощи groupService мы находим все группы, которые есть в БД.
Потом разбегаемся по всем группам и для каждой вызываем созданный в прошлой статье клиент — javaRushPostClient.findNewPosts.
Далее при помощи метода setNewArticleId мы обновляем ID шник нашей последней новой статьи, чтобы наша база данных знала, что мы уже обработали новые.
И при помощи того, что у GroupSub есть коллекция пользователей, пробегаем по активным и отсылаем уведомления о новых статьях.
Создаем FindNewArticleJob
Мы уже говорили о том, что такое SpringScheduler, но повторим еще раз быстро: это механизм в Spring фреймворке для создания фонового процесса, который будет выполняться в определенное время, задаваемого нами. Что нужно для этого? Первый этап — добавить аннотацию @EnableScheduling к нашему входному классу для спринга:
package com.github.javarushcommunity.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.javarushcommunity.jrtb.job;
import com.github.javarushcommunity.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 последней статьи. Пойдем в базу и поменяем значение на две статьи назад. В итоге ожидаем, что будет столько статей, на сколько раньше поставим lastArticleId.





Окончание
Как и всегда — обновляем версию в pom.xml и добавляем запись в RELEASE_NOTES, чтобы история работы сохранилась и всегда можно было вернуться и понять, что изменилось. Поэтому инкрементируем на одну единицу версию:
<version>0.7.0-SNAPSHOT</version>
И обновляем RELEASE_NOTES:
## 0.7.0-SNAPSHOT
* JRTB-4: added ability to send notifications about new articles
* JRTB-8: added ability to set inactive telegram user
* JRTB-9: added ability to set active user and/or start using it.
Теперь уж можно создавать пулл-реквест и заливать новые изменения.
Вот пулл-реквест со всеми изменениями за две части: STEP_8.
Что дальше? Уже казалось бы все готово и, как говорят у нас, может выходить в продакшен, но есть еще некоторые вещи, которые хочется сделать. Например, настроить работу админов у бота, добавить их и добавить возможность задавать их.
Также перед окончанием хорошо бы пройтись по коду и посмотреть, нет ли вещей, которые можно отрефакторить. Я вот уже вижу рассинхрон в именовании article/post.
В самом конце сделаем ретроспективу того, что мы планировали и что получили. И что хочется сделать в будущем.
Сейчас поделюсь с вами достаточно сырой идеей, которая может и увидит свет: сделать springboot starter, который имел бы всю функциональность по работе с телеграм-ботом и поиском статей. Это даст возможность унифицировать подход и использовать его для других телеграм-ботов. Таким образом этот проект станет более доступным для других и сможет принести пользу большему числу людей.
Это одна из идей. Другая идея — идти в глубь разработки нотификаций. Но об этом мы поговорим несколько позже.
Всем спасибо за внимание, с вас как обычно:
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ