Skip to content

StringerDM/bootjava

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Открытый курс для всех желающих приобщиться к живой современной разработке на Java

Java приложения на самом современном и востребованном стеке: Spring Boot 2.x, Spring Data Rest/HATEOAS, Lombok, JPA, H2, ....

Мы создадим с нуля основу любого современного REST веб-приложения: аутентификация и авторизация на основе ролей, регистрация пользователя в приложении, управление своим профилем и администрирование пользователей.

Конспект:

1. Основы Spring Boot 1.1 Создаем проект через Spring Initializer
commit: https://github.com/StringerDM/bootjava/commit/35a21d499357b464ebb5b571cb97ac0bc5e57f01

-   Подключаем зависимости:
-   Lombock
-   Spring Web
-   H2 database
-   Spring Data JPA

По умолчанию приложение открывается по адресу localhost:8080

Ссылки: 
Spring Initializrs: https://start.spring.io/

1.2	Spring Boot maven plugin. Конвертация в WAR

Ссылки:
Конвертация JAR приложения в WAR http://spring-projects.ru/guides/convert-jar-to-war-maven/

1.3	Настройка проекта
Готовый проект с патчами находится в ветке patched:   git clone --branch patched https://github.com/JavaOPs/bootjava.git

1.4 Проект Lombok

Commit: https://github.com/StringerDM/bootjava/commit/ef6cdb5d5fb182bf1387e77206ddf174ce4ed005 

В Pom.xml он уже у нас есть, причем <optional> true </optional>:
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

Если мы посмотрим, что такое optional dependencies: http://maven.apache.org/guides/introduction/introduction-to-optional-and-excludes-dependencies.html то увидим,  что оно используется для библиотек, у которых есть много транзитивных зависимостей и подключая эти библиотеки с optional мы избавляемся от их зависимостей которые нам возможно не понадобятся. У нас совсем не библиотека, а собственный проект поэтому использование optional достаточно сомнительно.

Кроме того, если мы посмотрим: Maven Scope for Lombok (Compile vs. Provided) https://stackoverflow.com/questions/29385921/548473 то увидим что в оф документации Lombok нужно подключать со скопом provided. То есть lombok на нужен только на этапе компиляции и из сборки он исключается. 

И еще одна ссылка Exclude lombok in Spring Boot https://stackoverflow.com/questions/45202639/548473 где говорится что если мы делаем JAR то туда включается embedded Tomcat и все зависимости даже со скопом provided также попадают в нашу сборку. Для того чтобы исключить lombock из сборки нужно явно добавить в pom.xml в boot maven plugin явную конфигурацию <exclude>:

          <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <excludes>
                    <exclude>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                    </exclude>
                </excludes>
            </configuration>
        </plugin>

Добавляем getters and setters и пустой + со всем аргументами конструктор используя аннотации Lombok.
    @Data
    @NoArgsConstructor
    @AllArgsConstructor

Полезная аннотация которая добавляет логгер классу.
    @Log   

Ссылки: Фичи Lombok https://urvanov.ru/2015/09/22/project-lombok/
2. Работа с DB (H2, Spring Data JPA) 2.1 Spring Data JPA. ApplicationRunner
Commit: https://github.com/StringerDM/bootjava/commit/530474b5f8ac9f85dd89284476fcb42685cb7aba
    
В проекте у нас уже есть подключенный spring-boot-starter-data-jpa, также подключина БД H2 и при запуске sping boot уже может сразу поднять БД с настройками по умолчанию. База embedded т.е. она работает в тойже JVM что и наше приложение и по умолчанию spring boot создает ее прямо и entites (классы отмеченные @Entity).

Добавляем требуемые аннотации в модель для валидации, названия таблиц и колонок (не обязательно, по умолчанию по имени полей). См. commit.

@Entity
@Table(name = "")
@Column(name = "")

@Size(max = 128)
@NotEmpty
@NotNull
@Email

и т.д. 

Чтобы не создавать поле Id можно унаследоваться от класса AbstractPersistable<Integer> который уже содержит поле Id с нужными аннотациями для генерации ключей в базе и методами setId, isNew, equals, heshcode, toString.

Также добавим lombok аннотацию @ToString(callSuper = true, exclude = {"password"}) с параметрами "callSuper = true" для включения поля id из суперкласса и exclude = {"password"} для исключения из строки поля password.

Для ролей мы не делаем отдельное entity а указываем их как @ElementCollection(fetch = FetchType.EAGER)

Cо spring boot v2.3 убрали валидацию по умолчанию, поэтому добавили в pom.xml:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

Далее определяем интерфейс userRepository extends JpaRepository<User, Integer>. Имплементация по умолчанию JpaRepository это класс SimpleJpaRepository, сбда можно брейк поинты ставить для дебага.

В aplication.property сделаем одну настройку (Common application Data properties - https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#data-properties все настройки spring boot и по ключевому слов JPA мы можем найти все конфигурационный классы и что можно объявлять):

spring.jpa.show-sql=true - для отображения запросов в базу. (это крайне полезно для Hibernate во время разработки).

Запускаем приложение и смотрим как наша таблица создается. По умолчанию для embedded БД таблицы сначало дропаются, затем создается общий для всех hibernate siquence и создаются таблицы.

Зделаем сначало заполнение таблиц програмно. В spring boot есть 2 интерфейса ApplicationRunner and CommandLineRunner которые позволяют выполнять произвольный код после старта приложения. Разница между ними в том что ApplicationRunner мы принимае массив аргументов обернутый в класс который позовляет нам выполнять какието удобные вещи например getOptional value. Реализовывать интерфейсы можно в любом из бинов spring, мы реализуем его в главном RestaurantVotingApplication:

//реализуем интерфейс ApplicationRunner
@SpringBootApplication
@AllArgsConstructor
public class RestaurantVotingApplication implements ApplicationRunner {

//инжектим userRepository через аннотацию @AllArgsConstructor
private final UserRepository userRepository;

//вставляем в базу 2х юзеров:

@Override
public void run(ApplicationArguments args) {
    userRepository.save(new User("user@gmail.com", "User_First", "User_Last", "password", Set.of(Role.ROLE_USER)));
    userRepository.save(new User("admin@javaops.ru", "Admin_First", "Admin_Last", "admin", Set.of(Role.ROLE_USER, Role.ROLE_ADMIN)));
}

Запускаем приложение и видимо что Hibernat делает 3 запроса, 1м он достает 2х юзеров и потом на каждого юзера он достает роли. Это измвестная проблема n+1, если бы у нас было 10 тысяч юзеров то Hibernate сгенерил бы 10 001 запрос.
Проблема N+1. Стратегии загрузки коллекций
  N+1 selects issue https://stackoverflow.com/questions/97197/548473
  в JPA             https://dou.ua/lenta/articles/jpa-fetch-types/
  в Hibernate       https://dou.ua/lenta/articles/hibernate-fetch-types/
  если ссылки выше не открываются: Runet Censorship Bypass https://chrome.google.com/webstore/detail/%D0%BE%D0%B1%D1%85%D0%BE%D0%B4-%D0%B1%D0%BB%D0%BE%D0%BA%D0%B8%D1%80%D0%BE%D0%B2%D0%BE%D0%BA-%D1%80%D1%83%D0%BD%D0%B5%D1%82%D0%B0/npgcnondjocldhldegnakemclmfkngch    
В TopJava мы решали её тремя сопособами:
  - Через fetch Join
  - Entity Graff
  - И для ролей в Юзере мы делали @BatchSize(size = 20)
  
В Hibernate есть настрока которая позволяет выставлять batch size глобально для всего приложения. 
  Hibernate configurations - http://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#configurations - по ссылке можно найти настройку spring.jpa.properties.hibernate.default_batch_fetch_size=20 (укажем 20 по размеру колонок в таблице на странице).    
  hibernate.jdbc.fetch_size vs hibernate.jdbc.batch_size - https://stackoverflow.com/questions/21257819/548473

Также добавим spring.jpa.properties.hibernate.format_sql=true - форматирование sql запросов в выводе (запросы читать легче)
и spring.jpa.properties.hibernate.jdbc.batch_size=20 это количество в баче апрдейтов и инсертов хибернейта.
# https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc

И последняя настройка, если мы посмотрим на лог то мы увидим Warning - spring.jpa.open-in-view is enabled by default и нужно его выключить:
spring.jpa.open-in-view=false
Open Session In View Anti-Pattern - # https://vladmihalcea.com/the-open-session-in-view-anti-pattern/
spring.jpa.open-in-view - # https://stackoverflow.com/a/48222934/548473
Это антипаттерн - если в модели при преобразовании view остались какието не проинициализированный поля которые lazy proxy то открывается транзакция и делаются еще дополнительный запросы в базу чтобы проинициализировать эти поля.
Запускаем приложение и смотрим на отработку запроса findAll и видем что теперь только 2 запроса.1й для юзеров и 1 запрос для всех ролей. Если юзеров будет много то роли будут доставаться пачками по 20 юзеров.

2.2 H2. Популирование и конфигурирование

Commit: https://github.com/StringerDM/bootjava/commit/2e03672e1984c941211e37256e7b07eaea5445a3
    
Открытая СУБД написанная полностью на Java не смотря на малый размер, поддерживает много возможностей... 

Первое что мы сделаем это перейдем с формата .properties на формат .yaml 
Явно объявим то что было по дефолту 
Встроенная база 
      hibernate:
        ddl-auto: create-drop
      datasource:
        url: jdbc:h2:mem:voting
        username: sa
        password:
      #    tcp: jdbc:h2:tcp://localhost:9092/mem:voting
      # Absolute path
      #    url: jdbc:h2:C:/projects/bootjava/restorant-voting/db/voting
      #    tcp: jdbc:h2:tcp://localhost:9092/C:/projects/bootjava/restorant-voting/db/voting
      # Relative path form current dir
      #    url: jdbc:h2:./db/voting
      # Relative path from home
      #    url: jdbc:h2:~/voting
      #    tcp: jdbc:h2:tcp://localhost:9092/~/voting
      h2.console.enabled: true

если у вас версия spring-boot 2.5.0 и выше, добавьте в application.yaml:
spring.jpa.defer-datasource-initialization: true

Чтобы поднять H2 TCP сервер мы делаем конф. класс и объявляем там
      @Bean(initMethod = "start", destroyMethod = "stop")
      public Server h2Server() throws SQLException {
          log.info("Start H2 TCP server");
          return Server.createTcpServer("-tcp", "-tcpAllowOthers", "-tcpPort", "9092");
      }

При этом в pom нам нужно убрать runtime зависимости h2 потомучто классы h2 теперь понадобились на этапе компиляции.

Запускаем приложение и подключаемся к базе через idea. Если мы попробуем приконектится по url то ничего не выйдет, конект пройдет но если мы на неё посмотрим то никаких баз не увидим. База данных к которой мы приконектились поднимается в памяти в процессе JVM idea и никакой отношение к БД приложения не имеет. Поэтому мы подняли TCP сервер чтобы мы могли приконектится извне - jdbc:h2:tcp://localhost:9092/mem:voting

Подключаемся к базе и делаем интеграцию с Idea выбирая в persistence/springboot -> data source – H2.

H2 console также доступна по http://localhost:8080/h2-console

Давайте пропопулируем нашу БД не через приложение а через скрипт как это обычно делается.
Из applicationRunner удаляем save user и добавляем в ресурсы файл data.sql где популируем users и userRoles (у spring boot 2 файла который он автоматически исполняет data.sql и schema.sql schema нам не требуется т.к. за создание схемы базы отвечает hibernate).
Loading Initial Data https://www.baeldung.com/spring-boot-data-sql-and-schema-sql
Запускаем приложение и сталкиваемся с проблемой что ID у нас должно быть NotNull но оно автоматически не генерится. Смотрим на лог генерации таблицы и видимо что ID сгенерировалось как обычное поле.
H2: NULL not allowed for column “ID”  - https://stackoverflow.com/a/54697387/548473
Смотрим решение проблемы на stackoverflow и видим 3 варианта:

  1.	Поменять @GeneratedValue с авто, как у нас в наследуемом AbstractPersistable классе на
  change @GeneratedValue to strategy = GenerationType.IDENTITY

  2.	Set spring.jpa.properties.hibernate.id.new_generator_mappings=false (spring-boot alias spring.jpa.hibernate.use-new-id-generator-mappings) это означает      
  работу по старой стратегии не по sequence а по identity 

  3.	insert with nextval: INSERT INTO TABLE(ID, ...) VALUES (hibernate_sequence.nextval, ...) – вставлять в базу ID сгенерированный hibernate.
 
Для нас самое просто использовать 2й вариант. Теперь все работает. Со старой стратегии ID генерится как identity.

2.3 Рефакторинг model. Spring Data JPA @Query

commit: https://github.com/StringerDM/bootjava/commit/f789d22071f65c732533c9b512015e8a05b8ede5
Заменим стандартный AbstractPersistable собственным классом BaseEntity:
    @Access(AccessType.FIELD)
    Здесь объявляем чтобы hibernate работал с entity по полям - https://stackoverflow.com/a/6084701/548473
Методы тип isNew() не нужно помечать что они transient.
Методы equals и hashCode сделаны попроще. 
И в equal эту строчку взяли из класса AbstractPersistable:

    if (o == null || !getClass().equals(ProxyUtils.getUserClass(o))) {
    return false;
    }

Т.к. hibernate может проектировать классы и перед сравнением их нужно развернуть.
Ссылка как правильно в Entity hibernate переопределять equals и hashCode (очень частая ошибка)
https://stackoverflow.com/questions/1638723

По правилам рекомендуется делать уникальное неизменяемое бизнес поле, а обычно такого нет и во всех 
проектах использовался primary key. На primary key сделали @GeneratedValue(strategy = GenerationType.IDENTITY) 
как у нас и генирурется на данный момент, поэтому в файле конфигурации id.new_generator_mappings: false уже не 
требуется.

Все наши Entity классы будем наследовать он BaseEntity.

interface UserRepository {
В репозиториях в запросе @Query для именованных параметров (:email) теперь в методе можно не указывать аннотацию 
@Param(“email”), hibernate теперь берет имя параметра через отражение.

    @Query("SELECT u FROM User u WHERE u.email = LOWER(:email)")
    Optional<User> findByEmailIgnoreCase(String email);
}

Также как и в контроллерах в аннотациях @Pasthariable и @RequestParam атрибуты nameValue не требуется.
3 Spring Data REST + HATEOAS 3.1 Spring Data REST

commit: https://github.com/StringerDM/bootjava/commit/8671606a67ce4d9e57da95c30c0a736508804e0f

Оживим наше приложение, добавим зависимость org.springframework.boot spring-boot-starter-data-rest

Теперь в браузере стали доступны следующие странички: GET http://localhost:8080/api GET http://localhost:8080/api/users GET http://localhost:8080/api/users/1 GET http://localhost:8080/api/users/search GET http://localhost:8080/api/users/search/by-email?email=User@gmail.com GET http://localhost:8080/api/users/search/by-lastname?lastName=Admin GET http://localhost:8080/api/users/search/by-lastname?lastName=last POST http://localhost:8080/api/users Content-Type: application/json

{ "email": "test@test.com", "firstName": "Test", "lastName": "Test", "password": "test", "roles": [ "ROLE_USER"] }

PATCH http://localhost:8080/api/users/1 Content-Type: application/json

{ "lastName": "User+Last" }

Spring Data Rest - это модуль который входит в семейство Spring Data, анализирует репозитории и доменную модель и проанализированный результат выставляет наружу через контроллеры как hypermedia driven HTTP resources.

Понимание HATEOAS (Hypermedia as the Engine of Application State) - http://spring-projects.ru/understanding/hateoas/ Это правило создания REST приложений когда нам возвращаются не только результаты но и еще URL на ресурсы. Все данные представляются как набор ресурсов, к ним есть URL и в более сложном случае к нам возвращается набор разных URL по которым мы можем достать все возможные в данном контексте ресурсы. Таким образом мы можем общаться с сервисом без спецификации, все действия с резурсами на клиенте производятся через URL. Id на клиенте не выводится - Spring Data REST expose ids - https://stackoverflow.com/questions/24936636/548473/33744785#33744785

Сделаем небольшую кастомизацию. Spring Data REST settings - https://docs.spring.io/spring-data/rest/docs/current/reference/html/#getting-started.basic-settings По ссылке есть различные настройки, по которым мы можем менять поведение Data Rest: Сделаем: data.rest: basePath: /api returnBodyOnCreate: true // возращать дело при создании ресурса.

В низу http://localhost:8080/api/users Spring Data Rest также нам выставил ссылку по которой мы можем делать операции с users: http://localhost:8080/api/users/search Он проанализировал методы репозитория и выставил их наружу, имена их совпадают с именем методов и мы таже можем их кастомизировать:

Делается это через аннотации @RestResource:

@RestResource(rel = "by-email", path = "by-email")
@Query("SELECT u FROM User u WHERE u.email = LOWER(:email)")
Optional<User> findByEmailIgnoreCase(String email);

@RestResource(rel = "by-lastname", path = "by-lastname")
List<User> findByLastNameContainingIgnoreCase(String lastName);

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

  "_links" : {
    "by-email" : {
      "href" : "http://localhost:8080/api/users/search/by-email{?email}",
      "templated" : true
    },
    "by-lastname" : {
      "href" : "http://localhost:8080/api/users/search/by-lastname{?lastName}",
      "templated" : true
    },
    "self" : {
      "href" : "http://localhost:8080/api/users/search"
    }
  }
}

Тажке можно подключить зависимость: Spring REST and HAL Browser - https://www.baeldung.com/spring-rest-hal org.springframework.data spring-data-rest-hal-browser runtime Заглавная страница будет доступна через API здесь hal browser в таком виде позваляет отдавать не только Get запросы но и другие запросы.

В свежих версиях Spring, вместо spring-data-rest-hal-browser нужно использовать spring-data-rest-hal-explorer При проблеме с Lombok с новыми JDK поднимите его версию до последней.

В IDEA появился инструмент который позволяет отправлять запросы Tools / HTTP client / Show HTTP Request Hostory Можно скопировать POST http://localhost:8080/api/users Content-Type: application/json

    {
     "email": "test@test.com",
     "firstName": "Test",
     "lastName": "Test",
     "password": "test",
     "roles": [ "ROLE_USER"]
    }

Что создаст нового юзера

    PATCH http://localhost:8080/api/users/1
    Content-Type: application/json

    {
      "lastName": "User+Last"
    }

Данным запросом поменяем lastName у user 1 HAL vs HATEOAS - https://stackoverflow.com/questions/25819477/548473 (HAL реализация правила HATEOAS в виде запросов такого вида).

Сколько кода надобыло написать чтобы сделать это вручную, и сколько мы написали используя Spring Data Rest

3.2 Конфигурирование Jackson

commit: https://github.com/StringerDM/bootjava/commit/3cba92bbb7c392e258c9ff86cfbd0ea39a2cb895

Поговорим немножко про сериализацию / десериализацию Jackson - это библиотека которая по умолчанию используется Spring Boot Если мы посмотрим на вывод юзеров в нашем приложении то увидим здесь поле new:

http://localhost:8080/api/users/ ... "roles" : [ "ROLE_USER" ], "new" : false, "_links" : { ...

Это метод isNew в нашем BaseEntity, по умолчанию Jackson сериализует / десериализует через getters / setters В курсе TopJava мы решали это через переопределение ObjectMapper - для всего приложения запрещали смотреть на getters / setters и разрешали поля.

В Spring Boot можно сделать эти настроки через config application, мы можем сказать что

Jackson Serialization Issue Resolver

jackson:

visibility.field: any - сериализуем / десериализуем только поля

visibility.getter: none

visibility.setter: none - не смотрим на getters / setters

visibility.is-getter: none и is getters (для boolean полей).

Запустим приложение и увидим что поля isNew уйдут но зато появятся поля links - Spring Data Rest наши Entity оборачивает в ресурс в этом ресурсе есть линки соответственно есть такие поля и он их выводит, т.е. через Spring Data Rest у нас не полчается сделатьэ общее решение для всего приложения (поэтому уберем эту конфигурацию). И мы вместо общего решение сделаем стандартное: @JsonIgnore @Override public boolean isNew() { return id == null; }

Common application JSON properties - https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#json-properties Аннотации Jackson - https://nsergey.com/jackson-annotations/

Также наш метод не работает для hibernate lazy объектов - используем его только для проинициализированных сущностей. // doesn't work for hibernate lazy proxy public int id() { Assert.notNull(id, "Entity must have id"); return id; }

3 Spring Data REST + HATEOAS 3.1 Spring Data REST

About

Открытый курс Spring Boot 2.x HATEOAS application (BootJava): https://javaops.ru/view/bootjava

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Java 100.0%