JavaRush /Java Blog /Random EN /Adding Spring Scheduler - "Java project from A to Z"

Adding Spring Scheduler - "Java project from A to Z"

Published in the Random EN group
Hello everyone, my dear friends. In the previous article , we prepared a client for working with the JavaRush API for articles. Now we can write logic for our job, which will be executed every 15 minutes. Exactly as shown in this diagram: “Java project from A to Z”: Adding Spring Scheduler - 1Every 15 minutes a job will be launched (in our opinion, just a method in a specific class), which is executed in the background of the main application and does the following:
  1. Finds in all groups that are in our database new articles published after the previous execution.

    This scheme specifies a smaller number of groups - only those with active users. At that time it seemed logical to me, but now I understand that regardless of whether there are active users subscribed to a specific group or not, you still need to keep the latest article that the bot processed up-to-date. A situation may arise when a new user immediately receives the entire number of articles published since the deactivation of this group. This is not expected behavior, and to avoid it, we need to keep those groups from our database that currently do not have active users up-to-date.
  2. If there are new articles, generate messages for all users who are actively subscribed to this group. If there are no new articles, we simply complete the work.

By the way, I already mentioned in my TG channel that the bot is already working and sending new articles based on subscriptions. Let's start writing FindNewArtcileService . All the work of searching and sending messages will take place there, and the job will only launch the method of this service:

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();
}
Very simple, right? This is its essence, and all the difficulty will be in the implementation:
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);
   }
}
Here we will deal with everything in order:
  1. Using groupService we find all the groups that are in the database.

  2. Then we disperse to all groups and for each we call the client created in the last article - javaRushPostClient.findNewPosts .

  3. Next, using the setNewArticleId method , we update the article ID of our latest new article so that our database knows that we have already processed new ones.

  4. And using the fact that GroupSub has a collection of users, we go through the active ones and send notifications about new articles.

We won’t discuss what the message is now, it’s not very important for us. The main thing is that the method works. The logic for searching for new articles and sending notifications is ready, so you can move on to creating a job.

Create FindNewArticleJob

We've already talked about what SpringScheduler is, but let's repeat it quickly: it's a mechanism in the Spring framework for creating a background process that will run at a specific time that we set. What do you need for this? The first step is to add the @EnableScheduling annotation to our spring input class:
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);
   }

}
The second step is to create a class, add it to the ApplicationContext and create a method in it that will be run periodically. We create a job package at the same level as repository, service, and so on, and there we create the FindNewArticleJob class :
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));
   }
}
To add this class to the Application Context I used the @Component annotation . And so that the method inside the class knows that it needs to be run periodically, I added an annotation to the method: @Scheduled(fixedRateString = "${bot.recountNewArticleFixedRate}") . But we set it in the application.properties file:
bot.recountNewArticleFixedRate = 900000
Here the value is in milliseconds. It will be 15 minutes. In this method, everything is simple: I added a super simple metric for myself in the logs to calculate the search for new articles, in order to at least roughly understand how fast it works.

Testing new functionality

Now we will test on our test bot. But how? I won’t delete articles every time to show that notifications have arrived? Of course not. We will simply edit the data in the database and launch the application. I will test it on my test server. To do this, let's subscribe to some group. When the subscription is completed, the group will be given the current ID of the latest article. Let's go to the database and change the value two articles back. As a result, we expect that there will be as many articles as we set lastArticleId to earlier . "Java project from A to Z": Adding Spring Scheduler - 2Next, we go to the site, sort the articles in the Java projects group - new ones first - and go to the third article from the list: "Java project from A to Z": Adding Spring Scheduler - 3Let's go to the bottom article and from the address bar we get article Id - 3313: "Java project from A to Z": Adding Spring Scheduler - 4Next, go to MySQL Workbench and change the lastArticleId value to 3313. Let's see that such a group is in the database: "Java project from A to Z": Adding Spring Scheduler - 5And for it we will execute the command: "Java project from A to Z": Adding Spring Scheduler - 6And that's it, now you need to wait until the next launch of the job to search for new articles. We expect to receive two messages about a new article from the Java projects group. As they say, the result was not long in coming: "Java project from A to Z": Adding Spring Scheduler - 7It turns out that the bot worked as we expected.

Ending

As always, we update the version in pom.xml and add an entry to RELEASE_NOTES so that the work history is saved and you can always go back and understand what has changed. Therefore, we increment the version by one unit:
<version>0.7.0-SNAPSHOT</version>
And update 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.
Now you can create a pull request and upload new changes. Here is the pull request with all the changes in two parts: STEP_8 . What's next? It would seem that everything is ready and, as we say, it can go into production, but there are still some things that I want to do. For example, configure the work of admins for the bot, add them and add the ability to set them. It's also a good idea to go through the code before finishing and see if there are things that can be refactored. I can already see the desynchronization in the naming of article/post. At the very end, we will do a retrospective of what we planned and what we received. And what would you like to do in the future? Now I’ll share with you a fairly crude idea that can and will see the light of day: to make a springboot starter that would have all the functionality for working with a telegram bot and searching for articles. This will make it possible to unify the approach and use it for other telegram bots. This will make this project more accessible to others and can benefit more people. This is one of the ideas. Another idea is to go deeper into notification development. But we'll talk about this a little later. Thank you all for your attention, as usual: like - subscribe - bell , star for our project , comment and rate the article! Thanks everyone for reading.

A list of all materials in the series is at the beginning of this article.

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