JavaRush /Java блог /Random UA /Створення системи моніторингу цін на авіаквитки: покроков...
Roman Beekeeper
35 рівень

Створення системи моніторингу цін на авіаквитки: покрокове керівництво [Частина 2]

Стаття з групи Random UA
Створення системи моніторингу цін на авіаквитки: покрокове керівництво [Частина 1]

Зміст

Створення системи моніторингу цін на авіаквитки: покрокове керівництво [Частина 2] - 1

Налаштовуємо 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, яка визначає, з якою таблицею з'єднуватиметься ця сутність.
Далі потрібно налаштувати H2 на проекті. На щастя, залежність вже маємо: нам потрібно написати простенький скрипт для створення таблиці і додати налаштування в application.properties файлі. Повний опис як додати H2 .

Витримка з книги Spring in Action 5th edition:
schema.sql — якщо файл намічений schema.sql у вікні application's classpath, тоді SQL в цьому файлі буде executed against the database when the application starts.

Spring Boot зрозуміє, що файл з ім'ям schema.sql у правильному місці означатиме, що він потрібен для бази даних. Як файл зі схемою БД помістимо його в корінь main/resources : schema.sql
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 — дані, які можна оновлювати у передплаті.
У Create, Update DTO не потрапабо такі поля як ID, minPrice, тому що користувач має доступ до них тільки на читання. ID визначає базу даних, а minPrice отримуємо від запиту на Skyscanner API. І реалізація цього сервісу, SubscriptionServiceImpl:
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);
               }
           });
       }
    }
і все можна використовувати. Тепер кожні 30 хвабон (ця цифра задана в SchedulerTasks) відбуватиметься перерахунок minPrice без нашої участі. Якщо ціна стане меншою, повідомлення буде надіслано користувачу та збережено у базі даних. Це буде відбуватися daemon потоком.

Додаємо Swagger та Swagger UI у додаток

Перш ніж написати контролери, додамо Swagger та Swagger UI. Чудова стаття на цю тему.

Swagger – це програмне середовище з відкритим вихідним кодом, що спирається на велику екосистему інструментів, яка допомагає розробникам проектувати, створювати, документувати та використовувати веб-сервіси RESTful.

Swagger UI надає веб-сторінку з інформацією про REST API, щоб показати, які запити, які дані приймає, а які повертає. Додати описи до полів, показати приклади полів, які очікує на додаток. Також за допомогою Swagger UI можна надсилати запити до програми без допомоги сторонніх інструментів. Це важлива частина, оскільки жодного front-end не передбачається. Розібралися для чого, тепер як це додати:
  • додаємо дві залежності в 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();
       }
    }
І все: тепер при запуску програми, якщо перейти за посиланням swagger-ui.html , буде видно всю інформацію про контролерів. Так як жодного фронтенду в додатку немає, при попаданні на головну сторінку буде помилка. Додамо редирект із головної сторінки на Swagger UI. Для цього ye;ty ще один клас конфігурації WebConfig:
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]
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ