Redditアプリケーションの5回目の改良

1概要

Redditアプリケーションを次のリンクから進めていきましょう。

2コメント投稿時にEメール通知を送信する

Redditには電子メール通知がありません - わかりやすく単純です。私が見たいのは - 誰かが私の投稿の1つにコメントするときはいつでも、私はコメントが付いている短い電子メール通知を受け取ります。

つまり、この機能の目的はここにあります。コメントに関する電子メール通知です。

以下をチェックする簡単なスケジューラを実装します。

  • 投稿の返信付きの電子メール通知を受け取るユーザー

  • ユーザーが投稿をRedditの受信トレイに投稿した場合

それはそれから単に未読の投稿の返信と一緒にEメール通知を送ります。

2.1. ユーザー設定

まず、次のものを追加して、PreferenceエンティティとDTOを変更する必要があります。

private boolean sendEmailReplies;

投稿の返信付きの電子メール通知を受け取るかどうかをユーザーが選択できるようにする

2.2. 通知スケジューラ

次に、これが簡単なスケジューラです。

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

スケジューラは1時間ごとに実行されることに注意してください。ただし、必要に応じて、はるかに短いリズムで進むこともできます。

2.3. 通知サービス

それでは、通知サービスについて説明しましょう。

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

ご了承ください:

  • 私たちはReddit APIを呼び出してすべての返事を得て、それからそれらを一つずつチェックします

それが新しい「未読」かどうかを確認してください。

  • 未読の返信があると、このユーザーに送信するイベントが発生します。

電子メール通知。

2.4. 新しい返信イベント

これが私たちの簡単なイベントです。

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. 返信リスナ

最後に、これが私たちのリスナーです。

@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セッション同時実行制御

次に、アプリケーションで許可されている同時セッション数に関するより厳密なルールを設定しましょう。もっと端的に言えば - 同時セッションを許可しない :

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

カスタム UserDetails 実装を使用しているので、セッションコントロール戦略はすべてのプリンシパルをマップに格納し、それらを取得できるようにする必要があるため、 equals() および hashcode() をオーバーライドする必要があります。

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個別のAPIサーブレット

アプリケーションは、同じサーブレットからフロントエンドとAPIの両方を処理しています。これは理想的ではありません。

それでは、これら2つの主要な責任を分けて、それらを 2つの異なるサーブレット に分けてみましょう。

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

フロントエンド要求をすべて処理し、フロントエンドに固有のSpringコンテキストのみをブートストラップするフロントエンドサーブレットがあることに注意してください。それから、APIサーブレットがあります - APIのためのまったく異なるSpringコンテキストをブートストラップします。

また - 非常に重要な - これら2つのサーブレットのSpringコンテキストは子コンテキストです。 SpringApplicationBuilder によって作成された親コンテキストは、永続性、サービスなどの一般的な設定について root パッケージをスキャンします。

これが私たちの 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/");
    }
}

そして 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

最後に - 私たちはRSSの扱いをより良くするつもりです。

RSSフィードは、Feedburnerなどの外部サービスを介して短縮またはリダイレクトされることがあります。そのため、アプリケーションにフィードのURLをロードするときは、メインURLに到達するまで、すべてのリダイレクトを通じてそのURLをたどる必要があります。私たちは実際に気にしています。

そのため、記事のRedditへのリンクを投稿するときには、実際に正しい元の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;
}

この実装で注意すべきことがいくつかあります。

  • 複数レベルのリダイレクトを処理しています

  • リダイレクトループを回避するために、訪問したすべてのURLも追跡しています

6. 結論

それだけです - Redditアプリケーションをより良くするためのいくつかの確かな改善。次のステップは、APIのパフォーマンステストをいくつか実行して、プロダクションシナリオにおけるAPIの動作を確認することです。