Cinquième série d’améliorations apportées à l’application Reddit

1. Vue d’ensemble

Continuons à faire avancer l’application Reddit depuis notre lien:/étude-de-cas-a-reddit-app-avec-spring[étude de cas en cours].

2. Envoyer des notifications par courrier électronique sur les commentaires post

Il manque des notifications par courrier électronique à Reddit - en clair. Ce que j’aimerais voir, c’est que chaque fois que quelqu’un commente un de mes messages, je reçois une brève notification par courrier électronique avec le commentaire.

En bref, c’est le but de cette fonctionnalité: les notifications par courrier électronique sur les commentaires.

Nous allons implémenter un ordonnanceur simple qui vérifie:

  • quels utilisateurs doivent recevoir une notification par e-mail avec les réponses de posts

  • si l’utilisateur a reçu des réponses à ses messages dans sa boîte de réception Reddit

Il enverra ensuite simplement une notification par courrier électronique avec des réponses aux messages non lus.

2.1. Préférences de l’utilisateur

Premièrement, nous devrons modifier notre entité de préférence et notre DTO en ajoutant:

private boolean sendEmailReplies;

Permettre aux utilisateurs de choisir s’ils souhaitent recevoir une notification par courrier électronique avec les réponses des publications.

2.2. Planificateur de notifications

Ensuite, voici notre planificateur simple:

@Component
public class NotificationRedditScheduler {

    @Autowired
    private INotificationRedditService notificationRedditService;

    @Autowired
    private PreferenceRepository preferenceRepository;

    @Scheduled(fixedRate = 60 **  60 **  1000)
    public void checkInboxUnread() {
        List<Preference> preferences = preferenceRepository.findBySendEmailRepliesTrue();
        for (Preference preference : preferences) {
            notificationRedditService.checkAndNotify(preference);
        }
    }
}

Notez que le planificateur fonctionne toutes les heures - mais nous pouvons bien entendu utiliser une cadence beaucoup plus courte si nous le souhaitons.

2.3. Le service de notification

Parlons maintenant de notre service de notification:

@Service
public class NotificationRedditService implements INotificationRedditService {
    private Logger logger = LoggerFactory.getLogger(getClass());
    private static String NOTIFICATION__TEMPLATE = "You have %d unread post replies.";
    private static String MESSAGE__TEMPLATE = "%s replied on your post %s : %s";

    @Autowired
    @Qualifier("schedulerRedditTemplate")
    private OAuth2RestTemplate redditRestTemplate;

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Autowired
    private UserRepository userRepository;

    @Override
    public void checkAndNotify(Preference preference) {
        try {
            checkAndNotifyInternal(preference);
        } catch (Exception e) {
            logger.error(
              "Error occurred while checking and notifying = " + preference.getEmail(), e);
        }
    }

    private void checkAndNotifyInternal(Preference preference) {
        User user = userRepository.findByPreference(preference);
        if ((user == null) || (user.getAccessToken() == null)) {
            return;
        }

        DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(user.getAccessToken());
        token.setRefreshToken(new DefaultOAuth2RefreshToken((user.getRefreshToken())));
        token.setExpiration(user.getTokenExpiration());
        redditRestTemplate.getOAuth2ClientContext().setAccessToken(token);

        JsonNode node = redditRestTemplate.getForObject(
          "https://oauth.reddit.com/message/selfreply?mark=false", JsonNode.class);
        parseRepliesNode(preference.getEmail(), node);
    }

    private void parseRepliesNode(String email, JsonNode node) {
        JsonNode allReplies = node.get("data").get("children");
        int unread = 0;
        for (JsonNode msg : allReplies) {
            if (msg.get("data").get("new").asBoolean()) {
                unread++;
            }
        }
        if (unread == 0) {
            return;
        }

        JsonNode firstMsg = allReplies.get(0).get("data");
        String author = firstMsg.get("author").asText();
        String postTitle = firstMsg.get("link__title").asText();
        String content = firstMsg.get("body").asText();

        StringBuilder builder = new StringBuilder();
        builder.append(String.format(NOTIFICATION__TEMPLATE, unread));
        builder.append("\n");
        builder.append(String.format(MESSAGE__TEMPLATE, author, postTitle, content));
        builder.append("\n");
        builder.append("Check all new replies at ");
        builder.append("https://www.reddit.com/message/unread/");

        eventPublisher.publishEvent(new OnNewPostReplyEvent(email, builder.toString()));
    }
}

Notez que:

  • Nous appelons l’API Reddit et obtenons toutes les réponses, puis vérifions-les un à un.

voir si c’est nouveau "non lu".

  • S’il y a des réponses non lues, nous déclenchons un événement pour envoyer à cet utilisateur une

notification par courrier électronique.

2.4. Nouvelle réponse

Voici notre événement simple:

public class OnNewPostReplyEvent extends ApplicationEvent {
    private String email;
    private String content;

    public OnNewPostReplyEvent(String email, String content) {
        super(email);
        this.email = email;
        this.content = content;
    }
}

2.5. Répondeur

Enfin, voici notre auditeur:

@Component
public class ReplyListener implements ApplicationListener<OnNewPostReplyEvent> {
    @Autowired
    private JavaMailSender mailSender;

    @Autowired
    private Environment env;

    @Override
    public void onApplicationEvent(OnNewPostReplyEvent event) {
        SimpleMailMessage email = constructEmailMessage(event);
        mailSender.send(email);
    }

    private SimpleMailMessage constructEmailMessage(OnNewPostReplyEvent event) {
        String recipientAddress = event.getEmail();
        String subject = "New Post Replies";
        SimpleMailMessage email = new SimpleMailMessage();
        email.setTo(recipientAddress);
        email.setSubject(subject);
        email.setText(event.getContent());
        email.setFrom(env.getProperty("support.email"));
        return email;
    }
}

3. Contrôle de la simultanéité de session

Ensuite, définissons des règles plus strictes concernant le nombre de sessions simultanées autorisées par l’application. Plus précisément - n’autorisons pas les sessions simultanées :

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.sessionManagement()
          .maximumSessions(1)
          .maxSessionsPreventsLogin(true);
}

Notez que, comme nous utilisons une implémentation personnalisée de UserDetails , nous devons remplacer equals () et hashcode () , car la stratégie de contrôle de session stocke tous les principaux dans une carte et doit pouvoir les récupérer:

public class UserPrincipal implements UserDetails {

    private User user;

    @Override
    public int hashCode() {
        int prime = 31;
        int result = 1;
        result = (prime **  result) + ((user == null) ? 0 : user.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        UserPrincipal other = (UserPrincipal) obj;
        if (user == null) {
            if (other.user != null) {
                return false;
            }
        } else if (!user.equals(other.user)) {
            return false;
        }
        return true;
    }
}

4. Servlet d’API séparé

L’application sert maintenant à la fois le front-end et l’API à partir du même servlet - ce qui n’est pas idéal.

Séparons maintenant ces deux responsabilités principales et répartissez-les en deux servlets différents :

@Bean
public ServletRegistrationBean frontendServlet() {
    ServletRegistrationBean registration =
      new ServletRegistrationBean(new DispatcherServlet(), "/** ");

    Map<String, String> params = new HashMap<String, String>();
    params.put("contextClass",
      "org.springframework.web.context.support.AnnotationConfigWebApplicationContext");
    params.put("contextConfigLocation", "org.baeldung.config.frontend");
    registration.setInitParameters(params);

    registration.setName("FrontendServlet");
    registration.setLoadOnStartup(1);
    return registration;
}

@Bean
public ServletRegistrationBean apiServlet() {
    ServletRegistrationBean registration =
      new ServletRegistrationBean(new DispatcherServlet(), "/api/** ");

    Map<String, String> params = new HashMap<String, String>();
    params.put("contextClass",
      "org.springframework.web.context.support.AnnotationConfigWebApplicationContext");
    params.put("contextConfigLocation", "org.baeldung.config.api");

    registration.setInitParameters(params);
    registration.setName("ApiServlet");
    registration.setLoadOnStartup(2);
    return registration;
}

@Override
protected SpringApplicationBuilder configure(final SpringApplicationBuilder application) {
    application.sources(Application.class);
    return application;
}

Notez que nous avons maintenant un servlet frontal qui gère toutes les demandes frontales et amorce uniquement un contexte Spring spécifique au frontal et nous avons ensuite l’API Servlet - amorçant un contexte Spring totalement différent pour l’API.

De plus, il est très important que ces deux contextes Spring servlet soient des contextes enfants. Le contexte parent - créé par SpringApplicationBuilder - analyse le package root à la recherche d’une configuration commune telle que la persistance, le service, …​ etc.

Voici notre WebFrontendConfig :

@Configuration
@EnableWebMvc
@ComponentScan({ "org.baeldung.web.controller.general" })
public class WebFrontendConfig implements WebMvcConfigurer {

    @Bean
    public static PropertySourcesPlaceholderConfigurer
      propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/jsp/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home");
        ...
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/** ** ").addResourceLocations("/resources/");
    }
}

Et WebApiConfig :

@Configuration
@EnableWebMvc
@ComponentScan({ "org.baeldung.web.controller.rest", "org.baeldung.web.dto" })
public class WebApiConfig implements WebMvcConfigurer {

    @Bean
    public ModelMapper modelMapper() {
        return new ModelMapper();
    }
}

5. URL de flux unshorten

Enfin, nous allons améliorer le travail avec RSS.

Parfois, les flux RSS sont raccourcis ou redirigés via un service externe tel que Feedburner. Ainsi, lorsque nous chargeons l’URL d’un flux dans l’application, nous devons nous assurer de suivre cette URL dans toutes les redirections jusqu’à atteindre l’URL principale. nous nous soucions réellement de.

Ainsi, lorsque nous publions le lien de l’article vers Reddit, nous publions en fait l’URL d’origine correcte:

@RequestMapping(value = "/url/original")
@ResponseBody
public String getOriginalLink(@RequestParam("url") String sourceUrl) {
    try {
        List<String> visited = new ArrayList<String>();
        String currentUrl = sourceUrl;
        while (!visited.contains(currentUrl)) {
            visited.add(currentUrl);
            currentUrl = getOriginalUrl(currentUrl);
        }
        return currentUrl;
    } catch (Exception ex) {
       //log the exception
        return sourceUrl;
    }
}

private String getOriginalUrl(String oldUrl) throws IOException {
    URL url = new URL(oldUrl);
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    connection.setInstanceFollowRedirects(false);
    String originalUrl = connection.getHeaderField("Location");
    connection.disconnect();
    if (originalUrl == null) {
        return oldUrl;
    }
    if (originalUrl.indexOf("?") != -1) {
        return originalUrl.substring(0, originalUrl.indexOf("?"));
    }
    return originalUrl;
}

Quelques points à prendre en compte avec cette implémentation:

  • Nous gérons plusieurs niveaux de redirection

  • Nous suivons également toutes les URL visitées pour éviter les boucles de redirection

6. Conclusion

Et c’est tout - quelques améliorations concrètes pour améliorer l’application Reddit. L’étape suivante consiste à tester les performances de l’API et à voir comment elle se comporte dans un scénario de production.