Skip to content

Latest commit

 

History

History

ReadMe.md

StepThree (Servlet API и AOP)

Основная задача проекта: Разработать приложение для управления коворкинг-пространством. Приложение должно позволять пользователям бронировать рабочие места, конференц-залы, а также управлять бронированиями и просматривать доступность ресурсов.

Т.е. все то, что было описано и проделано в:

Чтобы не прыгать по описаниям вспомним, что на первом шаге мы использовали только возможности JAVA. На втором шаге мы добавили новые технологии (но, данные все еще возвращались в консоль и взаимодействие происходило через CLI):

Функциональные и технические требования предыдущего шага:

  • Репозитории должны писать ВСЕ сущности в БД PostgreSQL (документация);
  • Идентификаторы при сохранении в БД должны выдаваться через sequence;
  • DDL-скрипты на создание таблиц и скрипты на предзаполнение таблиц должны выполняться только инструментом миграции Liquibase (Liquibase GitHub);
  • Скрипты миграции Liquibase должны быть написаны в нотации XML или YAML (SQL);
  • Скриптов миграции должно быть несколько:
    • Создание всех таблиц;
    • Предзаполнение данными;
  • Служебные таблицы должны быть в отдельной схеме;
  • Таблицы сущностей хранить в схеме public запрещено (создать свою схему);
  • В тестах необходимо использовать test-containers (short tutorial);
  • В приложении должен быть docker-compose.yml:
    • В котором должны быть прописаны инструкции для развертывания postgres БД в докере;
    • Логин, пароль к БД должны быть отличными от тех, что прописаны в образе по-умолчанию;
    • Приложение должно работать с БД, развернутой в докере с указанными параметрами;
  • Приложение должно поддерживать конфиг-файлы:
    • Всё, что относится к подключению БД;
    • Настройки и инструкции к миграциям, должно быть сконфигурировано через конфиг-файл;

Текущий проект должен содержать и реализовывать:

  • Взаимодействие с БД идет по средствам Hibernate (Hibernate Tutorial);
  • Взаимодействие с приложением должно осуществляться через отправку HTTP запросов;
  • При запросах и ответах сервлеты (servlets) должны принимать JSON и возвращать JSON;
  • Для сериализации и десериализации необходимо использовать библиотеку Jackson (статьи);
  • Использовать понятное названия эндпоинтов (endpoint);
  • Возвращать разные статус-коды (HTTP-коды);
  • Добавить DTO;
  • Для маппинга сущностей в DTO использовать MapStruct (Quick Guide);
  • Реализовать валидацию входящих DTO;
  • Реализовать аудит ключевых действий пользователя через AOP (введение в AspectJ);
  • Реализовать через AOP логирование выполнения ключевых методов (с замером времени выполнения) (short intro);
  • Покрыть сервлеты тестами (спорный момент);

Реализация:

  • Функциональные требования выполнены полностью.
  • Технические требования выполнены полностью.

Особенности:

Данная версия приложения взаимодействует с пользователем (сервисом или другим приложением) по средствам HTTP-запросов и использует в своей реализации Servlet API. Значит нам нужен или сервер приложений (Payara, GlassFish) или контейнер сервлетов (TomCat) для запуска нашего приложения. Используем TomCat. Скрипты Liquibase при первичном старте создадут необходимые предзаполненные таблицы.

Структура проекта (это не Spring проект, но слоистую структуру можно применять где угодно, отсюда и названия):

  • annotation - аннотации для аудирования и логирования;

  • aspects - аспектные классы (аудит, логирование, транзакции);

  • config:

    • connection - папка содержит классы отвечающие за связь с БД;
    • liquibase - папка с классом управляющим работой миграционного фреймворка Liquibase;
    • util - папка с конфигуратором приложения и Hibernate;
  • core:

    • controllers - классы управляющие манипуляцией с сущностями на 'верхнем' уровне приложения;
    • dto - классы для передачи данных со слоя на слой;
    • mapper - классы сопоставители;
    • model:
      • criteria.expander - класс дополнение для формирования запросов при помощи Criteria API (см. ком-ии внутри класса);
      • database:
        • audit - класс работающий с функционалом аудирования;
        • entity - основные рабочие элементы проекта с которыми происходят манипуляции (залы/рабочие места, слоты времени для резервирования, пользователи, записи о резервировании содержащие сведения о том, что, когда, и на сколько было зарезервировано);
        • repository - классы для взаимодействия с БД;
      • service - классы отвечающие за обработку запросов с уровня контроллеров;
  • exceptions - папка с исключениями;

  • filter - фильтр задающий кодировку при обращении к сервлетам;

  • security - классы для работы с JWT аутентификацией;

  • servlets - классы HTTP интерфейса;

  • validates - класс валидатор (для проверки входящих DTO);

  • AppContextBuilder - класс формирующий общий контекст приложения;

  • resources:

    • db/changelog - файлы миграции Liquibase;
    • application.properties - файл настроек (см. комментарии);
    • hibernate.cfg.xml - укороченный конфигуратор Hibernate (основная нагрузка на Java конфигурации);
    • log4j.xml - конфигурация логера;

Тесты (247 шт.) согласно расчетам IDE покрывают:

  • Классы 92% (234/252);
  • Методы 84% (742/874);
  • Строки кода 73% (2122/2904);

Тестирование проводилось в двух вариантах:

Наиболее интересными моментами в тестировании сервлетов является тот факт, что разработчики того же Mockito не рекомендуют использовать заглушки на классы, которые мы (разработчики) сами не создавали (т.е. не mock-ать чужие библиотеки, возможно я и ошибаюсь):

Remember
Do not mock types you don’t own
Don’t mock value objects
Don’t mock everything
Show love with your tests!

Любопытным показался вариант использования метода doAnswer см. тест. Пара статей по вопросу применения doAnswer.


Применение StreamAPI при работе с коллекциями:

Во всех реализациях приложения есть задача вывода свободных слотов на конкретную дату, при это не уточняется в каком формате. Мы решили выводить информацию в формате коллекции - на конкретную дату будет существовать список наших Places (рабочих мест и залов) и каждый из них будет иметь ограниченное количество слотов, которые можно забронировать (возможно останутся и свободные) - вот их мы и выводим. Если у Place (места/зала) нет резервирования на конкретную дату - все слоты свободны - логично. Реализацию данного вывода берет на себя метод *.findAllFreeSlotsByDate(LocalDate date) класса ReservationService.java. И тут полет фантазии на реализацию обширен, просто на коллекциях и циклах (многословно и запутанно) или с применением StreamAPI.

Один из вариантов (можно сравнить с текущей реализацией):

List<Reservation> reservationList =
            reservationRepository.findReservationByDate(LocalDate.of(2029, 7, 28)).get();

    Map<Long, List<Long>> allFreeSlotsByDay = new HashMap<>();

    List<Long> allAvailableSlot = slotRepository.findAll().stream()
                                                          .map(Slot::getSlotId)
                                                          .toList();

    var placeReservationMap=
            reservationList.stream()
                           .collect(Collectors.groupingBy(Reservation::getPlace))
                           .entrySet().stream()
                                      .collect(Collectors.toMap(eKey -> eKey.getKey().getPlaceId(),
                                                                eValue -> eValue.getValue().stream()
                                                                                           .map(r -> r.getSlot().getSlotId())
                                                                                           .collect(Collectors.toList())));

    placeReservationMap.forEach((key, value) -> {
        List<Long> freeSlotByPlace = new ArrayList<>(allAvailableSlot);
        value.forEach(freeSlotByPlace::remove);
        allFreeSlotsByDay.put(key, freeSlotByPlace);
    });

    System.out.println(allFreeSlotsByDay);

Параметры запросов к приложению (API)

Адрес хоста стандартный для локальной машины (порт исходя из настроек TomCat): http://localhost:8081

Естественно при развертке приложения в контейнере, или в локальном TomCat, в полном адресе запроса может появиться имя приложения (папки где оно развернуто), например cw и тогда между адресом хоста и endpoint-ом появится дополнение, например:

http://localhost:8081/cw/cw_api/v1/places/

Не забываем про это!

Запросы для работы с залами и рабочими местами (Place entity):

Тип запроса (Servlet метод) Endpoint Полный запрос (пример) Тело запроса Описание запроса
GET (doGet) /cw_api/v1/places/available http://localhost:8081/cw_api/v1/places/available Получить список всех Place-ов (залов/рабочих мест)
POST (doPost) /cw_api/v1/places/ http://localhost:8081/cw_api/v1/places/ {"species": "HALL", "placeNumber": "5"} Создать Place с заданными параметрами
PUT (doPut) /cw_api/v1/places/ http://localhost:8081/cw_api/v1/places/ {"placeId":"13", "species": "WORKPLACE", "placeNumber": "65"} Обновить выбранный Place (Место/Зал)
DELETE (doDelete) /cw_api/v1/places/ http://localhost:8081/cw_api/v1/places/ {"species": "HALL", "placeNumber": "5"} Удаление Place (Место/Зал)
GET (doGet) /cw_api/v1/places/ http://localhost:8081/cw_api/v1/places/?placeId=1 Посмотреть данные на Place (Место/Зал) по ID
GET (doGet) /cw_api/v1/places/ http://localhost:8081/cw_api/v1/places/?species=HALL&placeNumber=3 Посмотреть данные Place (Место/Зал) по виду и номеру

Запросы для работы с залами и рабочими местами (Slot entity):

Тип запроса (Servlet метод) Endpoint Полный запрос (пример) Тело запроса Описание запроса
GET (doGet) /cw_api/v1/slots/available http://localhost:8081/cw_api/v1/slots/available Получить список всех Slot-ов (отрезков времени)
POST (doPost) /cw_api/v1/slots/ http://localhost:8081/cw_api/v1/slots/ {"slotNumber": "20", "timeStart": "20:00", "timeFinish": "21:00"} Создать Slot с заданными параметрами
PUT (doPut) /cw_api/v1/slots/ http://localhost:8081/cw_api/v1/slots/ {"slotId":"19", "slotNumber": "20", "timeStart": "20:10", "timeFinish": "20:30"} Обновить выбранный Slot
DELETE (doDelete) /cw_api/v1/slots/ http://localhost:8081/cw_api/v1/slots/ {"slotNumber": "20", "timeStart": "20:00", "timeFinish": "21:00"} Удаление Slot-а
GET (doGet) /cw_api/v1/slots/ http://localhost:8081/cw_api/v1/slots/?slotId=18 Посмотреть данные Slot-a по ID
GET (doGet) /cw_api/v1/slots/ http://localhost:8081/cw_api/v1/slots/?slotNumber=28 Посмотреть данные Slot-a по его номеру
GET (doGet) /cw_api/v1/slots/free-by-date http://localhost:8081/cw_api/v1/slots/free-by-date?date=2029-07-28 Посмотреть данные Slot-a по виду и номеру

Запросы для работы с пользователем (User entity):

Тип запроса (Servlet метод) Endpoint Полный запрос (пример) Тело запроса Описание запроса
POST (doPost) /cw_api/v1/users/register http://localhost:8081/cw_api/v1/users/register {"userName": "nameOf", "password": "passOf", "role":"ADMIN"} Регистрация пользователя
POST (doPost) /cw_api/v1/users/login http://localhost:8081/cw_api/v1/users/login {"login": "nameOf", "password": "passOf"} Аутентификация пользователя в системе
GET (doGet) /cw_api/v1/users http://localhost:8081/cw_api/v1/users Получить список всех User-ов
PUT (doPut) /cw_api/v1/users http://localhost:8081/cw_api/v1/users {"userId": "1", "userName": "UserUpdate", "password": "4321End", "role":"ADMIN"} Обновить данные по User-у
DELETE (doDelete) /cw_api/v1/users http://localhost:8081/cw_api/v1/users {"userId": "4", "userName": "UserTwo", "password": "1234", "role":"USER"} Удалить User-a

Запросы для работы с системой резервирования (Reservation entity):

Тип запроса (Servlet метод) Endpoint Полный запрос (пример) Тело запроса Описание запроса
GET (doGet) /cw_api/v1/reservations/available http://localhost:8081/cw_api/v1/reservations/available Посмотреть все существующие брони
POST (doPost) /cw_api/v1/reservations/ http://localhost:8081/cw_api/v1/reservations/ {"reservationDate": "2047-08-11", "userId": "3", "placeId": "5", "slotId": "4"} Создание новой брони
PUT (doPut) /cw_api/v1/reservations/ http://localhost:8081/cw_api/v1/reservations/ {"reservationId":16, "reservationDate":"2048-03-10", "userId":2, "placeId":3, "slotId":3} Обновить данные по брони
DELETE (doDelete) /cw_api/v1/reservations/ http://localhost:8081/cw_api/v1/reservations/ {"reservationId": "10", "reservationDate": "2027-06-12", "userId": "3", "placeId": "8", "slotId": "4"} Удалить бронь
GET (doGet) /cw_api/v1/reservations/ http://localhost:8081/cw_api/v1/reservations/?userId=3 Посмотреть все брони конкретного пользователя
GET (doGet) /cw_api/v1/reservations/ http://localhost:8081/cw_api/v1/reservations/?reservationDate=2029-07-28 Посмотреть брони на конкретную дату
GET (doGet) /cw_api/v1/reservations/ http://localhost:8081/cw_api/v1/reservations/?placeId=3 Посмотреть все брони конкретного места/зала

Для тестирования приложения можно использовать Postman (простая проверка - запустилось или нет обратиться к HelloWorld странице приложения): http://localhost:8081/cw_api/hello

Приложение для аутентификации используем JWT ключ, посему не забываем его применять. После авторизации сервис (пользователь) получает его в ответ, пример:

JWT_Token_example

Далее при формировании запроса через Postman выбираем тип аутентификации и добавляем полученный JWT ключ в соответствующее поле:

Request_with_jwt_token


Применение Docker.

После разработки приложения, и тестирования его в локальном окружении, у нас появилась мысль упаковать его в Docker-образ. Более подробно процесс описан в разделе docker-practice. Рассмотрено несколько способов:

Естественно мы не забыли про docker-compose - теперь приложение и БД можно быстро развернуть практически на любой машине. Отличие текущего docker-compose файла от более старой его версии, примененной для предпоследней версии приложения, это размещение Volumes не в рабочей папке приложения запускающейся из Intellij IDEA, а размещение их внутри виртуальной машины Docker-a:

VolumeLS