JavaRush /Blogue Java /Random-PT /Adicionando Spring Scheduler - "Projeto Java de A a Z"
Roman Beekeeper
Nível 35

Adicionando Spring Scheduler - "Projeto Java de A a Z"

Publicado no grupo Random-PT
Olá a todos, meus queridos amigos. No artigo anterior preparamos um cliente para trabalhar com a API JavaRush para artigos. Agora podemos escrever a lógica do nosso trabalho, que será executado a cada 15 minutos. Exatamente como mostra este diagrama: “Projeto Java de A a Z”: Adicionando Spring Scheduler - 1A cada 15 minutos será lançado um job (em nossa opinião, apenas um método de uma classe específica), que é executado em background da aplicação principal e faz o seguinte:
  1. Encontra em todos os grupos que estão em nossa base de dados novos artigos publicados após a execução anterior.

    Este esquema especifica um número menor de grupos – apenas aqueles com usuários ativos. Naquela época parecia lógico para mim, mas agora entendo que independentemente de haver usuários ativos inscritos em um grupo específico ou não, você ainda precisa manter atualizado o artigo mais recente que o bot processou. Pode surgir uma situação em que um novo usuário receba imediatamente todo o número de artigos publicados desde a desativação deste grupo. Este comportamento não é esperado e, para evitá-lo, precisamos manter atualizados os grupos de nosso banco de dados que atualmente não possuem usuários ativos.
  2. Se houver novos artigos, gere mensagens para todos os usuários que estão ativamente inscritos neste grupo. Se não houver novos artigos, simplesmente concluímos o trabalho.

Aliás, já mencionei no meu canal TG que o bot já está funcionando e enviando novos artigos com base em assinaturas. Vamos começar a escrever FindNewArtcileService . Lá acontecerá todo o trabalho de busca e envio de mensagens, e o trabalho apenas lançará o método deste serviço:

EncontrarNovoArtigoServiço:

package com.github.javarushcommunity.jrtb.service;

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

   /**
    * Find new articles and notify subscribers about it.
    */
   void findNewArticles();
}
Muito simples, certo? Esta é a sua essência, e toda a dificuldade estará na implementação:
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);
   }
}
Aqui trataremos de tudo em ordem:
  1. Usando groupService encontramos todos os grupos que estão no banco de dados.

  2. Em seguida, dispersamos todos os grupos e para cada um chamamos o cliente criado no último artigo - javaRushPostClient.findNewPosts .

  3. Em seguida, usando o método setNewArticleId , atualizamos o ID do artigo do nosso novo artigo mais recente para que nosso banco de dados saiba que já processamos novos.

  4. E aproveitando o fato do GroupSub possuir um acervo de usuários, percorremos os ativos e enviamos notificações sobre novos artigos.

Não vamos discutir qual é a mensagem agora, não é muito importante para nós. O principal é que o método funcione. A lógica de busca de novos artigos e envio de notificações está pronta, para que você possa prosseguir com a criação de uma vaga.

Criar FindNewArticleJob

Já falamos sobre o que é SpringScheduler, mas vamos repetir rapidamente: é um mecanismo do framework Spring para criar um processo em segundo plano que será executado em um horário específico que definimos. O que você precisa para isso? A primeira etapa é adicionar a anotação @EnableScheduling à nossa classe de entrada 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);
   }

}
O segundo passo é criar uma classe, adicioná-la ao ApplicationContext e criar nela um método que será executado periodicamente. Criamos um pacote de tarefas no mesmo nível do repositório, serviço e assim por diante, e lá criamos a classe 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));
   }
}
Para adicionar esta classe ao Contexto da Aplicação utilizei a anotação @Component . E para que o método dentro da classe saiba que precisa ser executado periodicamente, adicionei uma anotação ao método: @Scheduled(fixedRateString = "${bot.recountNewArticleFixedRate}") . Mas qual será o valor - já o definimos no arquivo application.properties:
bot.recountNewArticleFixedRate = 900000
Aqui o valor está em milissegundos. Serão 15 minutos. Nesse método tudo é simples: adicionei uma métrica super simples para mim nos logs para calcular a busca por novos artigos, para pelo menos entender aproximadamente a rapidez com que funciona.

Testando novas funcionalidades

Agora vamos testar em nosso bot de teste. Mas como? Não vou deletar artigos todas as vezes para mostrar que as notificações chegaram? Claro que não. Simplesmente editaremos os dados no banco de dados e iniciaremos o aplicativo. Vou testá-lo no meu servidor de testes. Para fazer isso, vamos nos inscrever em algum grupo. Quando a assinatura for concluída, o grupo receberá o ID atual do artigo mais recente. Vamos ao banco de dados e alteramos o valor dois artigos atrás. Como resultado, esperamos que haja tantos artigos quanto definimos lastArticleId anteriormente . "Projeto Java de A a Z": Adicionando Spring Scheduler - 2Em seguida, vamos ao site, classificamos os artigos no grupo de projetos Java - os novos primeiro - e vamos para o terceiro artigo da lista: "Projeto Java de A a Z": Adicionando Spring Scheduler - 3Vamos para o artigo de baixo e na barra de endereço obtemos o Id do artigo - 3313: "Projeto Java de A a Z": Adicionando Spring Scheduler - 4Próximo , vá para MySQL Workbench e altere o valor lastArticleId para 3313. Vamos ver que tal grupo está no banco de dados: "Projeto Java de A a Z": Adicionando Spring Scheduler - 5E para isso executaremos o comando: "Projeto Java de A a Z": Adicionando Spring Scheduler - 6E pronto, agora você precisa esperar até o próximo lançamento do trabalho para procurar novos artigos. Esperamos receber duas mensagens sobre um novo artigo do grupo de projetos Java. Como se costuma dizer, o resultado não demorou a chegar: "Projeto Java de A a Z": Adicionando Spring Scheduler - 7Acontece que o bot funcionou como esperávamos.

Final

Como sempre, atualizamos a versão em pom.xml e adicionamos uma entrada em RELEASE_NOTES para que o histórico de trabalho seja salvo e você possa sempre voltar e entender o que mudou. Portanto, incrementamos a versão em uma unidade:
<version>0.7.0-SNAPSHOT</version>
E atualize RELEASE_NOTES:
## 0.7.0-SNAPSHOT * JRTB-4: capacidade adicional de enviar notificações sobre novos artigos * JRTB-8: capacidade adicional de definir usuário de telegrama inativo * JRTB-9: capacidade adicional de definir usuário ativo e/ou começar a usá-lo.
Agora você pode criar uma solicitação pull e fazer upload de novas alterações. Aqui está a solicitação pull com todas as alterações em duas partes: STEP_8 . Qual é o próximo? Parece que está tudo pronto e, como dizemos, pode entrar em produção, mas ainda há algumas coisas que quero fazer. Por exemplo, configure o trabalho dos administradores do bot, adicione-os e adicione a capacidade de configurá-los. Também é uma boa ideia revisar o código antes de terminar e ver se há coisas que podem ser refatoradas. Já posso ver a dessincronização na nomenclatura do artigo/post. Ao final faremos uma retrospectiva do que planejamos e do que recebemos. E o que você gostaria de fazer no futuro? Agora vou compartilhar com vocês uma ideia bastante rudimentar que pode e verá a luz do dia: fazer um starter springboot que teria todas as funcionalidades para trabalhar com um bot de telegrama e pesquisar artigos. Isso permitirá unificar a abordagem e utilizá-la para outros bots de telegramas. Isto tornará este projeto mais acessível a outros e poderá beneficiar mais pessoas. Esta é uma das ideias. Outra ideia é aprofundar o desenvolvimento de notificações. Mas falaremos sobre isso um pouco mais tarde. Obrigado a todos pela atenção, como sempre: curta - inscreva-se - sininho , estrela nosso projeto , comente e avalie o artigo! Obrigado a todos pela leitura.

Uma lista de todos os materiais da série está no início deste artigo.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION