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.