Skip to content

Files

Latest commit

 

History

History

Spring_part_16

Spring Boot lessons part 16 - Web Starter - Part 2

В папке DOC sql-скрипты и др. полезные файлы.

Док. (ссылки) для изучения:


Для начала проведем предварительную подготовку (первые 6-ть шагов из предыдущих частей, некоторые пропущены):

Шаг 1. - в файле build.gradle добавим необходимые plugin-ы:

/* 
   Плагин Spring Boot добавляет необходимые задачи в Gradle 
   и имеет обширную взаимосвязь с другими plugin-ами.
*/
id 'org.springframework.boot' version '3.1.3'
/* 
   Менеджер зависимостей позволяет решать проблемы несовместимости 
   различных версий и модулей Spring-а
*/
id "io.spring.dependency-management" version '1.0.11.RELEASE'
/* Подключим Lombok */
id "io.freefair.lombok" version "8.3"

Шаг 2. - подключаем Spring Boot starter:

/* 
   Подключим Spring Boot Starter он включает поддержку 
   авто-конфигурации, логирование и YAML
*/
implementation 'org.springframework.boot:spring-boot-starter'

Шаг 3. - подключаем блок тестирования (Spring Boot Starter Test) (он будет активен на этапе тестирования):

testImplementation 'org.springframework.boot:spring-boot-starter-test'

Шаг 4. - автоматически Gradle создал тестовую зависимость на Junit5 (мы можем использовать как Junit4, так и TestNG):

test {
    useJUnitPlatform()
}

Шаг 5. - подключим блок для работы с БД (Spring Boot Starter Data Jpa):

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

Для работы с PostgreSQL подключим и его зависимости:

implementation 'org.postgresql:postgresql'

Шаг 6. - Для использования средств подобных Hibernate ENVERS:

implementation 'org.springframework.data:spring-data-envers'

Шаг 7. - Подключим миграционный фреймворк Liquibase:

implementation 'org.liquibase:liquibase-core'

Шаг 8. - Подключаем Wed - Starter:

implementation 'org.springframework.boot:spring-boot-starter-web'

Шаг 9. - Подключим Jasper, пока мы не используем Thymeleaf:

implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'

Lesson 77 - CRUD - API Design на уровне Controller.

Существует некий негласный стандарт или Best Practices при разработке своего API (естественно необязательный, но все же лучше его придерживаться), для примера можно рассмотреть вариант предложенный в статье: RESTful API Design. или DOC/API_Design/API_Design_Best_Practices.txt.

Попробуем реализовать 'нашу API' согласно предложенному материалу. Создадим еще один контроллер для работы с сущностями User, который будет реализовывать CRUD операции - UserController.java. Для создания и редактирования полей User-ов нам нужен DTO - UserCreateEditDto.java.

Еще раз освежим в памяти 'послойную схему MVC'.


Lesson 78 - CRUD - API Design на уровне Service.

На уровне контроллеров мы создали каркас методов реализующих наш API для класса User - UserController.java. Теперь нам нужно реализовать уровень сервисов, т.е. его CRUD методы - UserService.java:

  • На уровне сервисов у нас уже есть UserService доставшийся нам с первых уроков. Изменим его (естественно если бы его не было мы бы его создали).

  • Дополним полями UserReadDto.java, тот, что мы создали на прошлом уроке;

  • Добавим к 'id' еще один принимаемый параметр в CompanyReadDto.java, это 'name' - название компании;

  • Временно подправим методы в других классах (и в тестах, в том числе) на которые влияет обновленный CompanyReadDto, просто в параметры вместо названия компании внесем пока 'null'.

  • Нам понадобятся преобразователи одного класса в другой, или 'мапперы' - mappers, для примера можно вернуться к урокам по HTTP_Servlets_Java_EE/MVCPractice/. Для этого создаем папку mapper и сразу основной интерфейс Mapper<F, T>, в дальнейшем, все классы 'мапперы' будут реализовывать его методы *.map().

  • Создаем первый маппер UserReadMapper.java в котором метод *.map() см. ниже, преобразует объект User в UserReadDto:

      @Override
      public UserReadDto map(User object) {
          return new UserReadDto(
                    object.getId(),
                    object.getUsername(),
                    object.getBirthDate(),
                    object.getFirstname(),
                    object.getLastname(),
                    object.getRole(),
                    company
          );
      }
    

И вот тут вместо компании, в качестве параметра наш UserReadDto получает CompanyReadDto, а значит нам понадобится еще один преобразователь.

  • Создаем второй маппер-преобразователь CompanyReadMapper.java, который реализуя метод *.map() и из Company сделает CompanyReadDto.
  • Оба маппера мы аннотируем как компонент Spring или @Component. Еще раз вспомним, что @Component в Spring была создана для того, чтобы избавить разработчиков от необходимости определения bean-ов в XML. Вместо этого, Spring может автоматически находить классы с этой аннотацией при сканировании classpath и автоматически регистрировать их как bean-ы.
  • Теперь у нас появилась возможность внедрять наши классы мапперы туда куда это необходимо.
  • В текущей ситуации нам в UserReadMapper нужно инжектировать (внедрить) CompanyReadMapper, что мы и делаем см. код. UserReadMapper.java.

Теперь в нашем UserService мы можем использовать наш UserReadMapper.

  • У нас уже есть UserRepository аннотированный как @Repository и он уже внедрен в UserService. Теперь для реализации метода *.findAll() у нас все есть, прописываем его код.
  • Возвращаемся на уровень контроллеров - в наш UserController и придаем методу *.findAll(Model model) рабочий вид.

И так первый метод на уровне контроллера и сервиса реализован, продолжаем, теперь займемся методом *.findById():

Теперь займемся созданием User-a или реализуем метод *.create():

ТЕПЕРЬ НУЖНО ВСПОМНИТЬ О ТРАНЗАКЦИЯХ!

Поскольку мы делаем запросы к БД, то мы используем транзакции. Пока на уровне сервисов мы их не использовали, или явно не задействовали. Основная их часть приходится на уровень репозитория или UserRepository.java. Однако на уровне сервисов могут проходить запросы, которые могут подтягивать lazy initialization сущности, что может привести к броску исключения если такую сущность не подтянуть из базы при запросе. Поэтому на уровне сервисов у нас тоже нужны транзакции. Тогда транзакция будет открываться в момент обращения к методу и закрываться автоматически после его завершения. Это можно реализовать при помощи аннотации @Transactional над всеми классами слоя сервисов, у нас это пока UserService.java. Так же следует отметить, что правила хорошего тона рекомендуют в параметрах аннотации передавать 'readOnly = true'.

Поскольку на уровне сервисов у нас есть методы в которых мы не просто читаем информацию, а вносим ее, изменяем, именно их (помимо самого класса) мы отдельно (каждый, где необходимо) аннотируем той же @Transactional, но уже без параметров, см. UserService.java. Это позволит вносить изменения без бросков исключений в методах типа *.create(), *.edit() и т.д.

И так с транзакциями разобрались, продолжаем дорабатывать слой сервисов и контроллеров, реализуем метод *.update():

  • Поскольку мы обращаемся к БД (к User) по ID, то снова можем столкнуться с ситуацией когда не нашли требуемую запись и значит должны вернуть (отобразить) Optional объект - Optional см. UserService.java в методе *.update().
  • На уровне контроллеров у нас может быть ситуация, когда мы отображаем изменения внесенные в данные user-a по введенному ID (в случае если он есть) или возвращаем статус HTTP 404 - NOT FOUND (в случае если user с нужным ID не найден), что мы и реализуем см. *.update() в UserController.java.

Теперь займемся удалением user-ов, это метод *.delete():

  • На уровне сервисов данный метод однозначно транзакционный и вносит изменения в БД, по-этому он помечается аннотацией @Transactional. Для того чтобы на уровне контроллеров иметь выбор и возможность вернуть страницу отображения или страницу с кодом 404 - NOT FOUND (в случае успеха или неуспеха соответственно) данный метод *.delete() на уровне сервисов возвращает булеву переменную.
  • На уровне контроллеров есть свой одноименный метод *.delete(), который обрабатываем результат работы метода удаляющего user-a на уровне сервисов и возвращает результат удаления в качестве страницы отображения.

См. коды HTTP статусов: DOC/HTTP_Status См. док.:


Lesson 79 - Тестирование разработанного CRUD API на уровне СЕРВИСОВ (Service).

Небольшое напоминание. Мы используем БД развернутую в Docker контейнере, настройки доступа к ней прописаны в файле application.yml (путь к базе, логин и пароль), настраиваем нашу IDE. Теперь, в разделе Test, создаем файл для проведения интеграционных тестов - UserServiceIT.java, данный класс будет наследником IntegrationTestBase.

На уровне сервисов у нас 4-и основных метода (создать запись - CREATE, найти запись - READ, изменить запись - UPDATE, удалить запись - DELETE) отвечающих стандарту и естественно есть и другие расширяющие возможности нашего WEB приложения, пока всех методов 5-ть. Это значит, что в разделе тестов у нас будет как минимум 5-ть тестов для проверки наших CRUD операций. Хотя все тесты помечены аннотацией @Test и нынче не требуется в названии тестового метода явно прописывать слово Test (как это было в JUnit 4), все же пропишем его, чтобы визуально отделять методы из разных классов (слоев, разделов).

См. комментарии в: src\test\java\spring\oldboy\integration\service\UserServiceIT.java См. док.:


Lesson 80 - Тестирование разработанного CRUD API на уровне КОНТРОЛЛЕРОВ (Controller).

Для начала тестирования слоя контроллеров в нашем приложении создадим UserControllerIT.java, в нем и будут находиться тестовые методы. Тут нам придется имитировать запрос по HTTP протоколу и значит мы будем использовать инструментарий Mockito. Для авто-конфигурирования Mockito мы помечаем наш тестовый класс аннотацией @AutoConfigureMockMvc.

Особенность пакета Spring-Boot-Autoconfigure-API в том, что он содержит большой набор инструментов для тестирования отдельных частей (слоев) нашего приложения, а самое главное фреймворков, которые мы можем применять при разработке нашего сервиса, что значительно экономит время.

Теперь в нашем тестовом классе UserControllerIT появилась возможность внедрить объект класса MockMvc и использовать его возможности см. код класса, комментарии и док. DOC/MockMvc.

Мы уже отмечали, что на уровне контроллеров у нас открываются транзакции, но по-хорошему так делать не надо, для этого мы внесем изменения в файл свойств - сделаем параметр open-in-view как false в свойствах JPA (см. application.yml):

# Настроим свойства Hibernate
jpa:
  properties.hibernate:
    batch_size: 50
    fetch_size: 50
    show_sql: true
    format_sql: true
    hbm2ddl.auto: validate
  open-in-view: false

Это позволит открывать транзакции только на уровне сервисов, это более правильный подход.

См. комментарии в: src\test\java\spring\oldboy\integration\http\controller\UserControllerIT.java См.док.:


Lesson 81 - Конвертор дат.

При тестировании нашего слоя контроллеров мы столкнулись с проблемой отправки дат, причем явно эта проблема обрисовалась только на этапе тестирования в методе *.createControllerTest(), где мы имитируем HTTP POST запрос и передаем UserCreateEditDto полем которого является LocalDate birthDate. Для решения этой задачи существует 3-и решения:

  • Решение 1: Настройка файла свойств application.yml добавим в раздел spring подраздел format:

    spring:
      mvc:
        view:
          prefix: /WEB-INF/jsp/
          suffix: .jsp
      format:
        date: iso  
    

В параметр date устанавливаем значение iso. Теперь при тестировании нашего слоя контроллеров мы сможем передавать даты, либо (просто времени) сочетание даты и времени. И обычно рекомендуется использовать данный способ, как состыкованный с авто-конфигурацией Spring-a.

  • Решение 2: Данный вариант может понадобиться, когда клиент отправляет специальным образом отформатированную дату. Для этого мы используем аннотацию @DateTimeFormat(pattern = "yyyy-MM-dd") из пакета org.springframework.format.annotation. Она ставится над одним из полей, которое требует специального преобразования см. UserCreateEditDto.java, в параметрах передается паттерн преобразования полученных данных.

  • Решение 3: Сей вариант тяжел и мощен, т.к. требует вмешательство в конфигурацию Spring-a и создания своих конфигурационных классов, в нашем случае - WebConfiguration.java. В нем мы переопределим *.addFormatters() метод, интерфейса WebMvcConfigurer см. WebMvcConfigurer.txt.

В принципе все три решения реализованные одновременно будут работать, но лучше выбрать первый вариант.


См. официальные Guides:

  • Getting Started Guides - Эти руководства, рассчитанные на 15–30 минут, содержат быстрые практические инструкции по созданию «Hello World» для любой задачи разработки с помощью Spring. В большинстве случаев единственными необходимыми требованиями являются JDK и текстовый редактор.
  • Topical Guides - Тематические руководства предназначенные для прочтения и понимания за час или меньше, содержит более широкий или субъективный контент, чем руководство по началу работы.
  • Tutorials - Эти учебники, рассчитанные на 2–3 часа, обеспечивают более глубокое контекстное изучение тем разработки корпоративных приложений, что позволяет вам подготовиться к внедрению реальных решений.