1. Обзор
Давайте продолжим продвигать приложение Reddit из нашего текущего тематического исследования .
2. Отправляйте уведомления по электронной почте о комментариях к публикациям
Reddit не хватает уведомлений по электронной почте – просто и понятно. Что я хотел бы видеть – всякий раз, когда кто-то комментирует один из моих постов, я получаю короткое уведомление по электронной почте с комментарием.
Так что – проще говоря – вот цель этой функции здесь – уведомления по электронной почте о комментариях.
Мы реализуем простой планировщик, который проверяет:
- какие пользователи должны получать уведомления по электронной почте с ответами на сообщения
- если пользователь получил какие-либо ответы на сообщения в свой почтовый ящик Reddit
Затем он просто отправит уведомление по электронной почте с непрочитанными ответами на сообщения.
2.1. Предпочтения Пользователя
Во-первых, нам нужно будет изменить наш объект предпочтений и 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() { Listpreferences = preferenceRepository.findBySendEmailRepliesTrue(); for (Preference preference : preferences) { notificationRedditService.checkAndNotify(preference); } } }
Обратите внимание, что планировщик работает каждый час, но мы, конечно, можем использовать гораздо более короткую частоту, если захотим.
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{ @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 из одного и того же сервлета, что не идеально.
Давайте теперь разделим эти две основные обязанности и разделим их на два разных сервлета :
@Bean public ServletRegistrationBean frontendServlet() { ServletRegistrationBean registration = new ServletRegistrationBean(new DispatcherServlet(), "/*"); Mapparams = new HashMap (); 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 params = new HashMap (); 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- загрузка совершенно другого контекста Spring для API.
Кроме того, что очень важно, эти два контекста пружины сервлета являются дочерними контекстами. Родительский контекст, созданный SpringApplicationBuilder , сканирует корневой пакет на наличие общих конфигураций, таких как сохраняемость, служба и т. Д.
Вот наша Конфигурация веб-интерфейса :
@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 { Listvisited = new ArrayList (); 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 и посмотреть, как он ведет себя в производственном сценарии.