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の動作を確認することです。