Fünfte Verbesserungsrunde der Reddit-Anwendung

1. Überblick

Lassen Sie uns die Reddit-Anwendung über unseren Link weiter vorantreiben:/case-study-a-reddit-app-with-spring[laufende Fallstudie].

2. E-Mail-Benachrichtigungen zu Post-Kommentaren senden

Bei Reddit fehlen E-Mail-Benachrichtigungen - schlicht und einfach. Was ich gerne sehen möchte, ist: Wenn jemand einen meiner Posts kommentiert, erhalte ich eine kurze E-Mail-Benachrichtigung mit dem Kommentar.

Einfach ausgedrückt - dies ist das Ziel dieser Funktion hier - E-Mail-Benachrichtigungen zu Kommentaren.

Wir implementieren einen einfachen Scheduler, der Folgendes überprüft:

  • welche Benutzer eine E-Mail-Benachrichtigung mit den Antworten der Beiträge erhalten sollen

  • wenn der Benutzer Post-Antworten in seinen Reddit-Posteingang erhalten hat

Es wird dann einfach eine E-Mail-Benachrichtigung mit ungelesenen Antworten gesendet.

2.1. Nutzerpreferenzen

Zuerst müssen wir unsere Preference-Entität und DTO ändern, indem wir Folgendes hinzufügen:

private boolean sendEmailReplies;

Benutzer können auswählen, ob sie eine E-Mail-Benachrichtigung mit den Antworten der Beiträge erhalten möchten.

2.2. Benachrichtigungsplaner

Als nächstes hier unser einfacher Scheduler:

@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);
        }
    }
}

Beachten Sie, dass der Scheduler stündlich ausgeführt wird. Wenn Sie möchten, können wir natürlich eine viel kürzere Trittfrequenz verwenden.

2.3. Der Benachrichtigungsdienst

Lassen Sie uns nun unseren Benachrichtigungsdienst besprechen:

@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()));
    }
}

Beachten Sie, dass:

  • Wir rufen Reddit API an und erhalten alle Antworten, die dann einzeln geprüft werden

Sehen Sie, ob es neu "ungelesen" ist.

  • Wenn es ungelesene Antworten gibt, lösen wir ein Ereignis aus, um diesen Benutzer zu senden

Email Benachrichtigung.

2.4. Neues Antwortereignis

Hier ist unsere einfache Veranstaltung:

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. Antworten Listener

Schließlich ist hier unser Zuhörer:

@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. Session Concurrency Control

Als Nächstes legen wir einige strengere Regeln für die Anzahl gleichzeitiger Sitzungen fest, die die Anwendung zulässt. Mehr zum Punkt - erlauben wir nicht gleichzeitige Sitzungen :

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

Beachten Sie, dass wir - da wir eine benutzerdefinierte UserDetails -Implementierung verwenden - equals () und hashcode () überschreiben müssen, da die Strategie der Sitzungssteuerung alle Principals in einer Map speichert und diese abrufen muss:

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. Separates API-Servlet

Die Anwendung bedient jetzt sowohl das Frontend als auch die API aus demselben Servlet - was nicht ideal ist.

Lassen Sie uns nun diese beiden Hauptaufgaben aufteilen und in zwei verschiedene Servlets aufteilen:

@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;
}

Beachten Sie, dass wir jetzt ein Front-End-Servlet haben, das alle Front-End-Anforderungen verarbeitet und nur einen Spring-Kontext für das Front-End aufruft. und dann haben wir das API Servlet - bootstrapping einen völlig anderen Spring-Kontext für die API.

Auch - sehr wichtig - diese beiden Servlet-Spring-Kontexte sind untergeordnete Kontexte. Der übergeordnete Kontext - erstellt von SpringApplicationBuilder - durchsucht das root -Paket nach allgemeiner Konfiguration wie Persistenz, Service, usw.

Hier ist unsere 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/");
    }
}

Und 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 für unkürzte Feeds

Schließlich - wir werden die Arbeit mit RSS verbessern.

Manchmal werden RSS-Feeds über einen externen Dienst wie Feedburner gekürzt oder umgeleitet. Wenn Sie also die URL eines Feeds in der Anwendung laden, müssen Sie sicherstellen, dass wir dieser URL durch alle Weiterleitungen folgen, bis wir die Haupt-URL erreichen wir kümmern uns eigentlich um.

Wenn wir also den Link des Artikels bei Reddit veröffentlichen, veröffentlichen wir tatsächlich die korrekte, ursprüngliche URL:

@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;
}

Einige Dinge, die Sie bei dieser Implementierung beachten sollten:

  • Wir behandeln mehrere Umleitungsstufen

  • Wir verfolgen auch alle besuchten URLs, um Umleitungsschleifen zu vermeiden

6. Fazit

Und das ist es - ein paar solide Verbesserungen, um die Reddit-Anwendung zu verbessern. Der nächste Schritt besteht darin, einige Leistungstests der API durchzuführen und zu sehen, wie sich diese in einem Produktionsszenario verhalten.