JavaRush /Java 博客 /Random-ZH /添加 Spring Scheduler - 《Java 项目从 A 到 Z》
Roman Beekeeper
第 35 级

添加 Spring Scheduler - 《Java 项目从 A 到 Z》

已在 Random-ZH 群组中发布
大家好,我亲爱的朋友们。在上一篇文章中,我们准备了一个用于文章的 JavaRush API 的客户端。现在我们可以为我们的作业编写逻辑,该逻辑将每 15 分钟执行一次。如下图所示: “Java 项目从头到尾”:添加 Spring Scheduler - 1每 15 分钟就会启动一个作业(在我们看来,只是特定类中的一个方法),该作业在主应用程序的后台执行,并执行以下操作:
  1. 在我们数据库中的所有组中查找上次执行后发布的新文章。

    该方案指定了较少数量的组 - 仅那些具有活跃用户的组。当时这对我来说似乎很合乎逻辑,但现在我明白,无论是否有活跃用户订阅特定组,您仍然需要保持机器人处理的最新文章是最新的。当新用户立即收到自该组停用以来发布的全部文章数时,可能会出现这种情况。这不是预期的行为,为了避免这种情况,我们需要将那些当前没有活动用户的组保留在我们的数据库中。
  2. 如果有新文章,则为主动订阅该组的所有用户生成消息。如果没有新的文章,我们就简单地完成工作。

顺便说一句,我已经在我的 TG 频道中提到,该机器人已经开始工作并根据订阅发送新文章。让我们开始编写FindNewArtcileService。所有搜索和发送消息的工作都将在那里进行,并且该作业只会启动该服务的方法:

查找新文章服务:

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);
   }
}
在这里我们将按顺序处理所有事情:
  1. 使用groupService我们找到数据库中的所有组。

  2. 然后我们分散到所有组,并为每个组调用上一篇文章中创建的客户端 - javaRushPostClient.findNewPosts

  3. 接下来,使用setNewArticleId方法,我们更新最新文章的文章 ID,以便我们的数据库知道我们已经处理了新文章。

  4. 利用 GroupSub 拥有用户集合的事实,我们可以浏览活跃用户并发送有关新文章的通知。

我们现在不会讨论这个消息是什么,这对我们来说不是很重要。最主要的是该方法有效。搜索新文章和发送通知的逻辑已准备就绪,因此您可以继续创建作业。

创建 FindNewArticleJob

我们已经讨论过 SpringScheduler 是什么,但让我们快速重复一下:它是 Spring 框架中的一种机制,用于创建将在我们设置的特定时间运行的后台进程。为此你需要什么?第一步是将@EnableScheduling注解添加到我们的 spring 输入类中:
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并在其中创建一个将定期运行的方法。我们在与存储库、服务等相同的级别创建一个作业包,并在那里创建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));
   }
}
为了将此类添加到应用程序上下文,我使用了@Component注释。为了让类内的方法知道它需要定期运行,我向该方法添加了一个注释:@Scheduled(fixedRateString = "${bot.recountNewArticleFixedRate}")。但它的值是什么 - 我们已经在 application.properties 文件中设置了它:
bot.recountNewArticleFixedRate = 900000
这里的值以毫秒为单位。需要 15 分钟。在这种方法中,一切都很简单:我在日志中为自己添加了一个超级简单的指标来计算新文章的搜索量,以便至少大致了解它的工作速度。

测试新功能

现在我们将在我们的测试机器人上进行测试。但如何呢?我不会每次都删文章才显示通知到了?当然不是。我们将简单地编辑数据库中的数据并启动应用程序。我将在我的测试服务器上测试它。为此,我们需要订阅某个群组。订阅完成后,群组将获得最新文章的当前 ID。我们去数据库把两篇文章的值改回来。因此,我们预计将有与我们将lastArticleId设置为更早的文章一样多的文章。“Java 项目从头到尾”:添加 Spring Scheduler - 2接下来,我们访问该网站,对 Java 项目组中的文章进行排序 -首先是新文章- 然后转到列表中的第三篇文章:"Java-проект от А до Я": Добавляем Spring Scheduler - 3让我们转到底部文章,从地址栏中我们得到文章 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。下一步是什么?看起来一切都准备好了,正如我们所说,它可以投入生产,但仍然有一些事情我想做。例如,为机器人配置管理员的工作,添加管理员并添加设置它们的功能。在完成之前检查一下代码并看看是否有可以重构的东西也是一个好主意。我已经可以看到文章/帖子命名中的不同步。最后,我们将对我们的计划和收到的内容进行回顾。您将来想做什么?现在我将与您分享一个相当粗略的想法,该想法可以并且将会看到曙光:制作一个 springboot 启动器,该启动器将具有使用电报机器人和搜索文章的所有功能。这将使统一该方法并将其用于其他电报机器人成为可能。这将使这个项目更容易为其他人所接受,并使更多的人受益。这是想法之一。另一个想法是更深入地进行通知开发。但我们稍后会讨论这个。 感谢大家一如既往的关注:点赞 - 订阅 - 响铃为我们的项目加星,评论并评价文章! 感谢大家的阅读。

该系列所有材料的列表位于本文开头。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION