Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

StringerDM/bootjava

Open more actions menu
 
 

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%
Morty Proxy This is a proxified and sanitized view of the page, visit original site.