JavaRush /Java Blog /Random-JA /Spring Scheduler の追加 - 「Java プロジェクトの A から Z まで」
Roman Beekeeper
レベル 35

Spring Scheduler の追加 - 「Java プロジェクトの A から Z まで」

Random-JA グループに公開済み
親愛なる皆さん、こんにちは。前回の記事では、記事用の JavaRush API を操作するためのクライアントを準備しました。これで、15 分ごとに実行されるジョブのロジックを作成できるようになりました。まさにこの図に示されているとおりです。15 「Java プロジェクトの A to Z」: Spring Scheduler の追加 - 1分ごとにジョブ (私たちの意見では、特定のクラスのメソッドにすぎません) が起動され、メイン アプリケーションのバックグラウンドで実行され、次の処理が実行されます。
  1. データベース内のすべてのグループで、前回の実行後に公開された新しい記事を検索します。

    このスキームでは、少数のグループ (アクティブなユーザーが含まれるグループのみ) を指定します。当時はそれが当然だと思えましたが、今では、特定のグループに登録しているアクティブ ユーザーがいるかどうかに関係なく、ボットが処理した最新の記事を最新の状態に保つ必要があることが理解できました。新しいユーザーが、このグループの非アクティブ化以降に公開された記事の全数をすぐに受け取る状況が発生する可能性があります。これは予期された動作ではないため、これを回避するには、現在アクティブなユーザーがいないグループをデータベースから最新の状態に保つ必要があります。
  2. 新しい記事がある場合は、このグループにアクティブに購読しているすべてのユーザーにメッセージを生成します。新しい記事がない場合は、そのまま作業を完了します。

ちなみに、私の TG チャンネルでは、ボットがすでに動作しており、購読に基づいて新しい記事を送信していることをすでに述べました。FindNewArtcileServiceの作成を始めましょう。メッセージの検索と送信のすべての作業はそこで行われ、ジョブはこのサービスのメソッドのみを起動します。

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);
   }
}
ここでは、すべてを順番に処理します。
  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);
   }

}
2 番目のステップは、クラスを作成して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 が与えられます。データベースに移動して、値を 2 記事前に変更してみましょう。その結果、 lastArticleId をearly に設定したのと同じ数の記事が存在すると予想されます。「Java プロジェクトの A to Z」: Spring Scheduler の追加 - 2次に、サイトにアクセスし、Java プロジェクト グループ内の記事を並べ替えて (新しいものから順に)、リストの 3 番目の記事に移動します。一番下の記事に移動し、アドレス バーから記事 ID - 3313:次を「Java プロジェクトの A to Z」: Spring Scheduler の追加 - 3取得します。「Java プロジェクトの A to Z」: Spring Scheduler の追加 - 4、MySQL Workbench に移動し、lastArticleId値を3313 に変更します。そのようなグループがデータベース内にあることを確認しましょう:「Java プロジェクトの A to Z」: Spring Scheduler の追加 - 5そして、それに対してコマンドを実行します:「Java プロジェクトの A to Z」: Spring Scheduler の追加 - 6これで終わりです。次のジョブの起動まで待つ必要があります。新しい記事を検索します。Java プロジェクト グループから新しい記事に関するメッセージが 2 件届く予定です。彼らが言うように、結果が出るまでに時間はかかりませんでした。「Java プロジェクトの A から Z まで」: Spring Scheduler の追加 - 7ボットが期待どおりに機能したことが判明しました。

エンディング

いつものように、pom.xml のバージョンを更新し、RELEASE_NOTES にエントリを追加します。これにより、作業履歴が保存され、いつでも戻って変更内容を理解できるようになります。したがって、バージョンを 1 単位ずつインクリメントします。
<version>0.7.0-SNAPSHOT</version>
そして、RELEASE_NOTES を更新します。
## 0.7.0-SNAPSHOT * JRTB-4: 新しい記事に関する通知を送信する機能を追加 * JRTB-8: 非アクティブなテレグラム ユーザーを設定する機能を追加 * JRTB-9: アクティブなユーザーを設定したり、その使用を開始したりする機能を追加。
これで、プル リクエストを作成し、新しい変更をアップロードできるようになりました。これは、2 つの部分にすべての変更を加えたプル リクエストです: STEP_8。次は何ですか?すべての準備が整い、すでに本番環境に入ることができるように見えますが、まだやりたいことがいくつかあります。たとえば、ボットの管理者の作業を構成し、それらを追加し、それらを設定する機能を追加します。終了する前にコードを確認して、リファクタリングできる部分があるかどうかを確認することもお勧めします。記事/投稿の名前に非同期がすでに見られます。最後に、私たちが計画したことと受け取ったものの振り返りを行います。そして、将来何がしたいですか?ここで、日の目を見ることになる、そして将来日の目を見ることになるかなり大雑把なアイデアを共有します。それは、電報ボットと連携して記事を検索するためのすべての機能を備えたスプリングブート スターターを作成するというものです。これにより、アプローチを統一し、他の電報ボットにも使用できるようになります。これにより、このプロジェクトが他の人にとってよりアクセスしやすくなり、より多くの人に利益をもたらすことができます。これもアイデアの一つです。もう 1 つのアイデアは、通知の開発をさらに深く掘り下げることです。ただし、これについては少し後で説明します。 いつものように、ご清聴いただきありがとうございます。いいね - 購読 - ベル、私たちのプロジェクトにスターを付け、コメントし、記事を評価してください。 読んでくれた皆さん、ありがとう。

シリーズのすべてのマテリアルのリストは、この記事の冒頭にあります。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION