Створення системи моніторингу цін на авіаквитки: покрокове керівництво [Частина 1]
Spring Boot зрозуміє, що файл з ім'ям schema.sql у правильному місці означатиме, що він потрібен для бази даних. Як файл зі схемою БД помістимо його в корінь main/resources : schema.sql
Swagger UI надає веб-сторінку з інформацією про REST API, щоб показати, які запити, які дані приймає, а які повертає. Додати описи до полів, показати приклади полів, які очікує на додаток. Також за допомогою Swagger UI можна надсилати запити до програми без допомоги сторонніх інструментів. Це важлива частина, оскільки жодного front-end не передбачається. Розібралися для чого, тепер як це додати:
Зміст
- Налаштовуємо 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]
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ