Содержание
- Настраиваем H2 базу данных и создаем Subscription сущность. Слой Repository
- EmailNofiticationService: пишем сервис отправки электронных писем
- FlightPriceService — связь сервисов клиента с сервисами приложения
- Создаем SubscriptionService и CRUD операции в нём
- Пишем Spring Scheduler для проверки состояния билетов
- Добавляем Swagger и Swagger UI в приложение
Настраиваем H2 базу данных и создаем Subscription сущность. Слой Repository
Нужно хранить состояния подписок на стороне приложения, чтобы знать, кому отправлять уведомления о снижении цены. Для этого я выбрал объект со всеми необходимыми данными, которые нужны — Subscription. Воспользуемся аннотациями JPA (java persistence api), получим:
import java.io.Serializable;
import java.time.LocalDate;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Data;
import lombok.ToString;
@Data
@Entity
@Table(name = "subscription")
public class Subscription implements Serializable {
private static final long serialVersionUID = 1;
@Id
@GeneratedValue
private Long id;
@Column(name = "email")
private String email;
@Column(name = "country")
private String country;
@Column(name = "currency")
private String currency;
@Column(name = "locale")
private String locale;
@Column(name = "origin_place")
private String originPlace;
@Column(name = "destination_place")
private String destinationPlace;
@Column(name = "outbound_partial_date")
private LocalDate outboundPartialDate;
@Column(name = "inbound_partial_date")
private LocalDate inboundPartialDate;
@Column(name = "min_price")
private Integer minPrice;
}
где мы указали уже известную нам @Data.
Помимо ее есть новые:
- @Entity — аннотация из JPA, которая говорит, что это будет сущность из базы данных;
- @Table(name = “subscription”) — также из JPA, которая определяет, с какой таблицей будет соединяться эта сущность.
Выдержка из книги Spring in Action 5th edition: |
DROP TABLE IF EXISTS subscription;
CREATE TABLE subscription (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(250) NOT NULL,
country VARCHAR(250) NOT NULL,
currency VARCHAR(250) NOT NULL,
locale VARCHAR(250) NOT NULL,
origin_place VARCHAR(250) NOT NULL,
destination_place VARCHAR(250) NOT NULL,
outbound_partial_date DATE NOT NULL,
min_price INT,
inbound_partial_date DATE
)
Далее из гайда, который я привел выше, берем следующие настройки:
# H2
spring.h2.console.enabled=true
spring.h2.console.settings.web-allow-others=true
spring.datasource.url=jdbc:h2:mem:subscriptiondb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=flights
spring.datasource.password=flights
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
Добавим SubscriptionRespository — интерфейс, при помощи которого будет общаться с БД из Java-кода.
import com.github.romankh3.flightsmonitoring.repository.entity.Subscription;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SubscriptionRepository extends JpaRepository<Subscription, Long> {
List<Subscription> findByEmail(String email);
}
Со Spring Data этого вполне достаточно. JpaRepository имеет набор необходимых методов для нашей работы. Метод findByEmail(String email) создан как дополнительный, который нужен помимо стандартного набора. Прелесть Spring Data в том, что правильно написанного имени метода хватает, чтоб Spring уже сам все сделал без нашей реализации.
Аннотация @Repository нужна для того, чтобы потом можно было этот интерфейс инъектировать в другие классы.
И всё…) Вот так просто работать со Spring Data.
EmailNofiticationService: пишем сервис отправки электронных писем
Чтобы подписчики получали уведомления, необходимо добавить отправку писем на почту. Здесь можно выбрать и другой путь, например, отправку через мессенджеры. Из опыта использования gmail почты для отправки уведомлений скажу, что нужно сделать еще дополнительную настройку в аккаунте и отключить двойную авторизацию, чтобы все работало нормально. Вот хорошее решение. Если работаешь со Spring Boot, велика вероятность, что с задачей которую нужно реализовать, уже решили и можно воспользоваться этим. А именно, Spring boot starter mail. Зависимость уже добавили еще при создании проекта, поэтому добавляем необходимые настройки в application.properties как указано в статье.
# Spring Mail
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=IMAIL
spring.mail.password=PASSWORD
# Other properties
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.connectiontimeout=5000
spring.mail.properties.mail.smtp.timeout=5000
spring.mail.properties.mail.smtp.writetimeout=5000
# TLS , port 587
spring.mail.properties.mail.smtp.starttls.enable=true
и собственно сервис. Нужно отправлять уведомление о регистрации подписки и об уменьшении цены на перелет:
import com.github.romankh3.flightsmonitoring.repository.entity.Subscription;
/**
* Sends email notification.
*/
public interface EmailNotifierService {
/**
* Notifies subscriber, that the minPrice has decreased.
*
* @param subscription the {@link Subscription} object.
* @param oldMinPrice minPrice before recount.
* @param newMinPrice minPrice after recount.
*/
void notifySubscriber(Subscription subscription, Integer oldMinPrice, Integer newMinPrice);
/**
* Notifies subscriber, that subscription has added.
*
* @param subscription the {@link Subscription} object.
*/
void notifyAddingSubscription(Subscription subscription);
}
и реализация:
import com.github.romankh3.flightsmonitoring.repository.entity.Subscription;
import com.github.romankh3.flightsmonitoring.service.EmailNotifierService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
/**
* {@inheritDoc}
*/
@Slf4j
@Service
public class EmailNotifierServiceImpl implements EmailNotifierService {
@Autowired
private JavaMailSender javaMailSender;
/**
* {@inheritDoc}
*/
@Override
public void notifySubscriber(Subscription subscription, Integer oldMinPrice, Integer newMinPrice) {
log.debug("method notifySubscriber STARTED");
SimpleMailMessage msg = new SimpleMailMessage();
msg.setTo(subscription.getEmail());
msg.setSubject("Flights Monitoring Service");
msg.setText(String.format("Hello, dear! \n "
+ "the price for your flight has decreased \n"
+ "Old min price = %s,\n new min price = %s,\n Subscription details = %s", oldMinPrice, newMinPrice, subscription.toString()));
javaMailSender.send(msg);
log.debug("method notifySubscriber FINISHED");
}
/**
* {@inheritDoc}
*/
@Override
public void notifyAddingSubscription(Subscription subscription) {
log.debug("method notifyAddingSubscription STARTED");
SimpleMailMessage msg = new SimpleMailMessage();
msg.setTo(subscription.getEmail());
msg.setSubject("Flights Monitoring Service");
msg.setText(String.format("Hello, dear! \n "
+ "Subscription has been successfully added. \n"
+ "Subscription details = %s", subscription.toString()));
javaMailSender.send(msg);
log.debug("method notifyAddingSubscription FINISHED");
}
}
FlightPriceService — связь сервисов клиента с сервисами приложения
Чтобы связать работу нашего FlightPricesClient и сервиса для обработки подписок, нужно создать сервис, который будет на основании Subscription объекта выдавать полную информацию о рейсе с минимальной стоимостью. Для этого есть FlightPriceService:
import com.github.romankh3.flightsmonitoring.client.dto.FlightPricesDto;
import com.github.romankh3.flightsmonitoring.repository.entity.Subscription;
/**
* Service, for getting details based on {@link Subscription} object.
*/
public interface FlightPriceService {
/**
* Finds minPrice based on {@link Subscription}.
*
* @param flightPricesDto provided {@link FlightPricesDto} object.
* @return
*/
Integer findMinPrice(FlightPricesDto flightPricesDto);
/**
* Finds all the flight data related to {@link Subscription} object.
*
* @param subscription provided {@link Subscription} object
* @return {@link FlightPricesDto} with all the data related to flight specific in {@link Subscription}.
*/
FlightPricesDto findFlightPrice(Subscription subscription);
}
и реализация:
import com.github.romankh3.flightsmonitoring.client.dto.FlightPricesDto;
import com.github.romankh3.flightsmonitoring.client.service.FlightPricesClient;
import com.github.romankh3.flightsmonitoring.repository.entity.Subscription;
import com.github.romankh3.flightsmonitoring.service.FlightPriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* {@inheritDoc}
*/
@Service
public class FlightPriceServiceImpl implements FlightPriceService {
@Autowired
private FlightPricesClient flightPricesClient;
/**
* {@inheritDoc}
*/
@Override
public Integer findMinPrice(FlightPricesDto flightPricesDto) {
return flightPricesDto.getQuotas().get(0).getMinPrice();
}
/**
* {@inheritDoc}
*/
@Override
public FlightPricesDto findFlightPrice(Subscription subscription) {
if (subscription.getInboundPartialDate() == null) {
return flightPricesClient
.browseQuotes(subscription.getCountry(), subscription.getCurrency(), subscription.getLocale(),
subscription.getOriginPlace(), subscription.getDestinationPlace(),
subscription.getOutboundPartialDate().toString());
} else {
return flightPricesClient
.browseQuotes(subscription.getCountry(), subscription.getCurrency(), subscription.getLocale(),
subscription.getOriginPlace(), subscription.getDestinationPlace(),
subscription.getOutboundPartialDate().toString(), subscription.getInboundPartialDate().toString());
}
}
}
Здесь у нас есть два метода: один возвращает полную информацию о полете с минимальной ценой, а другой принимает эту информацию и выдает значение минимальной цены. Это можно было бы проделывать каждый раз и с результатом, но я считаю, что так удобнее использовать.
Создаем SubscriptionService и CRUD операции в нём
Для полного управления подписками нужно создать сервис с CRUD операциями. CRUD расшифровывается как create, read, update, delete. То есть нужно уметь создавать подписку, считать ее по ID, отредактировать и удалить. С одной лишь разницей, что получать подписки будем не по ID, а по email, так как нам нужно именно это. Ведь зачем нам подписки по непонятному ID, а вот все подписки пользователя по его почте реально нужны. Итак SubscriptionService:
import com.github.romankh3.flightsmonitoring.repository.entity.Subscription;
import com.github.romankh3.flightsmonitoring.rest.dto.SubscriptionCreateDto;
import com.github.romankh3.flightsmonitoring.rest.dto.SubscriptionDto;
import com.github.romankh3.flightsmonitoring.rest.dto.SubscriptionUpdateDto;
import java.util.List;
/**
* Manipulates with subscriptions.
*/
public interface SubscriptionService {
/**
* Add new subscription.
* @param dto the dto of the subscription.
*/
SubscriptionDto create(SubscriptionCreateDto dto);
/**
* Get all subscription based on email.
*
* @param email provided email;
* @return the collection of the {@link SubscriptionDto} objects.
*/
List<SubscriptionDto> findByEmail(String email);
/**
* Remove subscription based on it ID
*
* @param subscriptionId the ID of the {@link Subscription}.
*/
void delete(Long subscriptionId);
/**
* Update subscription based on ID
*
*
* @param subscriptionId the ID of the subscription to be updated.
* @param dto the data to be updated.
* @return updated {@link SubscriptionDto}.
*/
SubscriptionDto update(Long subscriptionId, SubscriptionUpdateDto dto);
}
В аргументах можно заметить три новых DTO объекта:
- SubscriptionDto — содержит всю информацию для показа;
- SubscriptionCreateDto — данные для создания подписки;
- SubscriptionUpdateDto — данные, которые можно обновлять в подписке.
import com.github.romankh3.flightsmonitoring.client.dto.FlightPricesDto;
import com.github.romankh3.flightsmonitoring.repository.SubscriptionRepository;
import com.github.romankh3.flightsmonitoring.repository.entity.Subscription;
import com.github.romankh3.flightsmonitoring.rest.dto.SubscriptionCreateDto;
import com.github.romankh3.flightsmonitoring.rest.dto.SubscriptionDto;
import com.github.romankh3.flightsmonitoring.rest.dto.SubscriptionUpdateDto;
import com.github.romankh3.flightsmonitoring.service.EmailNotifierService;
import com.github.romankh3.flightsmonitoring.service.FlightPriceService;
import com.github.romankh3.flightsmonitoring.service.SubscriptionService;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.stereotype.Service;
/**
* {@inheritDoc}
*/
@Slf4j
@Service
public class SubscriptionServiceImpl implements SubscriptionService {
@Autowired
private SubscriptionRepository subscriptionRepository;
@Autowired
private FlightPriceService flightPriceService;
@Autowired
private EmailNotifierService emailNotifierService;
/**
* {@inheritDoc}
*/
@Override
public SubscriptionDto create(SubscriptionCreateDto dto) {
Subscription subscription = toEntity(dto);
Optional<Subscription> one = subscriptionRepository.findOne(Example.of(subscription));
if (one.isPresent()) {
log.info("The same subscription has been found for Subscription={}", subscription);
Subscription fromDatabase = one.get();
FlightPricesDto flightPriceResponse = flightPriceService.findFlightPrice(subscription);
subscription.setMinPrice(flightPriceService.findMinPrice(flightPriceResponse));
return toDto(fromDatabase, flightPriceResponse);
} else {
FlightPricesDto flightPriceResponse = flightPriceService.findFlightPrice(subscription);
subscription.setMinPrice(flightPriceService.findMinPrice(flightPriceResponse));
Subscription saved = subscriptionRepository.save(subscription);
log.info("Added new subscription={}", saved);
emailNotifierService.notifyAddingSubscription(subscription);
return toDto(saved, flightPriceResponse);
}
}
/**
* {@inheritDoc}
*/
@Override
public List<SubscriptionDto> findByEmail(String email) {
return subscriptionRepository.findByEmail(email).stream()
.map(subscription -> {
FlightPricesDto flightPriceResponse = flightPriceService.findFlightPrice(subscription);
if (subscription.getMinPrice() != flightPriceService.findMinPrice(flightPriceResponse)) {
subscription.setMinPrice(flightPriceService.findMinPrice(flightPriceResponse));
subscriptionRepository.save(subscription);
}
return toDto(subscription, flightPriceResponse);
})
.collect(Collectors.toList());
}
/**
* {@inheritDoc}
*/
@Override
public void delete(Long subscriptionId) {
subscriptionRepository.deleteById(subscriptionId);
}
/**
* {@inheritDoc}
*/
@Override
public SubscriptionDto update(Long subscriptionId, SubscriptionUpdateDto dto) {
Subscription subscription = subscriptionRepository.getOne(subscriptionId);
subscription.setDestinationPlace(dto.getDestinationPlace());
subscription.setOriginPlace(dto.getOriginPlace());
subscription.setLocale(dto.getLocale());
subscription.setCurrency(dto.getCurrency());
subscription.setCountry(dto.getCountry());
subscription.setEmail(dto.getEmail());
subscription.setOutboundPartialDate(dto.getOutboundPartialDate());
subscription.setInboundPartialDate(dto.getInboundPartialDate());
FlightPricesDto flightPriceResponse = flightPriceService.findFlightPrice(subscription);
subscription.setMinPrice(flightPriceService.findMinPrice(flightPriceResponse));
return toDto(subscriptionRepository.save(subscription), flightPriceResponse);
}
private Subscription toEntity(SubscriptionCreateDto dto) {
Subscription subscription = new Subscription();
subscription.setCountry(dto.getCountry());
subscription.setCurrency(dto.getCurrency());
subscription.setDestinationPlace(dto.getDestinationPlace());
subscription.setInboundPartialDate(dto.getInboundPartialDate());
subscription.setOutboundPartialDate(dto.getOutboundPartialDate());
subscription.setLocale(dto.getLocale());
subscription.setOriginPlace(dto.getOriginPlace());
subscription.setEmail(dto.getEmail());
return subscription;
}
private SubscriptionDto toDto(Subscription entity, FlightPricesDto response) {
SubscriptionDto dto = new SubscriptionDto();
dto.setEmail(entity.getEmail());
dto.setCountry(entity.getCountry());
dto.setCurrency(entity.getCurrency());
dto.setLocale(entity.getLocale());
dto.setOriginPlace(entity.getOriginPlace());
dto.setDestinationPlace(entity.getDestinationPlace());
dto.setOutboundPartialDate(entity.getOutboundPartialDate());
dto.setInboundPartialDate(entity.getInboundPartialDate());
dto.setMinPrice(entity.getMinPrice());
dto.setId(entity.getId());
dto.setFlightPricesDto(response);
return dto;
}
}
Пишем Spring Scheduler для проверки состояния билетов
Чтобы знать, изменилась ли цена на авиабилеты, нужно время от времени проводить запросы по созданным подпискам и проверять с сохраненным состоянием минимальной цены, которая лежит в базе данных. Для этого есть Spring Scheduler, который и поможет нам с этим. Вот отличное описание. Как и всё во Spring, не нужно много действий:Добавляем аннотацию @EnableScheduling;
Создаем SchedulerTasks объект и помещаем его в Application Context
import com.github.romankh3.flightsmonitoring.service.RecountMinPriceService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Slf4j @Component public class SchedulerTasks { @Autowired private RecountMinPriceService recountMinPriceService; private static final long TEN_MINUTES = 1000 * 60 * 10; @Scheduled(fixedRate = TEN_MINUTES) public void recountMinPrice() { log.debug("recount minPrice Started"); recountMinPriceService.recount(); log.debug("recount minPrice finished"); } }
Пишем RecountMinPriceService, который будет выполнять всю логику:
/** * Recounts minPrice for all the subscriptions. */ public interface RecountMinPriceService { /** * Recounts minPrice for all the subscriptions. */ void recount(); }
и реализация:
import com.github.romankh3.flightsmonitoring.client.dto.FlightPricesDto; import com.github.romankh3.flightsmonitoring.repository.SubscriptionRepository; import com.github.romankh3.flightsmonitoring.service.EmailNotifierService; import com.github.romankh3.flightsmonitoring.service.FlightPriceService; import com.github.romankh3.flightsmonitoring.service.RecountMinPriceService; import java.time.LocalDate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * {@inheritDoc} */ @Service public class RecountMinPriceServiceImpl implements RecountMinPriceService { @Autowired private SubscriptionRepository subscriptionRepository; @Autowired private FlightPriceService flightPriceService; @Autowired private EmailNotifierService emailNotifierService; //todo add async /** * {@inheritDoc} */ @Override public void recount() { subscriptionRepository.findAll().forEach(subscription -> { if(subscription.getOutboundPartialDate().isAfter(LocalDate.now().minusDays(1))) { FlightPricesDto flightPricesDto = flightPriceService.findFlightPrice(subscription); Integer newNumPrice = flightPriceService.findMinPrice(flightPricesDto); if (subscription.getMinPrice() > newNumPrice) { emailNotifierService.notifySubscriber(subscription, subscription.getMinPrice(), newNumPrice); subscription.setMinPrice(newNumPrice); subscriptionRepository.save(subscription); } } else { subscriptionRepository.delete(subscription); } }); } }
Добавляем Swagger и Swagger UI в приложение
Прежде чем написать контроллеры, добавим Swagger и Swagger UI. Отличная статья на эту тему.Swagger — это программная среда с открытым исходным кодом, опирающаяся на обширную экосистему инструментов, которая помогает разработчикам проектировать, создавать, документировать и использовать веб-сервисы RESTful. |
добавляем две зависимости в pom.xml
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>
Новый класс для конфигурации SwaggerConfig, с настройками того, что будет показываться на UI (user interface).
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage("com.github.romankh3.flightsmonitoring.rest.controller")) .paths(PathSelectors.any()) .build(); } }
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web configuration class.
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:/swagger-ui.html");
}
}
Создание системы мониторинга цен на авиабилеты: пошаговое руководство [Часть 3]
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ