arrow-left

All pages
gitbookPowered by GitBook
1 of 10

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

1. Принцип Separation of Concerns, контроллер и представление

hashtag
Отделение графического интерфейса от бизнес-логики приложения. Язык FXML.

До этого момента мы создавали графический интерфейс напрямую в коде приложения, явно создавая объекты графического интерфейса, меняли их свойства, назначали слушатели событий и так далее.

Такой подход обладает рядом недостатков:

  • такой программный код превращается в череду созданий объектов и вызовов методов для их настройки, что затрудняет чтение кода и приводит к тому, что он выглядит не лучшим образом;

  • такой код тяжело отлаживать и исправлять ошибки при реализации графического интерфейса. Необходимо постоянно компилировать код и смотреть полученный результат в работающем приложении;

  • графический интерфейс, как правило, проектируется отдельным специалистом в области UI/UX, который может не знать языка, на котором этот интерфейс проектируется (в нашем случае, это Java). Кроме того, это приводит к тому, что java-программист, кроме программирования логики приложения, должен проектировать GUI, что приводит к дополнительным материальным затратам;

  • в случае необходимости изменения графического интерфейса (добавления нового функционала, изменения внешнего вида, анимации, эффектов), необходимо «перелопачивать» код и вносить туда изменения, что приводит к многочисленным ошибкам и тормозит процесс разработки программного обеспечения.

Основополагающим принципом разработки программного обеспечения является принцип separation of concerns

Исходя из этого, следует запомнить первое правило, которому должен следовать любой программист при разработке даже самого простого приложения: внешний вид (пользовательский интерфейс) приложения всегда должен быть максимально отделен от бизнес-логики работы приложения. Этот принцип еще кратко называют принципом отделения представления от содержания (эти принципы называются «Separation of concerns» или «Separation of presentation and content»).

circle-info

В общем случае, принцип separation of concerns формулируется следующим образом - программа должна быть разделена на функциональные блоки, как можно меньше перекрывающие функции друг друга.

Повторим еще раз, что пользовательский интерфейс и внутренняя логика работы программы должны быть максимально независимы друг от друга. В идеале, вашей программе должно быть все равно – в каком виде она представлена пользователю – в виде GUI, консольного приложения, в окне браузера или еще как-то. В идеале, при изменении GUI, остальным классы программы остаются нетронутыми и изменения в них вносить не надо (в реальности, конечно, изменения вноситься будут, но они должны быть минимальны). Это помогает сделать программу более гибкой, а также помогает добиться разделения труда: программист пишет код, а дизайнер (верстальщик, специалист по UI/UX) занимается проектированием графического интерфейса.

Подавляющее большинство современных графических библиотек и фреймворков обладают тем или иным механизмом отделения представления от содержания.

Давайте разберемся, как это реализовано в JavaFX. Рассмотрим простой пример. Создадим пустое JavaFX-приложение

В примере определен объект Stage, объект Scene и корневой элемент графа сцены. Очевидным следующим шагом было бы создание новых объектов графического интерфейса, установка свойств объектов и формирование графа сцены.

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

Выполним несколько шагов, чтобы настроить работу IntelliJ IDEA для использования языка FXML:

  1. скачаем и установим приложение SceneBuilder по ссылке ;

  2. зайдем в настройки IntelliJ IDEA (Ctrl+Alt+S или File -> Settings) и в пункте JavaFX укажем путь к exe-файлу установленного SceneBuilder;

  3. перезапустим IntelliJ IDEA.

Откроем наш проект с JavaFX-приложением и создадим в проекте новый FXML файл sample.fxml.

Давайте разберемся, зачем мы создали этот файл и зачем нам нужен язык FXML. В JavaFX пользовательский интерфейс можно описать с помощью специального языка разметки, который называется FXML. Язык FXML является подмножеством языка XML и немного напоминает HTML.

Таким образом, вместо того, чтобы создавать объекты пользовательского интерфейса на языке Java в исходном коде программы, мы описываем граф сцены в виде fxml-файла, который выглядит определенным образом. Благодаря тому, что XML позволяет описать любые данные, мы можем очень точно описать объекты и их свойства, а древовидная структура XML идеально описывает граф сцены, который тоже имеет структуру дерева.

Откроем файл sample.fxml и посмотрим его содержимое

Как мы видим, текст файла напоминает формат XML, в котором присутствуют некоторые элементы языка Java (в частности, подключение библиотек с помощью import).

Опишем граф сцены из нескольких элементов, после чего разберем, что происходит в получившемся коде.

Итак, давайте попробуем разобраться в этом коде:

  • строка 1 – в ней содержится так называемая XML-декларация. Она описывает версию XML и кодировку;

  • строки 3 – 6 отдаленно напоминают команды импорта в java, которые заключены между символами <? и ?>. Такая конструкция называется инструкцией обработки (Processing Instruction, PI), фактически, это инструкция тому приложению, которое будет «читать» XML-файл. Фактически, это означает инструкцию – подключить те или иные библиотеки;

Таким образом, данное описание довольно понятно и легко читается человеком, который хоть сколько-нибудь знаком с форматом XML и с элементами JavaFX.

Граф сцены, описанный в файле sample.fxml с помощью традиционного способа создания объектов Java в исходном коде выглядит следующим образом:

Визуальные редакторы UI

Использование специального языка описания FXML дает нам возможность использовать специальные визуальные редакторы, которые позволяют нам проектировать граф сцены в визуальном режиме.

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

При работе с fmxl-файлами вы можете легко переключаться между текстовым и визуальным представлением файла, редактировать код, который сгенерировал визуальный редактор и видеть результат без необходимости каждый раз компилировать и запускать приложение.

Использование редактора очень сильно облегчает и ускоряет разработку графических приложений. Кроме того, при работе с fxml очень удобно подключать css-стили и даже вызывать скрипты на различных языках программирования, например, например, Groovy, Clojure и даже JavaScript (данный вопрос в рамках этого курса не рассматривается).

Используя визуальный редактор, создадим граф сцены, который визуально будет выглядеть следующим образом

В формате XML этот код будет выглядеть следующим образом:

У нас есть готовый граф сцены, но что дальше? Как известно, в Java всё является объектами некоторых классов, в том числе и элементы графического интерфейса.

До этого мы просто создавали в исходном коде объекты нужных нам классов, после чего вызывали их методы для формирования нужного нам графа сцены. Но у нас только текстовое описание графа сцены в формате FXML, как его преобразовать в набор объектов, вызвать нужные методы этих объектов и сформировать граф сцены?

Генерация объектов графа сцены. Загрузчик FXML

Для решения вышеуказанной проблемы было придумано много различных методик и подходов, которые были реализованы в различных графических библиотеках в различных языках программирования.

Фреймворк JavaFX использует распространенный подход, который состоит в генерации объектов. Его суть состоит в следующем: в JavaFX существует специальный класс FXMLLoader (загрузчик FXML), который содержит статический метод load(). Этот метод реализует следующий функционал:

  1. метод считывает fxml-файл, URL которого вы должны указать;

  2. метод «парсит» fxml-файл (разбивает файл на отдельные элементы с атрибутами);

  3. используя результаты парсинга и механизм рефлексии (механизм рефлексии в данном курсе не рассматривается), метод создает объекты, устанавливает их свойства и помещает один объект в состав других объектов и таким образом формирует нужный нам граф сцены;

Вернемся к нашему исходному коду. Используя полеченные знания о работе класса FXML, выполним следующие шаги:

  1. вызовем статический метод FXMLLoader.load(), на вход которого передадим URL ресурса, в качестве которого выступает наш fxml-файл (что такое ресурс и как нам получить URL ресурса подробнее читайте здесь );

  2. ссылку на корневой элемент графа сцены передадим в созданный объект сцены.

Полученный код выглядит следующим образом.

Запустим приложение и убедимся, что сгенерированный граф сцены соответствует тому, что мы визуально спроектировали в SceneBuilder.

Таким образом, мы визуально спроектировали граф сцены для нашего окна, после чего класс FXMLLoader сгенерировал нужный граф сцены в объектном виде.

Но сразу же возникает следующая проблема, каким образом получить ссылки на эти сгенерированные объекты? Если мы хотим указать обработчик событий для элементов графического интерфейса или динамически изменить свойства некоторых элементов – нам нужна ссылка на объекты элементов графического интерфейса.

Когда мы создавали объекты самостоятельно в исходном коде классов, мы хранили ссылку на них и могли к ним обратиться. Но в нашем случае, объекты были сгенерированы в недрах класса FXMLLoader, упакованы один в другой и всё, что у нас есть – ссылка на корневой элемент графа сцены.

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

hashtag
Контроллер графа цены. Получения ссылок на объекты графа сцены.

Получение ссылки на сгенерированный элемент графического интерфейса – это одна из проблем, которая возникает, когда объекты графического интерфейса создаются не вручную. В различных графических библиотеках и в разных языках программирования эта проблема решается по-разному. Давайте рассмотрим, как эта проблема решается в JavaFX.

При описании интерфейса, в fxml-файле вы можете указать т.н. контроллер – специальный класс, который будет «управлять» объектами, которые описаны в fxml-файле. Почему этот класс называется контроллером и что такое вообще «контроллер» (вы, наверняка, слышали этот термин и даже что-то читали по этому поводу), мы рассмотрим при изучении архитектурного паттерна MVC.

Класс-контроллер это обычный java-класс, который должен реализовывать интерфейс javafx.fxml.Initializable, который определяет всего один метод initialize(). Создадим класс SampleController и пока оставим его пустым.

Дальнейший шаг – в fxml-файле необходимо указать, что для данного графа сцены будет использоваться этот класс контроллера. Эта информация будет считана FXMLLoader при парсинге fxml-файла.

Откроем файл sample.fxml и в корневом узле графа сцены укажем атрибут fx:controller = ”ua.opu.SampleController”.

Таким образом, мы заявляем, что для данного fxml-файла будет использоваться указанный класс контроллера. Этот атрибут можно использовать только в корневом узле fxml-файла.

Чтобы отличить один объект элемента UI от других, необходимо присвоить элементам UI различные идентификаторы, используя которые мы сможем обратиться к тому или иному элементу. Для того чтобы установить идентификатор для элемента, необходимо указать атрибут fx:id в коде либо в свойствах в визуальном редакторе.

Идем дальше. Теперь я хочу каким-то образом в классе контроллера получить ссылку на первые две кнопки и как-то ними дальше манипулировать. Как мне это сделать? Чтобы получить доступ к каким-то элементам, мне нужно этим элементам присвоить определенный id. Вы можете сделать это либо в визуальном редакторе (выделите нужный элемент, зайдите во вкладку Code: и установите значение в поле fx:id).

Для данного примера присвоим идентификаторы для двух кнопок: button1 и button2.

Нам необходимо получить ссылки на объекты этих кнопок. Для этого зайдем в класс контроллера и создадим два поля типа javafx.scene.control.Button, с названиями, которые точно совпадают с идентификаторами этих кнопок в fxml-файле. После чего, укажем перед каждым полем аннотацию @FXML. В итоге, класс контроллера будет иметь следующий вид

circle-info

Еще раз обратите внимание, что fx:id и имя переменной должно совпадать точно и перед каждым объявлением должно быть указано @FXML.

Давайте разберемся, что происходит при указании контроллера и полей с аннотацией @FXML:

  • класс FXMLLoader парсит fxml-файл;

  • загрузчик «считывает» атрибут fx:controller и создает объект этого класса;

  • загрузчик парсит аннотации @FXML

Таким образом, если мы правильно указали fx:id и не ошиблись с классами и названиями полей, то в момент вызова метода initialize(), наши поля button1 и button2 будут содержать ссылки на наши две кнопки.

Добавим обработчик нажатия на кнопку 1

Всё прошло успешно, мы имеем ссылки на нужные нам элементы, и мы можем делать с ними что захотим.

Для второй кнопки реализуем слушатель иначе. В классе-контроллере создадим метод handleButton2() и тоже пометим его аннотацией @FXML.

То есть, мы видим, что мы можем помечать этой аннотацией не только поля, но и методы. Далее, зайдем в fxml-файл, в режим визуального редактора, выделите вторую кнопку, зайдите во вкладку Code: и в поле On Action из выпадающего списка выберите нужный метод

В текстовом виде это выглядит следующим образом

Компилируем приложение и смотрим результат

Как вы уже, наверное, поняли, фактически это означает следующее «если для кнопки2 произошло событие Action – вызови метод handleButton2(). Таким образом, если у вас много слушателей, то удобнее просто закодировать методы и расставить нужные методы для нужных событий в нужных элементах, и не париться лишний раз с полями.

строки 8-10 определяет корневой элемент графа сцены – элемент HBox. В качестве атрибутов xmlns указаны пространства имен для тэгов FXML. Все остальные элементы размещаются внутри HBox, что в точности соответствует структуре дерева;
  • в строке 11 указан элемента <children>. Потомки того или иного элемента содержатся внутри элемента <children>, что напоминает метод getChildren(), который мы используем для добавления потомков элемента;

  • свойства элементов (высота, ширина, текст надписи и так далее) описываются в виде атрибутов тех или иных элементов.

  • в качестве возвращаемого значения, метод load() возвращает ссылку на корневую вершину (root node), которая прямо или опосредованно содержит в себе весь сгенерированный граф сцены.

    и внедряет в эти поля ссылки на созданные объекты (используется механизм рефлексии);
  • загрузчик вызывает метод initialize() (в этом методе мы прописываем все наши манипуляции с нужными элементами GUI).

  • http://gluonhq.com/products/scene-builder/arrow-up-right
    https://goo.gl/FJfvR9arrow-up-right

    2. Паттерн MVC

    Паттерн MVC можно описать следующим образом

    circle-info

    Шаблон проектирования MVC предполагает разделение данных приложения, пользовательского интерфейса и управляющей логики на три отдельных компонента: Модель, Представление и Контроллер – таким образом, что модификация каждого компонента может осуществляться независимо.

    В данном определении под компонентом следует понимать часть кода (как правило, это отдельный класс), каждая из которых играет одну из ролей Контроллера, Модели или Представления, где Модель служит для извлечения и манипуляций данными приложения, Представление отвечает за видимое пользователю отображение этих данных, а Контроллер управляет всем этим оркестром

    Архитектурная концепция (архитектурный паттерн) MVC позволяет разделить программу на три отдельных компонента, которые могут быть реализованы следующим образом:

    1. Пользователь взаимодействует с представлением. Представление – «окно», через которое пользователь воспринимает модель. Когда вы делаете что-то с представлением (скажем, щелкаете на кнопке воспроизведения), представление сообщает контроллеру, какая операция была выполнена. Контроллер должен обработать это действие.

    2. Контроллер обращается к модели с запросами об изменении состояния. Контроллер получает действия пользователя и интерпретирует их. Если вы щелкаете на кнопке, контроллер должен разобраться, что это значит и какие операции с моделью должны быть выполнены при данном действии.

    3. Контроллер также может обратиться к представлению с запросами об изменении. Когда контроллер получает действие от представления, в результате его обработки он может обратиться к представлению с запросом на изменение (скажем, заблокировать некоторые кнопки или команды меню).

    4. Модель оповещает представление об изменении состояния. Когда в модели что-то изменяется (вследствие действий пользователя или других внутренних изменений – скажем, перехода к следующей песне в списке), модель оповещает представление об изменении состояния.

    5. Представление запрашивает у модели информацию состояния. Представление получает отображаемую информацию состояния непосредственно от модели. Например, когда модель оповещает представление о начале воспроизведения новой песни, представление запрашивает название песни и отображает его. Представление также может запросить у модели информацию состояния в результате запроса на изменение состояния со стороны контроллера.

    Модель (Model) – содержит бизнес-логику приложения и включает методы выборки, обработки и предоставления конкретных данных, что зачастую делает ее очень «толстой», что вполне нормально. Модель не должна напрямую взаимодействовать с пользователем. Модель:

    • предоставляет (представлению и контроллеру):

      • данные;

      • методы работы с данными:

    Представление (View) – используется для задания внешнего вида отображения данных, полученных из контроллера и модели. Представление не должно обращаться к базе данных, этим должны заниматься модели. Также, представление не должно работать с данными, полученными из запроса пользователя. Эту задачу должен выполнять контроллер. Представление:

    • отвечает за получение необходимых данных от модели и отправку их пользователю;

    • не обрабатывает введенные данные пользователя;

    • может влиять на состояние модели через отправку ей сообщений (вызовы методов).

    Контроллер (Controller) – связующее звено, соединяющее модели, представления и другие компоненты в рабочее приложение. Контроллер отвечает за обработку действий пользователя. В хорошо спроектированном MVC-приложении контроллеры обычно очень «тонкие» и содержат только несколько десятков строк кода. Логика контроллера довольно типична и большая ее часть выносится в базовые классы. Модели, наоборот, очень «толстые» и содержат большую часть кода, связанную с обработкой данных, так как структура данных и бизнес-логика, содержащаяся в них, обычно довольно специфична для конкретного приложения. Контроллер:

    • обеспечивает «связь» между пользователем и системой, контролирует и направляет:

      • данные от пользователя к системе;

      • реакцию системы – пользователю.

    Рассмотрим пример реализации паттерна MVC на примере просто приложения. Приложение выводит на экран список студентов, добавляет нового студента и удаляет выделенного студента из списка.

    Для начала, создадим новый JavaFX проект. Шаблон JavaFX проекта содержит три файла. Давайте определимся, к какой категории относится тот или иной файл.

    Файл sample.fxml, очевидно, относится к представлению (View). Он задает внешний вид приложения.

    Класс Main (файл Main.java) относится к контроллеру. Этот класс можно назвать контроллером приложения. Он содержит метод Application.launch(), который запускает JavaFX приложение, настраивается окно, создается объект сцены, устанавливается нужный fxml файл для генерации графа сцены для данного окна.

    Класс Controller (файл Controller.java) очевидно является контроллером. Это контроллер окна, который содержит код для обработки событий, связанный с данным окном.

    Создадим соответствующие пакеты и распределим файлы по своим пакетам. Не забудьте изменить путь к файлу fxml.

    Теперь создадим класс Student, который будет содержать данные и логику для объекта студента. Очевидно, что этот класс относится к классу модели.

    Спроектируем UI для нашего приложения. Для этого отредактируем файл sample.fxml. За вывод списка студентов будет отвечать элемент ListView. Кроме него предусмотрены две кнопки для добавления и удаления студента.

    Добавим обработчики нажатий на кнопки в контроллер окна Controller.java.

    Теперь вернемся к классам модели. Очевидно, что в данном приложении объекты класса Student должны быть организованы в некоторую коллекцию. Кроме того, мы должны предоставить метод добавления и удаления студентов из коллекции.

    Так как «сборка» отдельных частей приложения осуществляется в контроллере – организаторе и посреднике между различными компонентами приложения, самым простым вариантом было бы просто создать пустую коллекцию студентов, в которую можно было бы напрямую добавлять или удалять студентов.

    Так как мы оставляем возможности для будущего расширения программы, в которой могут появиться новые окна (например, отдельное окно для добавления студента), коллекция со студентами должна быть доступна во всем приложении.

    Таким образом, первый кандидат на хранении коллекции со студентами – контроллер приложения, класс Main. Код для такого варианта будет примерно следующим:

    Методы addStudent() и deleteStudent() будут вызываться из контроллера окна вместе с объектом для добавления или удаления.

    Такой подход оправдан только, если мы точно знаем, что программа дальше не будет расширяться и наша модель будет, фактически, состоять из одного класса Student. Но мы хотим оставить задел на будущее, когда у нас может появиться класс студенческой группы, класс с оценками, класс учебной дисциплины, данные могут записываться и считываться из файлов, передаваться по сети и так далее.

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

    Поэтому, лучшим вариантом будет применение паттерна проектирования «Фасад», который позволит перенести весь будущий функционал в слой модели.

    Принцип работы паттерна «Фасад» можно объяснить следующим образом: фасад – это объект некоторого класса, который предоставляет простой (но урезанный) интерфейс работы со сложной системой объектов. Таким образом, если наша модель будет усложняться, и будут добавляться новые классы, то будет усложняться внутреннее строение фасада, а внешне он будет предоставлять те же самые простые методы для работы со сложной моделью.

    Создадим класс StudentsFacade. Класс будет содержать коллекцию со студентами, а также предоставлять публичные методы добавления и удаления студента.

    Теперь создадим объект фасада в контроллере приложения и создадим методы, которые будут вызывать методы фасада.

    Следующий шаг – необходимо написать обработчик кнопки «Добавить». При нажатии на кнопку необходимо создать объект класса Student и вызвать метод Main.addStudent(), которому необходимо передать созданный объект.

    Так как обработчик нажатия на кнопку находится в контроллере окна, мы должны из контроллера окна (Controller.java) вызвать метод контроллера окна (Main.java). Чтобы это сделать, мы должны передать контроллеру окна ссылку на контроллер приложения.

    Давайте разберемся, как это сделать. Для начала, откроем контроллер приложения и немного изменим загрузку fxml файла.

    Если ранее мы использовали статический метод FXMLLoader.load(), то теперь мы создадим объект класса FXMLLoader, укажем в конструкторе ресурс fxml файла и вызовем метод load() у созданного объекта.

    Эти манипуляции нужны нам для того, чтобы вызвать у объекта метод getController(), который вернет нам ссылку на созданный объект класса Controller. Таким образом, мы получим ссылку на объект контроллера окна.

    Далее, в контроллере окна необходимо добавить поле типа Main и публичный метод, который будет записывать в поле переданную ссылку на объект контроллера приложения.

    Таким образом, контроллер окна будет иметь ссылку на объект контроллера приложения. Теперь мы можем приступать к написанию кода для обработчика кнопки «Добавить». Для упрощения кода, будем генерировать значения полей для объекта студента.

    Итак, на данный момент мы реализовали этапы 1 – 3 схемы работы паттерна MVC: пользователь взаимодействует с представлением (нажимает на кнопку «Добавить»), контроллер обращается к модели с запросами об изменении состояния. Но нам необходимо оповестить представление о том, что модель изменилась, чтобы представление обновилось (на экране появилась новая запись о студенте).

    Для начала, реализуем взаимодействие модели и представления без использования встроенных средств JavaFX. Для этого нам необходимо разобраться с паттерном «Наблюдатель» (Observer).

    hashtag
    Паттерн «Наблюдатель» (Observer)

    Паттерн Observer определяет зависимость "один-ко-многим" между объектами так, что при изменении состояния одного объекта все зависящие от него объекты уведомляются и обновляются автоматически.

    Паттерн определяет объект Subject, хранящий данные (модель) и объекты Observer, которые реализуют функциональность представлений. При создании наблюдатели Observer регистрируются у объекта Subject. Когда объект Subject изменяется, он извещает об этом всех зарегистрированных наблюдателей. После этого каждый обозреватель запрашивает у объекта Subject ту часть состояния, которая необходима для отображения данных.

    Итак, реализуем этот паттерн в нашем исходном коде. В качестве Subject у нас выступает объект класса StudentsFacade, который хранит нужные данные. Укажем в исходном коде, что класс StudentsFacade наследуется от класса java.util.Observable. Класс Observable содержит готовый функционал для регистрации наблюдателей и для их оповещения об изменении модели. В классе StudentsFacade есть два метода, которые меняют состояние объекта – метод addStudent() добавляет новый объект студента в коллекцию, метод deleteStudent() извлекает из коллекции объект студента. Таким образом, при вызове этих методов, мы должны оповестить всех наблюдателей о том, что произошло изменение данных и оповестить их об этом и передать нужные данные для отображения.

    Далее необходимо указать, что используемый для вывода студентов элемент ListView реализует интерфейс java.util.Observer, а также необходимо запрограммировать реакцию на извещение об изменении модели.

    Когда в классе StudentsFacade будет выполняться метод addStudent() или deleteStudent() и выполнится команда notifyObservers(), у объекта класса ListViewObservers будет вызван метод update(), в котором будет передано два параметра – ссылка на объект, который вызвал метод и переданные данные, если они есть (иногда необходимо передать в Observer сам факт изменений, тогда вызывается метод notifyObservers() без параметров).

    Мы проверяем – от кого произошло событие, и обновляем список с помощью пришедших данных (очищаем список от существующих элементов и добавляем новые).

    Теперь необходимо заменить класс ListView на ListViewObserver. Регистрация Observer будет происходить в классе контроллера приложения

    Нам осталось только реализовать функционал для кнопки «Удалить» и наше небольшое приложение теперь полностью готово.

    Тестируем работу приложения.

    Мы реализовали паттерн «Наблюдатель» вручную, но Java FX уже имеет готовый функционал для реализации этого паттерна. Для этого в JavaFX предусмотрен целый набор классов Observable*, которые предоставляют различные значения и коллекции уже со встроенной поддержкой паттерна Observer. Давайте реализуем 4-5 этап паттерна MVC с помощью встроенных классов Observable.

    Для начала перейдем в класс StudentsFacade и изменим класс коллекции с List на ObservableList. Классу StudentsFacade теперь нет нужды наследоваться от класса java.util.Observable, так как этот функционал теперь реализует непосредственно коллекция со студентами. Кроме того, добавим геттер для коллекции (хотя это не очень согласуется с принципом инкапсуляции).

    В классе Main заменим метод bindObserverToFacade() на setItemsForListView(). Обратите внимание, что элемент ListView содержит метод setItems(), который принимает на вход объект класса ObservableList. При вызове метода регистрация и обновление спискового элемента происходит без нашего участия – мы просто должны вызвать метод setItems() и передать ему ObservableList.

    Заменим использование ListViewObserver на использование стандартного элемента ListView и изменим содержимое класса Controller.

    Запустим приложение и убедимся, что его работа не изменилась, но мы избавились от необходимости создавать свой подкласс спискового элемента и вручную добавлять блок оповещения наблюдателей и вручную прописывать реакцию на оповещение об изменении данных.

    Main.java
    public class Main extends Application {
    
        @Override
        public void start(Stage primaryStage) throws Exception {
            Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
            primaryStage.setTitle("Hello World");
            primaryStage.setScene(new Scene(root, 300, 275));
            primaryStage.show();
        }
    
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    sample.fxml
    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import java.lang.*?>
    <?import java.util.*?>
    <?import javafx.scene.*?>
    <?import javafx.scene.control.*?>
    <?import javafx.scene.layout.*?>
    
    <AnchorPane xmlns="http://javafx.com/javafx"
                xmlns:fx="http://javafx.com/fxml"
                fx:controller="sample.Sample"
                prefHeight="400.0" prefWidth="600.0">
    
    </AnchorPane>
    sample.fxml
    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import java.lang.*?>
    <?import java.util.*?>
    <?import javafx.scene.*?>
    <?import javafx.scene.control.*?>
    <?import javafx.scene.layout.*?>
    
    <HBox xmlns="http://javafx.com/javafx"
          xmlns:fx="http://javafx.com/fxml"
          prefHeight="400.0" prefWidth="600.0">
        <children>
            <Button text="Кнопка"/>
            <VBox prefHeight="200.0" prefWidth="100.0">
                <children>
                    <Label text="Текст"/>
                </children>
            </VBox>
        </children>
    </HBox>
    @Override
    public void start(Stage primaryStage) throws Exception {
    
        HBox hBox = new HBox();
        hBox.setPrefHeight(400);
        hBox.setPrefWidth(600);
        
        Button button = new Button("Кнопка");
        Label label = new Label("Текст");
        
        VBox vBox = new VBox();
        vBox.setPrefHeight(200);
        vBox.setPrefWidth(100);
        
        vBox.getChildren().add(label);
        hBox.getChildren().addAll(button,vBox);
        
        Scene scene = new Scene(hBox, 300, 200);
        primaryStage.setTitle("JavaFX-приложение");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
    sample.fxml
    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.geometry.*?>
    <?import javafx.scene.control.*?>
    <?import javafx.scene.layout.*?>
    <?import javafx.scene.text.*?>
    
    <!--        xmlns="http://javafx.com/javafx"-->
    <!--        xmlns:fx="http://javafx.com/fxml"-->
    
    <GridPane fx:controller="sample.SampleController" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.1" xmlns:fx="http://javafx.com/fxml/1">
      <columnConstraints>
        <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
        <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
      </columnConstraints>
      <rowConstraints>
        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
      </rowConstraints>
       <children>
          <Button maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" text="Кнопка 1" GridPane.hgrow="ALWAYS" GridPane.vgrow="ALWAYS">
             <font>
                <Font size="22.0" />
             </font>
             <GridPane.margin>
                <Insets />
             </GridPane.margin>
          </Button>
          <Button maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Кнопка 2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" GridPane.vgrow="ALWAYS">
             <font>
                <Font size="22.0" />
             </font>
          </Button>
          <Button maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Кнопка 3" GridPane.hgrow="ALWAYS" GridPane.rowIndex="1" GridPane.vgrow="ALWAYS">
             <font>
                <Font size="22.0" />
             </font>
          </Button>
          <Button maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Кнопка 4" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" GridPane.rowIndex="1" GridPane.vgrow="ALWAYS">
             <font>
                <Font size="22.0" />
             </font>
          </Button>
          <Button maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Кнопка 5" GridPane.hgrow="ALWAYS" GridPane.rowIndex="2" GridPane.vgrow="ALWAYS">
             <font>
                <Font size="22.0" />
             </font>
          </Button>
          <Button maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Кнопка 6" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" GridPane.rowIndex="2" GridPane.vgrow="ALWAYS">
             <font>
                <Font size="22.0" />
             </font>
          </Button>
       </children>
    </GridPane>
    
    Main.java
    public class Main extends Application {
    
        @Override
        public void start(Stage primaryStage) throws Exception {
    
            Parent root =
                    FXMLLoader.load(getClass().getResource("sample.fxml"));
    
            Scene scene = new Scene(root, 300, 200);
    
            primaryStage.setTitle("JavaFX-приложение");
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    SampleController.java
    public class SampleController implements Initializable {
        @Override
        public void initialize(URL location, ResourceBundle resources) {
        }
    }
    SampleController.java
    public class SampleController implements Initializable {
        
        @FXML
        private Button button1;
        
        @FXML
        private Button button2;
        
        @Override
        public void initialize(URL location, ResourceBundle resources) {
        }
    }
    SampleController.java
    public class SampleController implements Initializable {
    
        @FXML
        private Button button1;
    
        @FXML
        private Button button2;
    
        @Override
        public void initialize(URL location, ResourceBundle resources) {
    
            button1.setOnAction(event -> {
                new Alert(Alert.AlertType.CONFIRMATION,"Вы нажали кнопку 1").showAndWait();
            });
        }
    }
    SampleController.java
    public class SampleController implements Initializable {
    
        @FXML
        private Button button1;
    
        @FXML
        private Button button2;
    
        @FXML
        protected void handleButton2(ActionEvent event) {
            new Alert(Alert.AlertType.CONFIRMATION,"Вы нажали кнопку 2").showAndWait();
        }
    
        @Override
        public void initialize(URL location, ResourceBundle resources) {
    
            button1.setOnAction(event -> {
                new Alert(Alert.AlertType.CONFIRMATION,"Вы нажали кнопку 1").showAndWait();
            });
        }
    }

    Конспект лекций

    запросы к базам данных;

  • валидация данных;

  • бизнес-логика (если модель «активна»).

  • нуждается в следующем:

    • в представлении (не может самостоятельно демонстрировать данных и результаты их обработки);

    • в контроллере (не имеет точек взаимодействия с пользователем).

  • может иметь множество различных представлений и контроллеров;

  • отвечает на запросы изменением состояния. При этом, в модель может быть встроено автоматическое оповещение «наблюдателей».

  • использует модель и представление для необходимого действия;

  • в случае «пассивной» модели – реализует бизнес-логику.

  • 3. Клиент-серверная архитектура. Создание простой RESTful веб-службы с помощью Spring Boot.

    hashtag
    Клиент-серверная архитектура

    Сервер - компьютер или программа, которая управляет ресурсами (информация, файлы, база данных) называется сервером этого ресурса или просто сервером.

    Архитектура "клиент-сервер" определяет общие принципы организации взаимодействия, где имеются серверы (узлы-поставщики некоторых специфичных функций и сервисов) и клиенты, (потребители этих сервисов).

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

    circle-info

    Более подробно про клиент-серверное взаимодействие читайте здесь -

    В рамках данного курса рассматривается так называемая "трехзвенная архитектура"

    Компоненты трехзвенной архитектуры:

    • клиент - этот компонент отвечает за представление данных конечному пользователю;

    • выделенный сервер приложений - здесь содержится бизнес-логика приложения;

    • сервер БД - предоставляет запрашиваемые данные.

    Сервер приложений (application server) – сервисная программа, которая обеспечивает доступ клиентов к прикладным программам, выполняющимся на сервере.

    Большинство серверов приложений имеют в своем составе веб-сервер. Это означает, что сервер приложений может делать все, на что способен веб-сервер. Кроме того, сервер приложений имеет компоненты и функции для поддержки сервисов уровня приложения, таких как пул соединений, поддержка транзакций и так далее.

    circle-info

    Информация о сервере приложений - . Отличия веб-сервера и сервера приложений - Подробнее про сервлеты и контейнеры сервлетов -

    hashtag
    Технология Java Servlets

    На заре развития интернета не существовало технологий для создания динамических веб-страниц. В то время сайт представлял собой набор статических заранее написанных и сверстанных страниц с помощью языка разметки HTML. Если владелец сайта хотел обновить информацию на страничке, он делал это непосредственно на своем компьютере, после чего загружал на сервер обновленную версию HTML-страницы.

    Среди предложенных решений по созданию динамических страниц, одной из первых была технология Java Servlets. В то время это была революционная технология, которая позволяла расширить возможности веб-серверов на основе модели запрос-ответ (request - response). Технология сервлетов позволяла веб-серверам обрабатывать HTTP-запросы и динамически генерировать веб-странички в зависимости от HTTP-запроса.

    circle-info

    На данный момент актуальной версией технологии является версия 4.0, спецификацию технологии смотрите .

    Несмотря на почтенный возраст, технология сервлетов претерпела серьезные изменения для того, чтобы соответствовать современной технологии разработки веб-приложений. На данный момент, технология сервлетов является наиболее часто используемой технологией для обработки HTTP запросов/откликов. Кроме того, сервлеты являются базой для почти всех Java-фреймворков, которые работают с HTTP протоколом (JSF, Struts, Spring MVC, BIRT и так далее).

    Сервлет (Servlet), по сути, является классом Java, который используется для расширения возможностей сервером, предназначенных для размещения приложений. Сервлеты могут отвечать на запросы и генерировать отклики. Базовым классом для всех сервлетов является класс javax.servlet.GenericServlet. Этот класс определяет обобщенный, независимый от протокола сервлет.

    Схема работы технологии сервлетов представлена на рисунке ниже

    1. клиент (например, веб-браузер) передает HTTP-запрос веб-серверу. В случае, если от веб-сервера требуется предоставить статический файл или какой-то ресурс (например, изображение), то он просто возвращает требуемый статический файл или ресурс;

    2. если веб-сервер не может самостоятельно обработать HTTP-запрос (например, пользователь передает какие-то данные либо требуется предоставить динамическую страницу и так далее), веб-сервер передает этот запрос web-контейнеру (его еще называют servlet-контейнером);

    3. контейнер определяет – какой сервлет может выполнить этот запрос, создает объекты классов HttpServletRequest

    hashtag
    Фреймворк Spring

    Spring – свободно-распространяемый легковесный фреймворк, призванный упростить разработку корпоративных и веб-приложений (можно использовать и для любых других типов приложений) на языке Java (является альтернативной стеку Jakarta EE).

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

    Дадим краткую характеристику некоторым модулям Spring:

    • Spring Core – ядро платформы, предоставляет базовые средства для создания приложений — управление компонентами (бинами, beans), внедрение зависимостей, MVC фреймворк, транзакции, базовый доступ к БД. В основном это низкоуровневые компоненты и абстракции. По сути, неявно используется всеми другими компонентами;

    • Spring MVC – обеспечивает архитектуру паттерна Model-View-Controller при помощи слабо связанных готовых компонентов для разработки веб-приложений;

    hashtag
    Spring Boot

    Проект Spring Boot – решение, которое позволяет вам легко создавать полноценные приложения Spring, про которые можно сказать «просто запусти».

    Spring Boot позволяет быстро создать и сконфигурировать (т.е. настроить зависимости между компонентами) приложение, упаковать его в исполняемый самодостаточный артефакт. Это то связующее звено, которое объединяет вместе набор компонентов в готовое приложение.

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

    • создание полноценных Spring-приложений;

    • встроенный сервлет-контейнер (Tomcat или Jetty);

    • обеспечивает начальные pom-файлы для упрощения конфигурации Maven;

    Изучение фреймворка Spring лучше всего начать с установки требуемого программного обеспечения и разработки тестового приложения с помощью Spring Boot.

    hashtag
    Веб-сервисы

    Всемирная паутина является готовой платформой для создания и использования распределенных систем на основе веб-служб. Веб-сервер выступает в качестве сервера приложений, к которым обращаются не конечные пользователи, а сторонние приложения. Это позволяет многократно использовать функциональные элементы, устранить дублирование кода, упростить решение задач интеграции приложений.

    Веб-служба или веб-сервис (web-service) – сетевая технология, обеспечивающая межпрограммное взаимодействие на основе веб-стандартов. W3C определяет веб-службу как «программную систему, разработанную для поддержки интероперабельного межкомпьютерного (machine-to-machine) взаимодействия через сеть».

    К моменту появления веб-служб уже существовали технологии, позволяющие приложениям взаимодействовать на расстоянии, где одна программа могла вызвать какой-нибудь другой метод в другой программе, которая при этом могла быть запущена на компьютере, расположенном в другом городе или даже стране. Это сокращенно называется RPC (Remote Procedure Calling – удаленный вызов процедур). В качестве примеров можно привести технологии CORBA, а для Java – RMI (Remote Method Invoking – удаленный вызов методов).

    Идея веб-службы заключалась в создании такого RPC, который будет упаковываться в HTTP пакеты. Такой подход стал очень популярным, т.к. HTTP был хорошо известен, прост, понятен и обеспечивал лучшее «прохождение» через различные firewall`ы. Именно с появлением веб-сервисов развилась идея SOA – сервис-ориентированной архитектуры веб-приложений (Service Oriented Architecture).

    hashtag
    Протокол HTTP

    Протокол HTTP лежит в основе обмена данными в Интернете. HTTP является протоколом клиент-серверного взаимодействия, что означает инициирование запросов к серверу самим получателем (браузером или другим клиентским приложением).

    Клиенты и серверы взаимодействуют, обмениваясь одиночными сообщениями (а не потоком данных). Сообщения, отправленные клиентом называются запросами, а сообщения, отправленные сервером, называются ответами.

    HTTP - это клиент-серверный протокол, то есть запросы отправляются какой-то одной стороной - участником обмена (user-agent). Чаще всего в качестве участника выступает веб-браузер, но им может быть кто угодно.

    Каждый запрос (request) отправляется серверу, который обрабатывает его и возвращает ответ (response).

    Участник обмена (user agent) - это любой инструмент или устройство, действующее от лица пользователя.

    На другой стороне коммуникационного канала расположен сервер, который обслуживает (serve) пользователя, предоставляя ему документы по запросу. С точки зрения конечного пользователя, сервер всегда является некой одной виртуальной машиной, полностью или частично генерирующий документ, хотя фактически он может быть группой серверов, между которыми балансируется нагрузка, то есть перераспределяются запросы различных пользователей, либо сложным программным обеспечением, опрашивающим другие компьютеры.

    hashtag
    HTTP-запросы (HTTP-request)

    Пример HTTP запроса

    Запросы содержат следующие элементы:

    • HTTP-метод, обычно глагол подобно GET, POST или существительное, как OPTIONS или HEAD, определяющее операцию, которую клиент хочет выполнить. Обычно, клиент хочет получить ресурс (используя GET) или передать значения HTML-формы (используя POST), хотя другие операции могут быть необходимы в других случаях;

    • путь к ресурсу;

    • заголовки (опционально), предоставляющие дополнительную информацию для сервера;

    hashtag
    HTTP-ответы

    Пример HTTP-ответа

    Ответы содержат следующие элементы:

    • версию HTTP-протокола;

    • HTTP код состояния, сообщающий об успешности запроса или причине неудачи;

    • сообщение состояния - краткое описание кода состояния;

    Код состояния - это трехзначное число, которое отдает сервер на запрос клиента и благодаря которому корректируется дальнейшая обработка запрашиваемого документа. За числом всегда идет краткое пояснение кода на английском языке, отделенное пробелом - первичная инструкция клиенту.

    Классы состояния - группа кодов, объединенных определенными признаками. На класс состояния указывает первая цифра в коде.

    Выделяют пять классов:

    • 1ХХ - информационные кода. Они отвечают за процесс передачи данных. Это временные коды, они информируют о том, что запрос принят и обработка будет продолжаться;

    • 2ХХ - успешная обработка. Запрос был получен и успешно обработан сервером;

    • 3ХХ - перенаправление (редирект). Эти ответы сервера гласят, что нужно предпринять дальнейшие действия для выполнения запроса. Например, сделать запрос по другому адресу;

    hashtag
    Протоколы веб-сервисов

    На сегодняшний день наибольшее распространение получили следующие протоколы реализации веб-служб:

    • SOAP (Simple Object Access Protocol) – тройка стандартов SOAP/WSDL/UDDI. Сообщения упаковываются в виде структуры, которая называется конверт (envelope), которая включает идентификатор сообщения, заголовок и тело сообщения;

    • REST (Representational State Transfer) – архитектурны стиль, который использует концепцию ресурсов и определяет операции через методы HTTP-протокола;

    • XML-RPC (XML Remote Procedure Call) – вызов удаленных процедур, использующий XML для кодирования своих сообщений и HTTP в качестве транспортного механизма.

    hashtag
    Протокол REST

    Передача состояния представления (Representational State Transfer (REST)) является архитектурным стилем, в котором веб-службы рассматриваются, как ресурсы и могут быть идентифицированы Унифицированными идентификаторами ресурсов (Uniform Resource Identifiers (URI)).

    Веб-службы, разработанные в стиле REST и с учетом ограничений REST, известны как RESTful веб-службы.

    Каждая единица информации в REST называется ресурсом и имеет однозначный URI, который является ее, своего рода, первичным ключом. То есть, например, третья книга с книжной полки будет иметь URI /book/3, а 35ая страница в этой книге – /book/3/page/35/. Отсюда и получается строго заданный формат. Причем совершенно не имеет значения, в каком формате находятся данные по адресу /book/3/page/35/ – это может быть и HTML, и отсканированная копия книги в виде jpeg-файла и документ Microsoft Word.

    Над ресурсами выполняется ряд простых четко определенных операций. В качестве протокола передачи данных используется stateless-протокол, обычно HTTP.

    При использовании протокола HTTP действия над данными выполняются с помощью HTTP-методов: GET (получить), PUT (добавить, заменить), POST (добавить, изменить, удалить), DELETE (удалить). Таким образом, действия CRUD (Create-Read-Update-Delete) могут выполняться как со всеми 4-мя методами, так и только с помощью GET и POST. Примеры запросов:

    • GET /book/ – получить список всех книг;

    • GET /book/3 – получить книгу номер 3;

    • PUT /book/ – добавить книгу (данные в теле запроса);

    Как правило, необязательно поддерживать все методы, но, как правило, веб-служба должна поддерживать:

    • GET – используется для получения существующих ресурсов;

    • POST – используется для создания/обновления нового ресурса;

    • PUT – используется для обновления/замены ресурса;

    Кроме этого, служба может поддерживать такие методы как PATCH (обновление части ресурса), HEAD (возвращение заголовка ресурса, т.е. метаданных) и т.д.

    hashtag
    Практическая часть

    hashtag
    Установка программного обеспечения

    Для выполнения домашнего задания нам понадобится следующее программное обеспечение:

    • JDK - ;

    • приложение Postman - ;

    • среда разработки, которая поддерживает Spring (например, IntelliJ IDEA Ultimate Edition или дистрибутив Eclipse под названием Spring Tool Suite – ) либо любая другая IDE с поддержкой Java и Maven.

    hashtag
    Создание Spring Boot проекта

    Существует несколько способов создать Spring Boot проект. Из наиболее простых способов можно выделить:

    • генерация готового проекта на сайте (проект Spring Initializr);

    • создание проекта средствами IDE.

    Создадим проект с помощью мастера Intellij IDEA. Создадим новый Spring Boot проект (выберите пункт Spring Initializr). Необходимо указать JDK, метаданные проекта, а также выбрать из списка модулей нужные нам модули Spring

    Для выполнения задания нам необходимо выбрать web-модуль. Панель выбранных компонентов будет иметь следующий вид:

    После окончания работы мастера создания проектов, мы получим стартовый проект Spring Boot. Рассмотрим структуру проекта и обозначим ключевые файлы:

    • HotelApplication.java - стартовый класс Spring Boot приложения;

    • application.properties - файл с настройками приложения. В нем можно переопределить настройки по умолчанию;

    • pom.xml - POM-файл проекта. Используется сборщиком Maven.

    POM-файл (Project Object Model) – это XML-файл, который содержит информацию о деталях проекта, и конфигурации для создания проекта на Maven. Он всегда находится в базовом каталоге проекта. Во время выполнения задач, Maven ищет pom-файл в базовой директории проекта. Он читает его и получает необходимую информацию, после чего выполняет задачи.

    Корневым элементом является элемент <project>. Внутри тега project содержится основная и обязательная информация о проекте.

    Зависимости (dependency) – это те библиотеки, которые непосредственно используются в проекте для компиляции кода или его тестирования.

    Мы создаем RESTful веб-службу с помощью Spring Boot, поэтому нам нужно «подтянуть» для нашего проекта различные Spring-модули (библиотеки с классами, jar-файлы).

    В обычных проектах нам бы было необходимо добавлять каждую зависимость вручную, но Spring Boot позаботился о нас и предоставил нам своего рода «мета-зависимости». Смысл их в том, что Spring Boot понимает, что если вы создаете web-приложение то вам нужен примерно одинаковый набор jar-файлов, поэтому чтобы не писать каждый jar-файл отдельно, мы указываем одну зависимость, а она уже «подтянет» за нас другие отдельные зависимости для создания веб-приложения.

    hashtag
    Запуск приложения

    Теперь давайте сразу запустим приложение. Убедимся, что приложение запущено успешно

    перейдем в браузер и попробуем зайти на сайт.

    Как видите, Spring Boot приложение успешно запущено. Так как Spring Boot берет на себя большую часть рутинной работы по созданию и запуску приложения, давайте разберемся, что же происходит, когда мы запускаем приложение:

    1. Устанавливается конфигурация приложения по умолчанию;

    2. Запускается контекст приложения Spring (Spring application context) – это контейнер для кода, который работает на сервере (службы, контроллеры и т.д.). Все приложения Spring имеют этот контекст, который запускается при запуске приложения. Spring Boot создает этот контекст при запуске приложения;

    hashtag
    Структура enterprise-приложения Spring

    Простое приложение Spring имеет трехслойную структуру:

    • Web layer – верхний слой приложения. Он отвечает за обработку ввода пользователя и возврат корректного ответа. Также веб-слой отвечает за обработку исключений, которые могут выбрасываться в других слоях приложения. Так как веб-слой является точкой входа в приложение, он также отвечает за аутентификацию и является первой линией защиты приложения;

    • Service layer – слой сервисов, находится ниже веб-слоя. Этот слой содержит сервисы приложения и инфраструктуры. Сервисы приложения предоставляют публичный API сервисного слоя. Они также отвечают за транзакции и авторизацию. Инфраструктурные сервисы содержат код для взаимодействия с внешними ресурсами, такими как файловая система, базы данных, почтовые сервера и так далее. Часто эти сервисы используются несколькими сервисами приложения;

    hashtag
    Разработка Web Layer

    Для обработки запросов и возврата данных необходимо предусмотреть соответствующие контроллеры REST-запросов, которые и будут составлять наш веб-слой.

    Контроллер – это java-класс, методы которого призваны обрабатывать HTTP-запросы. Отличие обычного контроллера от REST-контроллера заключается в том, что в REST-контроллере каждый метод класса возвращает данные вместо представления. Рассмотрим пример простого REST-контроллера. Создадим в проекте пакет controllers, внутри которого создадим класс HelloController.

    Обратите внимание, что мы пометили класс аннотацией @RestController. Таким образом, мы даем знать Spring, что это не просто класс, а контроллер REST-запросов. В классе создадим метод, который будет возвращать строку.

    Говорят, что методы контроллера «отображаются» на HTTP-запросы. Это значит, что при поступлении определенного HTTP-запроса (с определенным URL и HTTP-методом), будет вызван определенный метод контроллера, который вернет некоторые данные. Этим данные будут упакованы в HTTP-ответ и высланы обратно клиенту.

    Нам необходимо сделать так, чтобы наш созданный метод был вызван, когда на сервер поступит HTTP-запрос с определенным URL, например http://localhost:8080/hello. Для этого необходимо пометить метод аннотацией @GetMapping c параметром (“/hello”) – часть URL, на который будет отображаться данный метод.

    circle-info

    Для каждого из четырех основных HTTP-метода предусмотрена своя аннотация (@GetMapping, @PostMapping, @PutMapping, @DeleteMapping). Метод, помеченный определенной аннотацией, обрабатывает запросы только с определенным HTTP-методом.

    Запустим сервер, заходим на и видим строку с ответом.

    Что произошло? Строка «hello» была помещена в тело HTTP-ответа, браузер получил text/plain с содержимым «hello» и просто вывел его на экран.

    hashtag
    Передача параметров в REST-запросах

    Очень часто клиенту необходимо вместе с запросом передать некоторые параметры запроса, которые уточняют и конкретизируют запрос.

    Параметры запроса можно передать несколькими способами. Рассмотрим следующие способы:

    • указание параметра в URL-пути (localhost:8080/rooms/256);

    • указание параметра в строке запроса, которая идет после URL-пути и отделяется символом ? (localhost:8080/rooms?id=256&param2=value2);

    • передача параметров в теле запроса (часто используется для передачи заполненной пользователем формы или передачи данных в формате JSON).

    Рассмотрим, каким образом можно получить и обработать параметры запроса, переданные тем или иным способом.

    hashtag
    Указание параметра в URL-пути

    При создании endpoint, в аннотации необходимо указать вариативную часть и назначить ей идентификатор

    Далее необходимо предусмотреть входной аргумент метода, куда Spring запишет значение вариативной части и указать аннотацию @PathVariable для этой переменной. Также необходимо указать идентификатор, который вы указали в аннотации @GetMapping.

    В рамках одного запроса может быть несколько вариативных частей, которые можно считать и обработать

    hashtag
    Указание параметра в строке запроса

    В этом случае, для каждого параметра запроса создается входной аргумент, указывается аннотация @RequestParam, а также указывается имя параметра.

    hashtag
    Передача параметров в теле запроса

    Если в качестве клиента выступает браузер пользователя, данные от клиента на сервер передаются в виде полей формы, которые заполняет пользователь браузера. В этом случае параметры передаются в теле запроса с помощью метода POST.

    Форма может иметь следующие MIME-типы:

    • application/x-www-form-urlencoded: значения кодируются в кортежах с ключом, разделенных символом '&', с '=' между ключом и значением. Не буквенно-цифровые символы - : это причина, по которой этот тип не подходит для использования с двоичными данными (вместо этого используйте multipart/form-data);

    • multipart/form-data: каждое значение посылается как блок данных ("body part"), с заданными пользовательским клиентом разделителем ("boundary"), разделяющим каждую часть. Эти ключи даются в заголовки

    Для обработки данных формы необходимо создать входной аргумент для каждого параметра, для каждого входного аргумента указать аннотацию @RequestParam, а также имя параметра.

    circle-info

    Существует несколько более простых способов получения данных формы, но в данном курсе они не рассматриваются. Вышеуказанный способ является самым простым и понятным на данном этапе изучения курса.

    hashtag
    Передача данных между клиентом и сервером.

    Так как язык Java является ОО языком, нам было бы удобно работать с входящими и исходящими данными в объектном виде - было бы здорово, если бы REST-контроллер возвращал бы данные в виде объекта некоторого класса, а не в виде набора полей со значениями. Также было бы здорово, чтобы мы могли просто возвращать клиенту объект или коллекцию объектов некоторых классов без необходимости формировать Map из полей и значений.

    Для реализации этого функционала, в Spring используется механизм сериализации и десериализации.

    Сериализация - это преобразование объекта в последовательность байтов, так что объект можно легко сохранить в постоянное хранилище или передать по каналу связи. Затем поток байтов можно десериализовать - преобразовать в реплику исходного объекта.

    Язык Java предоставляет стандартный механизм Java Serialization API для создания сериализуемых объектов, однако, он нам не подходит, так как ограничивает возможности для использования различных языков и технологий на стороне клиента и сервера.

    Мы можем использовать сторонние библиотеки для сериализации объекта с помощью формата XML или JSON.

    Использование формата JSON () является более предпочтительным. Для сериализации и десериализации в Spring по-умолчанию используется библиотека Jackson.

    circle-info

    Библиотека Jackson позволяет гибко настроить процесс сериализаци и десериализации, однако, в рамках данного курса мы будем использовать стандартные механизмы сериализации и десериализации, чтобы уделять этому процессу как можно меньше внимания.

    Рассмотрим ситуацию, когда нам необходимо вернуть клиенту данные в объектном виде. Создадим класс с несколькими полями, создадим объект и вернем его в качестве результата GET-запроса.

    circle-exclamation

    Обратите внимание, что в код класса Room не зря были включены геттеры и сеттеры. Их наличие обязательно для сериализации и десериализации!

    Используем Postman для эмуляции клиента, сделаем GET-запрос и получим следующий результат

    Как мы видим, поля объекта были сериализованы с помощью формата JSON. Теперь клиент, после получения этих данных, сможет с помощью процесса десериализации получить объект и удобно работать с ним.

    Теперь рассмотрим обратную ситуацию. Клиент делает POST-запрос и передает в теле запроса данные о новом студенте.

    На стороне сервера создаем класс Student с соответствующими полями.

    Создаем конечную точку для обработки запроса. Обратите внимание, что мы используем аннотацию @RequestBody.

    hashtag
    Конфигурация HTTP-response с помощью класса ResponseEntity

    Далеко не всегда ответ сервера состоит в возврате какого-то значения или какого-то объекта. Очень часто необходимо вернуть ответ с определенным HTTP-кодом и сообщением об ошибке, указать определенный заголовок и так далее.

    В этом случае необходимо использовать класс ResponseEntity. Класс ResponseEntity является оберткой для ответа и дополнительно для HTTP заголовков и кода статуса. Он является обобщенным, что позволяет использовать любой тип в качестве тела ответа.

    Подробную информацию по поводу ResponseEntity читайте здесь - .

    hashtag

    Student.java
    public class Student {
    
        private String firstName;
        private String lastName;
        private String group;
    
        public Student(String firstName, String lastName, String group) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.group = group;
        }
    
        @Override
        public String toString() {
            return "Student{" +
                    "firstName='" + firstName + '\'' +
                    ", lastName='" + lastName + '\'' +
                    ", group='" + group + '\'' +
                    '}';
        }
    }
    
    Controller.java
    public class Controller implements Initializable {
    
        @Override
        public void initialize(URL location, ResourceBundle resources) {
        }
    
        @FXML
        public void add(ActionEvent event) {
            // Обработчик кнопки "Добавить"
        }
    
        @FXML
        public void delete(ActionEvent event) {
            // Обработчик кнопки "Удалить"
        }
    }
    Main.java
    public class Main extends Application {
    
        private List<Student> list;
    
        @Override
        public void init() throws Exception {
            list = new ArrayList<>();
        }
    
        public void addStudent(Student student) {
            list.add(student);
        }
    
        public void deleteStudent(Student student) {
            list.remove(student);
        }
    
    
        @Override
        public void start(Stage primaryStage) throws Exception{
            Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
            primaryStage.setTitle("Hello World");
            primaryStage.setScene(new Scene(root, 300, 275));
            primaryStage.show();
        }
    
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    StudentFacade.java
    public class StudentFacade {
        private List<Student> studentList;
    
        public StudentFacade() {
            studentList = new ArrayList<>();
        }
    
        public void addStudent(Student student) {
            studentList.add(student);
        }
    
        public void deleteStudent(Student student) {
            studentList.remove(student);
        }
    }
    Main.java
    public class Main extends Application {
    
        private StudentFacade facade;
    
        @Override
        public void init() throws Exception {
            facade = new StudentFacade();
        }
    
        public void addStudent(Student student) {
            facade.addStudent(student);
        }
    
        public void deleteStudent(Student student) {
            facade.deleteStudent(student);
        }
        
        @Override
        public void start(Stage primaryStage) throws Exception{
            Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
            primaryStage.setTitle("Hello World");
            primaryStage.setScene(new Scene(root, 300, 275));
            primaryStage.show();
        }
        
        public static void main(String[] args) {
            launch(args);
        }
    }
    Main.java
        @Override
        public void start(Stage primaryStage) throws Exception {
    
            FXMLLoader loader = new FXMLLoader(getClass().getResource("../view/sample.fxml"));
            Parent root = loader.load();
    
            primaryStage.setTitle("Студенты");
            primaryStage.setScene(new Scene(root, 300, 275));
            primaryStage.show();
        }
    Controller.java
    public class Controller implements Initializable {
    
        private Main main;
        
        public void getMainController(Main main) {
            this.main = main;
        }
        
        ...
    }
    Main.java
        @Override
        public void start(Stage primaryStage) throws Exception {
    
            FXMLLoader loader = new FXMLLoader(getClass().getResource("../view/sample.fxml"));
            Parent root = loader.load();
    
            Controller controller = loader.getController();
            controller.getMainController(this);
            
            primaryStage.setTitle("Студенты");
            primaryStage.setScene(new Scene(root, 300, 275));
            primaryStage.show();
        }
    Controller.java
        @FXML
        public void add(ActionEvent event) {
            // Создаем объект студента
            Student student = new Student(
                    String.valueOf(new Random().nextInt(100)),
                    String.valueOf(new Random().nextInt(100)),
                    String.valueOf(new Random().nextInt(100))
            );
    
            // Вызываем метод добавления студента
            main.addStudent(student);
        }
    StudentFacade.java
    public class StudentFacade extends Observable {
        private List<Student> studentList;
    
        public StudentFacade() {
            studentList = new ArrayList<>();
        }
    
        public void addStudent(Student student) {
            studentList.add(student);
            setChanged();
            notifyObservers(studentList);
        }
        public void deleteStudent(Student student) {
            studentList.remove(student);
            setChanged();
            notifyObservers(studentList);
        }
    }
    public class ListViewObserver extends ListView<Student> implements Observer {
        @Override
        public void update(Observable o, Object arg) {
            if (o instanceof StudentFacade) {
                // В параметре arg передан список для отображения
                List<Student> list = (List<Student>) arg;
                getItems().clear();
                getItems().addAll(list);
            }
        }
    }
    Main.java
        public void bindObserverToFacade(Observer o) {
            facade.addObserver(o);
        }
    Controller.java
    public class Controller implements Initializable {
    
        private Main main;
    
        @FXML
        private ListViewObserver list;
    
        public void getMainController(Main main) {
            this.main = main;
            main.bindObserverToFacade(list);
        }
        
        ...
    }
    и
    HttpServletResponse
    , создает thread, создает объект класса сервлета и передает ему объекты классов
    HttpServletRequest
    и
    HttpServletResponse
    ;
  • Контейнер вызывает метод сервлета service(), который вызывает соответствующий HTTP-запросу метод (например, если запрос был HTTP GET, то будет вызван метод doGet(), подробнее этот вопрос будет разбираться далее), которому, в качестве аргументов, передает объекты классов HttpServletRequest и HttpServletResponse;

  • Соответствующий метод (например, метод doGet()) возвращает динамическую страницу внутри объекта класса HttpServletResponse, ссылку на который имеет контейнер;

  • После этого поток завершается, контейнер конвертирует объект класса HttpServletResponse в HTTP-отклик (HTTP response) и отдает его веб-серверу, который возвращает его клиенту.

  • Spring Data – обеспечивает доступ к данным: реляционные и нереляционные БД, KV хранилища и т.п.;
  • Spring Cloud – используется для микросервисной архитектуры;

  • Spring Security – авторизация и аутентификация, доступ к данным, методам и т.п. OAuth, LDAP, и различные провайдеры.

  • используется автоконфигурация, где это возможно;
  • используется принцип «convention over configuration». Для большинства конфигураций не нужно ничего настраивать.

  • для некоторых методов, таких как POST, тело метода, которое содержит отправленный ресурс.

    опционально: тело, содержащее пересылаемый ресурс.

    4ХХ - ошибка клиента. Это значит, что запрос не может быть выполнен на стороне клиента;

  • 5ХХ - ошибка сервера. Эти коды возникают из-за ошибок на стороне сервера. В данном случае клиент сделал все правильно, но сервер не может выполнить запрос. Для кодов этого класса сервер обязательно показывает сообщение, что не может обработать запрос и по какой причине.

  • POST /book/3 – изменить книгу (данные в теле запроса);
  • DELETE /book/3 – удалить книгу.

  • DELETE – используется для удаления ресурса.
    Выполняется сканирование пути к классам (class path scan). Чтобы добавить код в Spring Boot, необходимо создать свои классы и аннотировать их определенным образом. Например, если вы хотите добавить контроллер, вы создаете класс и аннотируете его с помощью аннотации @Controller и так далее. То есть, вы как бы помечаете ваши классы, что это контроллер, это сервис, это еще что-то. Spring сканирует эти классы и, в зависимости от нашего маркера, он работает с этими классами по-разному. То есть Spring сканирует ваш код и ищет классы с этими аннотациями (помимо маркеров, обычно в аннотациях содержатся другие метаданные, которые дают уточняющую информацию для Spring);
  • Запускается Tomcat-сервер. Мы как раз зашли на сервер через URL и получили страницу с ошибкой, так как на сервере не был предусмотрен обработчик запроса с таким URL. Мы не скачивали Tomcat и не устанавливали его – все за нас сделал Spring Boot.

  • Repository layer – самый нижний слой приложения. Он отвечает за взаимодействие с используемыми хранилищами данных.

    Content-Disposition
    каждой части
    text/plain
    .
    http://bit.ly/2qmKbHkarrow-up-right
    http://bit.ly/2qt2Q4tarrow-up-right
    http://bit.ly/2qlUaNearrow-up-right
    http://bit.ly/2Q9GAaParrow-up-right
    здесьarrow-up-right
    http://www.oracle.com/technetwork/java/javase/downloads/index.htmlarrow-up-right
    https://www.getpostman.com/downloads/arrow-up-right
    https://spring.io/toolsarrow-up-right
    https://start.spring.io/arrow-up-right
    http://localhost:8080/helloarrow-up-right
    percent encodedarrow-up-right
    http://bit.ly/32ZelBqarrow-up-right
    https://www.baeldung.com/spring-response-entityarrow-up-right
    Схема работы технологии Java Servlets

    6. Фреймворк Spring MVC

    hashtag
    1. Теоретические сведения

    Spring MVC – веб-фреймворк, призванный упростить разработку веб-приложений. Опираясь на шаблон модель–представление–контроллер (Model-View-Controller, MVC), фреймворк Spring MVC помогает строить веб-приложения, столь же гибкие и слабо связанные, как сам фреймворк Spring.

    Схема работы фреймворка Spring MVC

    Схема работы фреймворка выглядит следующим образом:

    Краткое описание схемы работы Spring MVC звучит следующим образом:

    • вначале DispatcherServlet (диспетчер сервлетов) получает запрос, далее он смотрит свои настройки, чтобы понять какой контроллер использовать (на рисунке Handler Mapping);

    • после получения имени контроллера запрос передается на обработку в этот контроллер (на рисунке Controller). В контроллере происходит обработка запроса и обратно посылается ModelAndView (модель — сами данные; view (представление) — как эти данные отображать);

    Давайте рассмотрим этот процесс более подробно:

    1. Когда запрос покидает браузер, он несет в себе информацию о требовании пользователя. По крайней мере, запрос будет нести в себе запрошенный URL. Но он может также нести дополнительные данные, такие как информация из формы, заполненной пользователем;

    2. Первой остановкой на пути запроса является DispatcherServlet. Как и большинство веб-фреймворков на языке Java, фреймворк Spring MVC пропускает все входящие запросы через единственный сервлет входного контроллера. Входной контроллер (front controller) является типичным шаблоном проектирования веб-приложений, где единственный сервлет берет на себя ответственность за передачу всех запросов остальным компонентам приложения, выполняющим фактическую их обработку. В Spring MVC входным контроллером является DispatcherServlet;

    Рассмотренную выше схему работы фреймворка можно также представить следующей диаграммой

    hashtag
    2. Создание и настройка Spring Boot проекта

    Создадим новый Spring Boot проект, выберем следующие модули

    В новом проекте обратите внимание на структуру папок. В папке resources\templates будут содержаться html-файлы с использованием шаблонизатора Thymeleaf.

    Создадим файл index.html. Обратите внимание, что в теге html необходимо указать пространство имен th для подключения тегов Thymeleaf.

    В проекте Spring Boot MVC страница index будет автоматически передана при переходе на URL "/". Запустим проект и зайдем в браузер.

    hashtag
    3. Создание контроллера, переход между страницами

    Создадим две html-страницы для нашего проекта.

    Создадим класс контроллера, который обрабатывает GET запросы с URL "/" и "/add_student".

    В файле index.html добавим ссылку для кнопки "Добавить студента" . Для формирования ссылки используем тег th:href. Для указания пути относительно домена используем комбинацию @{~}.

    Проверим работу приложения в браузере

    Нажмем на кнопку "Добавить студента"

    hashtag
    4. Получение данных формы

    Для обработки формы необходимо выполнить следующую последовательность действий:

    1. Создать объект, поля которого будут содержать данные формы. В нашем случае создадим класс Student;

    2. Передать пустой объект Student при переходе на страницу формы;

    3. В полях формы настроить соответствие между полями формы и полями объекта класса Student;

    Создадим класс Student

    Изменим метод контроллера, который обрабатывает URL "/add_student". Передадим пустой объект студента

    Изменим файл add_student.html. В полях формы добавим привязку к полям объекта, в теге формы укажем название объекта (исходя из метода контроллера он должен называться "student", а также укажем URL для отправки данных формы).

    Создадим метод контроллера, который принимает POST-запрос с URL "/add_student". Данный метод будет обрабатывать результат заполнения формы.

    Изменим класс Student, превратив его в сущность

    Создадим интерфейс репозитория

    Добавим класс сервиса для работы с DAO

    Изменим класс контроллера. После получения результатов заполнения формы, данные будут сохраняться в базе данных.

    hashtag
    5. Вывод данных из БД

    ля вывода данных на странице index, необходимо выполнить следующие действия:

    1. обратиться к базе данных для получения списка студентов;

    2. передать список студентов в View;

    3. предусмотреть вывод полей каждого студента в нужных ячейках таблицы.

    Модифицируем метод контроллера, который отвечает за обработку запроса "\"

    Модифицируем страницу index.html. Добавим вывод полей каждого объекта типа Student в таблице

    Запустим приложение и посмотрим на результат. Изначально таблица студентов пустая

    Добавляем нового студента

    После добавления студента, нас перенаправляют на страницу index

    hashtag
    6. Валидация данных формы

    Создадим проект с индексной страницей, на которой расположена форма добавления нового студента.

    Изначально, форма не имеет средств валидации, то есть мы не можем отследить корректность заполнения формы.

    Spring предоставляет несколько инструментов для реализации валидации формы, воспользуемся библиотекой Bean Validation API

    circle-info

    Информацию по поводу использования библиотеки можно найти (см. раздел 8 мануала)

    Добавим библиотеку в список зависимостей в файле pom.xml

    Будем использовать механизм встроенных ограничений. Алгоритм использования встроенных ограничений следующий - с помощью аннотаций необходимо указать над полем класса-сущности требуемые параметры валидации и другие параметры. В нашем случае, сущностью выступает класс Student. Добавим необходимые ограничения для полей сущности.

    Как мы видим, все достаточно просто и наглядно.

    Далее, нам необходимо модифицировать контроллеры и реализовать следующий функционал:

    • указать, что объект типа Student должен пройти валидацию;

    • получить результаты валидации объекта;

    • если объект не прошел валидацию - не добавлять объект в хранилище, выдать сообщение об ошибке в консоль.

    Нам необходимо модифицировать метод контроллера, который обрабатывает данные формы. Указываем аннотацию @Valid, которая говорит о том, что полученный объект необходимо подвергнуть валидации. Далее указываем аргумент типа BindingResult, который хранит информацию о результате валидации. С помощью метода hasErrors() получаем результат валидации объекта.

    При попытке отправить пустую форму, получаем сообщение в консоли

    Последний шаг - необходимо предоставить пользователю информацию о том, что то или иное поле формы не прошло валидацию.

    Самый простой способ проинформировать пользователь - показать сообщение об ошибке около поля, которое не прошло валидацию. Чтобы реализовать данный функционал, перейдем в шаблон index.html.

    Рассмотрим поле "Фамилия". Сообщение об ошибке мы разместим снизу поля. Добавим соответствующий элемент <small> в HTML-макет.

    Используем тег th:if. Если выражение внутри тега равно true, то элемент <small> будет показан на экране, если false - будет скрыт.

    Выражение ${fields.hasErrors('lastName)} означает, есть ли ошибки валидации для поля lastName? Если ошибки есть - поле будет показано. Текст ошибки выводим с помощью атрибута th:errors.

    Добавляем элементы для вывода ошибок для остальных полей формы. Проверяем результат

    Ниже представлен листинг классов и файлов

    hashtag
    7. Загрузка файлов на сервер

    4. Внедрение зависимостей (Dependency Injection)

    Любое мало-мальски серьезное приложение состоит из нескольких классов, которые взаимодействуют друг с другом, чтобы реализовывать бизнес-логику. Обычно, каждый объект отвечает за получение ссылок на другие объекты, с которыми он взаимодействует (такие другие объекты называются зависимостями, dependencies). Такой подход может привести к созданию тесно связанного кода, который тяжело тестировать.

    Рассмотрим небольшой участок кода, который состоит из класса User и класса Sender.

    В результате мы получим тесно связанный код – класс User теперь напрямую зависит от класса Sender. Таким образом, если мы создадим класс EmailSender, который будет отсылать сообщения по электронной почте, то чтобы использовать объект класса EmailSender, нам придется изменять код класса User. К тому же, тестирование метода sendMessage() будет затруднительным.

    Безусловно, мы не можем избежать связывания вообще, т.к. объектно-ориентированное программирование подразумевает взаимодействие множества объектов различных классов, программа из одного класса не имеет смысла. С другой стороны, нам необходимо избегать тесного связывания (tight coupling) классов, так как такой код тяжело повторно использовать, тестировать и тяжело понять, как это всё вместе работает.

    В противовес тесному связыванию кода существует принцип слабо связного (loose coupling) кода. Слабая связность означает, что изменения, вносимые в один класс, повлекут за собой небольшие изменения в другие классы, что упростит тестирование, рефакторинг, повторное использование кода. Приложение с использованием принципа слабо связного кода легче модифицируется и поддерживается.

    hashtag
    Инверсия контроля

    Одним из приемов для написания слабо связного кода является принцип инверсии управления (Inversion of Control, IoC). Он заключается в том, что жизненным циклом (созданием, вызовом методов и уничтожением) ваших объектов управляете не вы сами, а некий сторонний код. Отсюда и термин «инверсия» – не я управляю кодом, а сторонний код управляет моими классами. Он решает, когда создавать объекты моих классов, когда вызывать их методы и когда уничтожать объекты.

    На принципе инверсии управления базируется работа всех фреймворков.

    circle-info

    Подробно читайте про инверсию контроля здесь или здесь .

    Отличие библиотеки от фреймворка состоит в том, что библиотека – это по существу набор функций, организованных в классы, которые вы можете вызывать по мере надобности. Каждый вызов выполняет некоторую работу и возвращает управление обратно пользователю.

    С другой стороны, фреймворк воплощает в себе некоторый абстрактный дизайн приложения со своим поведением. Для того, чтобы использовать его, вы должны добавить свой код в различные места фреймворка, либо через наследование, либо подключив свой собственный класс. Код фреймворка впоследствии будет вызывать ваш код.

    hashtag
    Внедрение зависимости

    Одной из реализаций принципа инверсии управления является внедрение зависимости (Dependency Injection, DI). Это принцип заключается в том, что зависимости класса не создаются или ищутся в самом классе, а внедряются (inject) извне некоторым другим внешним источником (например, каким-то другим объектом). В статье Мартина Фаулера «Inversion of Control Containers and the Dependency Injection pattern» этот объект называется «сборщиком» (an assembler), а сейчас его обычно называют контейнером (container) или IoC-контейнером (IoC-container).

    circle-info

    Статью Мартина Фаулера (читать обязательно) читайте здесь или здесь (, ).

    В общем случае, IoC-контейнер – это некоторый программный код (фреймворк, отдельный класс), который осуществляет внедрение зависимостей в приложении и, насколько это возможно, упрощает данный процесс.

    Как правило, внедрение зависимости осуществляется через:

    • конструктор класса (constructor injection);

    • поле класса (field injection);

    • входной аргумент метода (method injection), то есть через сеттер.

    Внедрение через статические поля и методы не рекомендуется.

    Фреймворк Spring, прежде чем стать многофункциональной платформой, изначально разрабатывался как IoC-контейнер для упрощения разработки JavaEE-приложений.

    В приложениях на основе фреймворка Spring прикладные объекты располагаются внутри контейнера Spring. Как показано на рисунке, контейнер создает объекты, связывает их друг с другом, конфигурирует и управляет их полным жизненным циклом, от зарождения до самой их смерти (или от оператора new до вызова метода finalize()).

    Классы, которыми управляет Spring-контейнер, называются бинами (bean) или компонентами. Контейнер создает, связывает между собой, а также уничтожает бины.

    Фреймворк Spring имеет не один контейнер. В его состав входят несколько реализаций контейнера, которые подразделяются на два разных типа.

    Фабрики компонентов (bean factories) (определяются интерфейсом org.springframework.beans.factory.BeanFactory) – самые простые из контейнеров, обеспечивающие базовую поддержку DI.

    Контекст приложений (application contexts) (определяется интерфейсом org.springframework.context.ApplicationContext) основан на понятии фабрик компонентов и реализует прикладные службы фреймворка, такие как возможность приема текстовых сообщений из файлов свойств и возможность подписывать другие программные компоненты на события, возникающие в приложении.

    С фреймворком Spring можно работать, используя и фабрики компонентов, и контексты приложений, но для большинства приложений фабрики компонентов часто оказываются слишком низкоуровневым инструментом. Поэтому контексты приложений выглядят более предпочтительно, чем фабрики компонентов.

    В составе Spring имеется несколько разновидностей контекстов приложений. Три из них используются наиболее часто:

    • ClassPathXmlApplicationContext – загружает определение контекста из XML-файла, расположенного в библиотеке классов (classpath), и обрабатывает файлы с определениями контекстов как ресурсы;

    • FileSystemXmlApplicationContext – загружает определение контекста из XML-файла в файловой системе;

    Давайте перепишем наш код, чтобы подготовить его к использованию IoC-контейнера Spring. Руководствуясь принципом Dependency Inversion (не путать с Dependency Injection, это разные принципы), создадим интерфейс Sender, чтобы не привязываться к конкретной реализации отправщика сообщений.

    Создадим класс TwitterSender, который реализует данный интерфейс.

    Модифицируем класс User

    Обратите внимание на разницу – мы теперь не сами создаем объект зависимости, а получаем его «извне» с помощью аргумента конструктора либо с помощью сеттера. Использование интерфейса позволяет легко использовать разные реализации отправщик сообщений. Еще одним бонусом является удобство проведения тестирования методов класса User, так как вместо настоящего отправщика сообщений мы можем подставить специальный мок-объект (mock object), который будет имитировать работу настоящего отправщика.

    hashtag
    Внедрение зависимости (wiring)

    Итак, у нас есть соответствующие классы, теперь необходимо связывать это все воедино с помощью IoC-контейнера. Каким образом передать объект TwitterSender объекту User?

    Процесс создания связи между компонентами приложения обычно называют wiring (в русской версии книги Spring in Action этот термин переводят как связывание, не путайте с сильным и слабым связыванием, которое переводится как tight coupling и loose coupling).

    Подключим библиотеки Spring, которые нужны для связывания компонентов. Если вы используете Maven в качестве сборщика, от откройте pom-файл и добавьте следующие зависимости (на момент проведения занятия актуальная версия библиотек была 5.0.7, в вашем случае актуальная версия может быть другой)

    Важное отступление. Вы можете не добавлять библиотеку spring-core в pom-файл явно, код все равно будет работать. Это связано с тем, что spring-context не может работать без spring-core и Maven автоматически загрузит spring-core в любом случае, укажете ли вы ее в pom-файле или нет. В этом случае библиотека spring-core называется транзитивной зависимостью.

    circle-info

    Транзитивная зависимость - это зависимость, которая требуется для работы вашей прямой зависимости.

    Такой механизм позволяет избежать ручного добавления в pom-файл всего графа зависимостей - вы просто указываете прямые зависимости, а Maven сделает все остальное.

    Итак, вернемся к связыванию компонентов.

    Важный момент, который необходимо запомнить при работе с контейнером - любой контейнер необходимо сконфигурировать. То есть, на плечи разработчика ложится обязанность указать контейнеру, какие компоненты создать и как их связать вместе.

    Spring предлагает три способа связывания компонентов:

    • явная конфигурация с помощью XML-файлов;

    • явная конфигурация с помощью классов Java;

    • неявное обнаружение бинов и автоматическое связывание.

    В данном случае нет "самого лучшего" способа связывания, все три способа имеют право на жизнь. В данном занятии мы рассмотрим конфигурацию с помощью классов Java и автоматическое связывание.

    Конфигурация с помощью классов Java

    Для начала создадим класс, в котором будет осуществляться конфигурация. Создадим пакет config и класс AppConfig. Так как в Spring может использоваться несколько способов связывания компонентов, то желательно пометить класс аннотацией @Configuration - такая аннотация говорит контейнеру, что этот класс является классом конфигурации.

    Конфигурация в классе осуществляется с помощью методов и аннотаций. Добавим в класс следующий метод

    Пометив метод аннотацией @Bean, мы говорим что данный метод возвращает объект, который который должен быть зарегистрирован как бин в контексте приложения Spring (то есть, в нашем IoC-контейнере). Таким образом, мы фактически объявляем бин в нашем контейнере. Название бина будет совпадать с названием метода, в нашем случа бин будет называться twitterSender.

    Теперь добавим еще один метод

    Объявляем еще один бин User и в методе осуществляем связывание бинов. В нашем случае мы осуществляем связывание через конструктор (constructor injection).

    Таким образом, мы объявили два бина - twitterSender и user, после чего связали их с помощью constructor injection.

    Теперь модифицируем класс Main, создадим контейнер и попробуем использовать класс User.

    Итак, сначала мы создали объект контейнера. В качестве реализации мы используем класc AnnotationConfigApplicationContext, который является реализацией интерфейса ApplicationContext, которая позволяет регистрировать аннотированные классы конфигурации. В нашем случае классом конфигурации является класс AppConfig, объявленный с помощью аннотации @Configuration. После того как вы зарегистрируете указанный класс, также регистрируются все типы bean-компонентов, возвращаемые с помощью методов, которые аннотируются с помощью @Bean.

    После создания контейнера и загрузки конфигурации, используем класс User. Обратите внимание, что мы не сами создаем объект класса User и внедряем зависимости, а мы просто получаем объект из контейнера, с помощью метода getBean(). После того, как мы получили ссылку на объект, вызываем метод send() и получаем работающий класс User. Проверим работу приложения.

    circle-info

    Неплохой материал по поводу конфигурации с помощью классов можно почитать и .

    Таким образом мы реализовали связывание бинов с помощью контейнера Spring и конфигурации с помощью Java-классов. Теперь давайте рассмотрим автоматическое связывание.

    hashtag
    Автоматическое связывание

    Способ автоматического связывания является наиболее простым в использовании.

    Автоматическое связывание в Spring реализуется с помощью двух механизмов:

    • сканирование компонентов (component scanning) – механизм, с помощью которого Spring обнаруживает и создает экземпляры компонентов;

    • автосвязывание (autowiring) – механизм, с помощью которого Spring автоматически «удовлетворяет» зависимости компонентов (to satisfy a dependency).

    Совместная работа этих механизмов обеспечивает минимальное явное конфигурирование контейнера.

    Перепишем наш код для использования автоматического связывания. Для того, чтобы механизм сканирования компонентов обнаружил наши классы-бины, необходимо пометить их с помощью аннотации @Component.

    Тот участок кода, где контейнеру необходимо осуществить внедрение зависимости, аннотируется с помощью аннотации @Autowired. В рамках данного примера мы решили, что внедрение зависимости происходит в методе (method injection). Обратите внимание, что это не обязательно должен быть сеттер, хотя это крайне желательно

    Когда мы осуществляли конфигурацию с помощью Java-класса, мы явно указывали классы компонентов и явно создавали объекты бинов.

    Однако Spring способен автоматически отсканировать пакеты проекта, обнаружить бины и создать их экземпляры. Этот механизм называется сканирование компонентов (component scanning). По умолчанию, механизм сканирования компонентов отключен. Чтобы его включить, вернемся в конфигурационный класс AppConfig и укажем аннотацию @ComponentScan перед объявлением класса.

    Прежде всего, удалим из класса AppConfig написанные ранее методы - они теперь не нужны.

    Также обратите внимание, что в скобках я указал базовый пакет, где необходимо осуществить сканирование. Механизм сканирования компонентов будет искать компоненты в этом и в дочернем пакетах. Также вы можете указать параметр basePackages и перечислить пакеты для сканирования.

    Запустим приложение и убедимся, что автоматическое связывание работает корректно.

    Разрешение зависимости (Dependency Resolution)

    Использование автоматического связывания (связывание компонентов реализуется с помощью механизмов Spring) может привести к ситуации, когда будет существовать несколько бинов, которые могут быть использованы для связывания.

    Пока что у нас был только один класс, который реализовывал интерфейс Sender. А что, если их будет два? Создадим класс EmailSender

    Запустим приложение

    Сообщение при исключении четко описывает проблему: есть два бина, которые можно внедрить в класс User и Spring не знает, какой из них следует внедрить и закрывается с исключением.

    Чтобы избавиться от данной проблемы, можно дать указания контейнеру, какой из компонентов следует выбрать в том или ином случае (ищите информацию по аннотации @Qualifier).

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

    Аннотация @Conditional перед объявлением класса бина означает, что бин будет доступен для регистрации в контейнере только, когда будет удовлетворено некоторое условие. В нашем случае, для каждого кандидата мы создадим отдельный класс - реализацию интерфейса Condition, в котором реализуем специальный метод. Если метод вернет true, значит условие выполнено и компонент можно зарегистрировать.

    Прежде всего воспользуемся механизмом properties в Java. Создадим ресурс app.properties с содержимым

    В классе Main создадим объект Properties и загрузим файл

    Теперь у нас есть публичное статическое поле config, в котором хранятся свойства.

    Создадим классы условий

    В классах компонентов укажем аннотацию @Conditional и класс условия.

    Теперь запустим приложение

    Если мы изменим в app.properties значение с email на twitter и снова запустим приложение, то получим

    Таким образом, проблема нескольких кандидатов решена.

    9. Развертывание приложения в Heroku

    hashtag
    1. Настройка Heroku и подключения к БД

    Создадим аккаунт на сервисе Heroku

    Зайдем в аккаунт и создадим новое приложение (new app)

    5. Интеграция приложения с СУБД

    В Spring имеется набор модулей для интеграции с различными технологиями хранения данных. Spring позволяет избавить разработчика от рутины при разработке программного кода, реализующего доступ к данным. Вместо возни с низкоуровневым доступом к данным можно положиться на Spring, который выполнит эту работу за вас, и сконцентрироваться на управлении данными в самом приложении.

    Что такое JDBC, драйвер, JPA, ORM и как это все между собой соотносится?

    Как правило, каждая система управления базами данных (MySQL, PostgreSQL и так далее) имеет свой протокол взаимодействия с клиентами. Чтобы работать с базой данных, клиент должен соблюдать протокол взаимодействия с базой данных.

    Чтобы программист не тратил время на самостоятельную реализацию протокола при разработке очередного приложения, разработчик сервера баз данных сам предоставляет всем желающим программный код, который общается с базой данных на понятном этой базе протоколе. Такой программный код и называется драйвером базы данных. Драйвер реализует протокол общения с БД и предоставляет API, которое позволяет нам общаться с базой данных, не вдаваясь в детали реализации протокола.

    HelloController.java
    @RestController
    public class HelloController {}
    HelloController.java
    @RestController
    public class HelloController {
        
        public String hello() {
            return "hello";
        }
    }
    HelloController.java
    @RestController
    public class HelloController {
    
        @GetMapping("/hello")
        public String hello() {
            return "hello";
        }
    }
    @GetMapping("/room/{id}")
    public void getRoomById() {
        
    }
    @GetMapping("/room/{id}")
    public void getRoomById(@PathVariable(value = "id") int roomId) {
        // ...
    }
    @GetMapping("/room/{id1}/{id2}")
    public void getRoomById(@PathVariable(value = "id1") int blockId, @PathVariable(value = "id2") int roomId) {
        // ...
    }
    // запрос: http://localhost:8080/room?room_id=250&block_id=10
    @GetMapping("/room")
    public void getRoomById(@RequestParam(value = "room_id") int roomId, @RequestParam(value = "block_id") int blockId) {
        // ...
    }
    @PostMapping("/book")
    public void bookRoom(@RequestParam(value = "room_id") int room_id,
                         @RequestParam(value = "firstname") String firstname,
                         @RequestParam(value = "lastname") String lastname,
                         @RequestParam(value = "days") int days) {
        // ...
    }
        @GetMapping("/roominfo")
        public Room getRoomInfoById(@RequestParam(value = "room_id") int roomId) {
            return new Room(roomId, "Отличная комната!", 2, 200);
        }
    Room.java
    public class Room {
        private Integer id;
        private String roomInfo;
        private Integer roomCapacity;
        private double price;
    
        public Room(Integer id, String roomInfo, Integer roomCapacity, double price) {
            this.id = id;
            this.roomInfo = roomInfo;
            this.roomCapacity = roomCapacity;
            this.price = price;
        }
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getRoomInfo() {
            return roomInfo;
        }
    
        public void setRoomInfo(String roomInfo) {
            this.roomInfo = roomInfo;
        }
    
        public Integer getRoomCapacity() {
            return roomCapacity;
        }
    
        public void setRoomCapacity(Integer roomCapacity) {
            this.roomCapacity = roomCapacity;
        }
    
        public double getPrice() {
            return price;
        }
    
        public void setPrice(double price) {
            this.price = price;
        }
    }
    Student.java
    public class Student {
    
        private String firstname;
        private String lastname;
        private String phone;
        private int age;
    
        public Student(String firstname, String lastname, String phone, int age) {
            this.firstname = firstname;
            this.lastname = lastname;
            this.phone = phone;
            this.age = age;
        }
    
        public String getFirstname() {
            return firstname;
        }
    
        public void setFirstname(String firstname) {
            this.firstname = firstname;
        }
    
        public String getLastname() {
            return lastname;
        }
    
        public void setLastname(String lastname) {
            this.lastname = lastname;
        }
    
        public String getPhone() {
            return phone;
        }
    
        public void setPhone(String phone) {
            this.phone = phone;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Student{" +
                    "firstname='" + firstname + '\'' +
                    ", lastname='" + lastname + '\'' +
                    ", phone='" + phone + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    @PostMapping("/student")
    public void addStudent(@RequestBody Student student) {
        System.out.println(student);
    }
    @GetMapping("/student")
    public ResponseEntity<?> getStudentById(@RequestParam(value = "id") int studentId) {
        if (studentId < 1) {
            return ResponseEntity.badRequest().body("Invalid id");
        } else {
            return ResponseEntity.ok(new Student("Ivan", "Ivanov", "223322", 20));
        }
    }
    public class User {
    
      public void sendMessage(String message, String target) {
        Sender sender = new Sender();
        sender.send(message, target);
      }
    }
    
    public class Sender {
    
      public void send(String message, String target) {
        System.out.println("Tweet: " + message + " to " + target);
      }
    }
    DispatcherServlet на основании полученного ModelAndView, должен определить, какое представление будет выводить данные. Для этого используется арбитр представлений (View Resolver), который на основании полученного логического имени представления возвращает ссылку на файл View;
  • в представление передаются данные (Model) и обратно, если необходимо, посылается ответ от представления.

  • Задача контроллера DispatcherServlet состоит в том, чтобы передать запрос контроллеру Spring MVC. Контроллер – это компонент Spring, обрабатывающий запрос. Но приложение может иметь несколько контроллеров, и входному контроллеру DispatcherServlet требуется помощь, чтобы определить, какому контроллеру передать запрос. Поэтому контроллер DispatcherServlet консультируется c одним или несколькими механизмами отображения (Handler Mapping) и выясняет, какой контроллер будет обрабатывать тот или иной запрос. При принятии решения механизм отображения в первую очередь руководствуется адресом URL в запросе;
  • Как только будет выбран соответствующий контроллер, DispatcherServlet отправляет запрос в путь к выбранному контроллеру. Достигнув контроллера, запрос отдаст часть своего груза (информацию, отправленную пользователем) и терпеливо будет ждать, пока контроллер обработает эту информацию. (На самом деле хорошо спроектированный контроллер сам почти не занимается обработкой информации, вместо этого он делегирует ответственность за обработку одному или нескольким служебным объектам);

  • В результате работы контроллера часто появляется некоторая информация, которая должна быть передана назад пользователю и отображена в браузере. Эта информация называется моделью (Model). Но отправки обратно необработанной информации недостаточно, перед отправкой ее следует представить в удобном для пользователя формате, обычно в HTML. Для этого информация должна быть передана в одно из представлений (View), которыми обычно являются JSP-страницы;

  • Последнее, что должен сделать контроллер, – упаковать вместе модель и имя представления для отображения результатов в браузере. Затем он отсылает запрос вместе с моделью и именем представления обратно входному контроллеру DispatcherServlet;

  • Чтобы контроллер не оказался тесно связанным с каким-либо конкретным представлением, имя представления, возвращаемое входному контроллеру DispatcherServlet, не определяет JSP-страницу непосредственно. Фактически оно даже не предполагает, что представление вообще является страницей JSP. Оно является лишь логическим именем представления, используемым затем для поиска фактического представления. Чтобы отобразить логическое имя представления в ссылку на конкретную реализацию, входной контроллер DispatcherServlet обратится к арбитру представлений (view resolver);

  • Теперь, когда контроллер DispatcherServlet определил, какое представление будет отображать результаты, работа запроса подошла к концу. Его конечная остановка – реализация представления (возможно, страница JSP), куда он доставит модель данных. На этом работа запроса заканчивается. На основе модели данных представление создаст отображение страницы, которое будет отправлено обратно клиенту с другим (не таким трудолюбивым) курьером – объектом ответа.

  • После отсылки формы заполненный объект Student передается в теле HTTP-запроса с методом POST, после чего заполненный объект можно обработать, сохранить в базе данных и так далее.

    здесьarrow-up-right
    XmlWebApplicationContext – загружает определение контекста из XML-файла, содержащегося внутри веб-приложения.
    (оригинал)arrow-up-right
    (перевод)arrow-up-right
    (оригинал)arrow-up-right
    перевод первой частиarrow-up-right
    перевод второй частиarrow-up-right
    здесьarrow-up-right
    здесьarrow-up-right
    Схематическое изображение работы IoC-контейнера
    Зайдем в раздел Resources, в пункте Add-ons добавим Heroku Postgres

    Перейдем в настройки базы данных

    На страничке адд-она перейдем в раздел Settings и выберем View Credentials

    Таким образом, мы получим credentials для подключения к базе.

    Перейдем в application.yml и укажем настройки для БД на heroku

    circle-info

    После указания настроек, можете сразу запустить приложение, чтобы убедиться, что вы правильно указали настройки для подключения БД.

    hashtag
    2. Установка git и heroku

    Заходим на сайт https://git-scm.com/arrow-up-right, скачиваем последнюю версию установщик и устанавливаем Git.

    Заходим в командную строку и настраиваем имя и почту разработчика

    Возвращаемся в Heroku, скачиваем и настраиваем Heroku CLI

    Заходим в командую строку и логинимся на heroku с помощью команды heroku login

    Переходим в директорию проекта, после чего инициализируем git-репозиторий с помощью команды git init

    Далее устанавливаем удаленный репозиторий

    Начинаем отслеживать файлы проекта с помощью команды git add ., после чего делаем коммит с помощью команды git commit -am "initial commit".

    Теперь можно пушить проект на удаленный репозиторий с помощью команды git push heroku master.

    Как видим, мы успешно развернули проект на Heroku. Проверим работу веб-сервиса. Перейдем в браузер и укажем адрес https://opnu-ej.herokuapp.com/arrow-up-right

    Теперь попробуем осуществить REST-запросы к серверу. Добавим группу, добавим студента, получим список групп.

    Добавляем группу

    Добавляем студента

    Получаем список групп

    Как раз для этого разработчики Java предоставили стандарт JDBC (Java DataBase Connectivity) – специальное API, которое используется приложениями Java для взаимодействия с базой данных. Стандарт JDBC позволяет отправлять запросы к базе данных для выполнения операций выбора, вставки, обновления и удаления.

    Если разработчики СУБД хотят, чтобы их база данных использовалась Java-разработчиками, они предоставляют JDBC-драйвер для их базы данных. Разработчики Java подключат драйвер и используют его для общения с той или иной базой данных. Если, в какой-то момент, разработчики захотят сменить СУБД, они просто меняют драйвер старой базы на драйвер новой. Благодаря стандарту JDBC, ничего менять в коде работы с базой данных не требуется.

    Что такое и зачем нужна технология ORM?

    При написании объектно-ориентированного кода, который взаимодействует с базой данных, у разработчика возникает несколько проблем:

    1. данные в программе и в базе данных используют разные парадигмы (объектно-ориентированная и реляционная соответственно). Работу по преобразованию данных из одной парадигмы в другую ложатся на плечи программиста, что влечет за собой лишнюю работу и может приводить к ошибкам в процессе преобразования;

    2. программисту желательно абстрагироваться от конкретной схемы хранения данных. То есть, программисту желательно работать не с реляционной базой данных, а просто с некоторым «хранилищем», а конкретная реализация этого «хранилища» может быстро и безболезненно меняться.

    Для устранения этих проблем используется технология ORM (Object-Relational Mapping, «объектно-реляционное отображение») — технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая «виртуальную объектную базу данных».

    Проще говоря, ORM – это прослойка, посредник между базой данных и объектным кодом. Используя ORM, программист не занимается формированием SQL-запросов и не думает в терминах «таблица», «записи» и «реляционные отношения», а просто работает с «хранилищем объектов» – он может туда записывать и получать объекты, не заботясь о подробностях их хранения.

    В Java предусмотрен специальный стандарт JPA (Java Persistence API), который использует концепцию ORM. Существует несколько реализаций этого интерфейса, например, Hibernate, OpenJPA, EclipseLink и другие.

    Spring Data JPA – обертка над JPA в Spring, которая предоставляет много полезных «фишек» разработчику. Она позволяет легче создавать Spring-управляемые приложения, которые используют новые способы доступа к данным, например нереляционные базы данных, map-reduce фреймворки, cloud сервисы, а так же уже хорошо улучшенную поддержку реляционных баз данных.

    Терминология JPA

    Основное понятие JPA – сущность (Entity). Сущность – это Java-класс, который представляет бизнес-логику приложения и определяет данные, которые будут храниться в базе данных и извлекаться из нее.

    Как правило, класс сущности представляет таблицу в базе данных, поля или свойства класса представляют собой колонки в таблице, а объект сущности представляет собой одну запись в таблице.

    Важным моментом при работе с JPA являются аннотации, коих здесь будет очень много. Разберемся с некоторыми из них:

    • @Entity – позволяет серверу узнать, что это не просто какой-то класс, а сущность;

    • @Id – помечает первичный ключ в таблице. Вопрос составных ключей в данном занятии не рассматривается;

    • @Table – позволяет настраивать отображение класса в таблицу. В данном случае, мы можем указать, какое имя будет иметь соответствующая таблица в базе данных;

    • @GeneratedValue – указывает, что данное поле является генерируемым значением. Очень часто этой аннотацией помечают первичные ключи, чтобы они генерировались автоматически при добавлении новых записей в таблицу;

    • @Column – позволяет настраивать отображение колонки в таблице. В данном случае, мы можем указать, какое имя будет иметь соответствующая колонка в таблицу.

    Репозитории. Главными компонентами для взаимодействий с БД в Spring Data являются репозитории. Каждый репозиторий работает со своим классом-сущностью.

    В большинстве случаев, структура запросов к репозиторию будет одинаковая: «получить все записи», «получить записи, где столбец равен определенному значению» и так далее.

    Spring Data JPA позволяет вам избежать рутинного создания запросов. Для этого вместо класса создадим интерфейс, который будет наследоваться от стандартного generic-интерфейса. Первый параметр означает тип класса-сущности, второй параметр – тип первичного ключа.

    hashtag
    Практическая часть

    Установим СУБД Postgres и запустим pgAdmin 4.

    Создадим пользователя ejournal_user, после чего создадим базу данных для нашего приложения.

    Добавляем в pom.xml зависимости для работы с Spring Data JPA и JDBC драйвер для Postgres.

    Далее необходимо настроить подключение к СУБД и нужной базе данных.

    Для настройки приложения Spring воспользуемся языком YAML. Для этого удалим файл resources/application.properties и создадим вместо него файл application.yml.

    Создадим класс сущности Student

    Для уменьшения количества кода, мы будем использовать плагин Lombok.

    Проект Lombok — это плагин компилятора, который добавляет в Java новые «ключевые слова» и превращает аннотации в Java-код, уменьшая усилия на разработку и обеспечивая некоторую дополнительную функциональность.

    Lombok преобразует аннотации в исходном коде в Java-операторы до того, как компилятор их обработает: зависимость lombok отсутствует в рантайме, поэтому использование плагина не увеличит размер сборки.

    При использовании Lombok наш исходный код не будет валидным кодом Java. Поэтому потребуется установить плагин для IDE, иначе среда разработки не поймёт, с чем имеет дело. Lombok поддерживает все основные Java IDE. Интеграция бесшовная. Все функции вроде «показать использования» и «перейти к реализации» продолжают работать как и раньше, перемещая вас к соответствующему полю/классу.

    Далее подключим библиотеку в pom.xml.

    Вернемся в класс Student, добавим аннотацию для геттеров, сеттеров, а также конструктор со всеми параметрами.

    hashtag
    Программирование слоя сервисов

    Service – это Java класс, который содержит в себе основную бизнес-логику. В основном сервис использует готовые DAO/Repositories или же другие сервисы, для того чтобы предоставить конечные данные для пользовательского интерфейса. Сервисы, как правило, вызываются контроллерами или другими сервисами.

    Объект службы создается контейнером Spring, каждая служба является «одиночкой» (синглтоном), который создается в момент запуска приложения и уничтожается в момент закрытия приложения. Обратите внимание на аннотацию @Service. Этой аннотацией мы сообщаем контейнеру Spring, что это не просто класс, а класс сервиса.

    Итак, мы создали службу, у которой есть два публичных метода. Первый метод добавляет нового студента, второй метод возвращает список всех студентов. В дальнейшем служба будет обращаться к объекту репозитория за данными, а пока что оставим код таким, какой он есть.

    Вернемся к созданию веб-слоя. Создадим класс контроллера, создадим две конечные точки: для добавления студента и для получения списка всех студентов.

    Обратите внимание, что мы не создаем объект службы, а получаем его «извне» с помощью аннотации @Autowired. Контейнер Spring «внедрит» ссылку на объект службы в поле service. Подробнее про внедрение зависимостей будет изложено в следующем занятии.

    hashtag
    Работа с репозиторием

    Главными компонентами для взаимодействий с БД в Spring Data являются репозитории. Каждый репозиторий работает со своим классом-сущностью.

    В большинстве случаев, структура запросов к репозиторию будет одинаковая: «получить все записи», «получить записи, где столбец равен определенному значению» и так далее.

    Spring Data JPA позволяет вам избежать рутинного создания запросов. Для этого вместо класса создадим интерфейс, который будет наследоваться от стандартного generic-интерфейса. Первый параметр означает тип класса-сущности, второй параметр – тип первичного ключа.

    Теперь перейдем в класс службы и создадим ссылку на объект репозитория.

    Обратите внимание, что мы не создавали класс, который реализует интерфейс StudentRepository, тогда откуда мы его получим объект интерфейсного типа? Дело в том, что Spring сгенерирует класс за нас. Этот сгенерированный класс будет иметь набор стандартных операций для работы с сущностями. В нашем случае, это операция findAll(), которая возвращает все сущности в таблице student.

    Запустим сервер и выполним два клиентских запроса - один на создание студента, второй - на получение списка всех студентов.

    Добавляем нового студента

    Теперь получим список всех студентов.

    hashtag
    Реализация отношения "один-ко-многим"

    Как мы знаем, важной составляющей реляционных баз данных является отношения между таблицами "один-к-одному", "один-ко-многим", "многие-ко-многим".

    Реализуем отношение "один-ко-многим". Создадим сущность Group - студенческая группа. В студенческой группе может быть от 0 до N студентов.

    Прежде всего перейдем в сущность Student. Добавим поле group, который будет ссылаться на студенческую группу, в которой будет состоять студент. Так как в группе может быть много студентов, указываем аннотацию @ManyToOne. Также указываем аннотацию @JoinColumn, которая указывает на имя колонки, которая будет содержать Foreign Key.

    circle-info

    Технология ORM позволяет создавать двусторонние связи между таблицами. В этом случае, при выдаче JSON, может возникнуть бесконечный цикл. Чтобы его избежать, укажем аннотацию @JsonIgnore. В этом случае, колонка group будет проигнорирована в процессе сериализации\десериализации.

    Далее создадим сущность Group.

    Обратите внимание, что отношение один-ко-многим мы моделируем с помощью обычной коллекции. Указываем аннотацию @OneToMany, также в свойстве mappedBy указываем, какое поле "держит" отношение со стороны студента.

    Далее модифицируем класс контроллера. Создадим конечные точки для добавления новой группы, а также для получения списка всех групп. Также модифицируем конечную точку для добавления студента, чтобы указать id группы, в которую необходимо добавить студента.

    Теперь создадим репозиторий для сущности Group.

    Далее модифицируем класс сервиса. Добавим методы для добавления новой группы, а также для получения списка всех групп. Также модифицируем метод добавления новой группы. Метод работает следующим образом: получаем объект группы по id, после чего добавляем ссылку на группу в поле group объекта Student.

    Запустим приложение и проверим его работу. Сначала добавим группу, после чего получим список групп.

    Добавим новую группу

    Получим список групп

    Теперь добавим нового студента

    Получим список всех групп

    7. Работа со Spring Security, часть 1

    Spring Security – это фреймворк обеспечения безопасности, предоставляющий возможность декларативного управления безопасностью приложений на основе фреймворка Spring.

    Создадим новый проект, который включает модуль Spring Security или добавим в существующий проект зависимость

    При попытке перейти на любой URL-адрес проекта нас перенаправит на форму ввода логина и пароля

    По умолчанию, логином является user, а пароль генерируется каждый раз при старте приложения.

    Если вы ввели правильно логин и пароль, то сервер переадресует вас на указанный URL.

    В файле application.properties вы можете указать желаемый логин и пароль для пользователя по умолчанию (в данной лекции будет использоваться конфигурация с помощью языка yaml).

    Фреймворк Spring Security "из коробки" предоставляет вам возможность простой версии так называемой form-based аутентификации. Если быть точнее, то по умолчанию, Spring Security реализует следующее поведение:

    • добавляет обязательный процесс аутентификации для всех URL;

    • добавляет форму для входа;

    • обрабатывает ошибки формы ввода;

    hashtag
    Основные понятия, связанные со Spring Security:

    Authentication

    Authorization

    Principal - текущий залогиненный пользователь или текущий залогиненный аккаунт (если у одного физического лица или программы есть несколько аккаунтов, то тогда ему будет соответствовать несколько возможных principal`ов). Иногда, в общем случае, principal - это субъект, который принимает участие в осуществлении процедур безопасности. В качестве principal могут выступать люди, компьютеры, службы, процессы или их группа;

    Granted Authority - ;

    Role - .

    hashtag
    Настройка процесса аутентификации в Spring

    Для того, чтобы сконфигурировать процесс аутентификации, необходимо создать объект AuthenticationManager, в котором следует указать требуемые параметры аутентификации. Объект типа AuthenticationManager обычно настраивают с помощью builder`а, который имеет тип AuthenticationManagerBuilder.

    Добавим класс SecurityConfig, который наследуется от класса WebSecurityConfigurerAdaper. Также укажем аннотации @Configuration (это означает, что данный класс является конфигурационным) и @EnableWebSecurity (это означает, что данный класс является содержит настройки для защиты веб-приложения).

    Переопределим метод configure(), который принимает на вход объект типа AuthenticationManagerBuilder (обратите внимание, что нам нужна именно эта версия перегруженного метода).

    Для начала укажем, что источник аутентификации это жестко прописанные пользователи (так называемая inMemoryAuthentication(). Далее указываем логин, пароль и роль пользователя.

    Если необходимо указать несколько пользователей, после параметров первого пользователя вызываете метод and() после чего указываете параметры следующего пользователя.

    hashtag
    Хеширование паролей

    Хранить пароли без хеширования является грубейшим нарушением правил безопасности, поэтому нам необходимо добавить процесс хеширования пароля в нашу систему.

    Не будем вдаваться в подробности различных алгоритмов хеширования пароля, просто скажем, что на даннай момент рекомендуемым является алгоритм Bcrypt. Для обеспечения хеширования, вы можете поступить несколькими способами.

    Первый способ - создайте Bean, который будет возвращать объект Encoder`а и добавьте его как метод конфигурационного класса.

    Далее найдите в интернете генератор хеша с помощью алгоритма Bcrypt, скопируйте хеш для вашего пароля в метод password.

    Если не хотите использовать Bean для хеширования пароля, можете в начале хеша добавить обозначение, что это хеш для алгоритма bcrypt.

    hashtag
    Настройка процесса авторизации

    Добавим класс контроллера

    Изменим SecurityConfig

    Изменим formLogin() на httpBasic().

    Создадим MyUserDetailsService

    Создадим MyUserDetails

    Добавим в pom.xml

    Настроим подключение к БД

    Добавим сущность User

    Изменим MyUserDetailsService

    Создадим UserRepository

    Изменим MyUserDetails

    8. Работа со Spring Security, часть 2

    1. Добавляем в pom-файл поддержку jwt и jaxb

    Создадим класс JwtUtils, добавим приватный ключ и методы для создания токена

    Добавим метод для валидациия токена и сопутствующие ему методы

    Создадим классы-обертки для входящего логина и пароля и для исходящего jwt

    Создадим класс REST-контроллера, добавим метод для создания токена. Логика работы метода следующая:

    1. Производится аутентификация по пришедшему логину и паролю;

    2. Если аутентификация прошла успешно, то получаем UserDetails из БД по username;

    3. Генерируем токен с помощью данных UserDetails;

    Настраиваем конфигурационный класс SecurityConfig. Добавляем bean для AuthenticationManager, а также доступ к URL. Для того, чтобы сервер не генерировал новую сессию, устанавливаем sessionCreationPolicy.

    В качестве клиента используем Postman.

    Для начала попробуем сделать запрос с некорректным логином и паролем

    На стороне сервера было выброшено исключение

    Теперь попробуем ввести логин и пароль существующего пользователя. В ответ мы получим jwt, который будем использовать для последующих запросов.

    hashtag
    Аутентификация с помощью jwt

    Добавим в класс-контроллер endpoint, для доступа к которому требуется аутентификация. Входной аргумент типа Principal хранит информацию об аутентифицированном пользователе. Получаем username пользователя и извлекаем из БД информацию о нем.

    Редактируем конфигурационный класс SecurityConfig, добавляем требование аутентификации для endpoint /helloworld.

    Для того чтобы провести аутентификацию с помощью jwt, создадим отдельный фильтр, который потом встроим в filter chain, которая используется в Spring Security.

    Логика работы нашего фильтра следующая:

    1. Считываем заголовок GET-запроса с ключом "Authorization";

    2. Проверяем, есть ли в начале заголовка слово "Bearer ";

    3. Извлекаем из jwt значение username;

    Отредактируем конфигурационный класс SecurityConfig, добавим использование фильтра в цепочке фильтров (мы указываем, что наш фильтр должен быть встроен в цепочку ДО фильтра UsernamePasswordAuthenticationFilter, который является стандартным фильтром для аутентификации).

    index.html
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org">
        <title>Index page</title>
    </head>
    <body>
        <h1>Hello, world!</h1>
    </body>
    </html>
    index.html
    <!doctype html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    
        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
              integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    
        <title>Hello, world!</title>
    </head>
    <body class="bg-light">
    
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <a class="navbar-brand" href="#">Электронный деканат</a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExample05"
                aria-controls="navbarsExample05" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
    
        <div class="collapse navbar-collapse" id="navbarsExample05">
            <ul class="navbar-nav mr-auto">
                <li class="nav-item">
                    <a class="nav-link" href="#">Главная</a>
                </li>
                <li class="nav-item active">
                    <a class="nav-link" href="#">Студенты <span class="sr-only">(current)</span></a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="#">Группы</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="#">Кафедры</a>
                </li>
    
            </ul>
            <form class="form-inline mt-2 mt-md-0">
                <input class="form-control mr-sm-2" type="text" placeholder="Введите текст" aria-label="Search">
                <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Поиск</button>
            </form>
        </div>
    </nav>
    
    <div class="container">
    
        <div class="py-5 text-center">
            <h2>Управление студентами</h2>
            <p class="lead">На данной странице вы можете добавить, отредактировать поля или удалить студентов кафедры</p>
        </div>
    
    
        <div class="row">
            <div class="col">
                <h2>Группа АИ-171</h2>
            </div>
            <div class="col col-lg-3">
                <a th:href="@{~/add_student}" class="btn btn-lg btn-block btn-outline-primary" role="button"
                   aria-disabled="true">Добавить студента</a>
            </div>
        </div>
    
        <br/>
    
        <div class="table-responsive">
            <table class="table table-striped">
                <thead>
                <tr>
                    <th class="text-justify">#</th>
                    <th class="text-justify">Фамилия</th>
                    <th class="text-justify">Имя</th>
                    <th class="text-justify">Отчество</th>
                    <th class="text-justify">Почта</th>
                    <th class="text-justify">Телефон</th>
                    <th class="text-justify">Адрес</th>
    
    
                </tr>
                </thead>
                <tbody>
    
                <tr th:each="student : ${students}">
                    <td class="align-middle"><span th:text="${student.id}"/></td>
                    <td class="align-middle"><span th:text="${student.lastName}"/></td>
                    <td class="align-middle"><span th:text="${student.firstName}"/></td>
                    <td class="align-middle"><span th:text="${student.patronymic}"/></td>
                    <td class="align-middle"><span th:text="${student.email}"/></td>
                    <td class="align-middle"><span th:text="${student.phone}"/></td>
                    <td class="align-middle"><span th:text="${student.address}"/></td>
                </tr>
                </tbody>
            </table>
        </div>
    </div>
    
    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
            integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
            crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
            integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
            crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
            integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
            crossorigin="anonymous"></script>
    </body>
    </html>
    add_student.html
    <!doctype html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    
        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
              integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    
        <title>Добавление студента</title>
    </head>
    <body class="bg-light">
    <div class="container">
        <div class="py-5 text-center">
            <h2>Добавление студента</h2>
            <p class="lead">Заполните поля и нажмите кнопку 'Добавить студента'</p>
        </div>
    
        <form method="post" class="needs-validation" novalidate>
            <div class="row">
                <div class="col-md-12">
                    <h4 class="mb-3">Поля для заполнения</h4>
    
                    <div class="row">
    
                        <div class="col-md-4 mb-3">
                            <label for="lastName">Фамилия</label>
                            <input type="text" class="form-control" id="lastName" placeholder=""
                                   value="" required>
                        </div>
    
    
                        <div class="col-md-4 mb-3">
                            <label for="firstName">Имя</label>
                            <input type="text" class="form-control" id="firstName" placeholder=""
                                   value="" required>
                        </div>
                        <div class="col-md-4 mb-3">
                            <label for="patronymic">Отчество</label>
                            <input type="text" class="form-control" id="patronymic" placeholder=""
                                   value="" required>
                        </div>
                    </div>
    
                    <div class="mb-3">
                        <label for="email">Электронная почта</label>
                        <input type="email" class="form-control" id="email"
                               placeholder="" value="" required>
                    </div>
    
                    <div class="mb-3">
                        <label for="phone">Телефон</label>
                        <input type="text" class="form-control" id="phone"
                               placeholder="(ХХХ) ХХХ-ХХХХ" value="" required>
                    </div>
    
                    <div class="mb-3">
                        <label for="address">Домашний адрес</label>
                        <input type="text" class="form-control" id="address" placeholder="" value=""
                               required>
                    </div>
    
                    <hr class="mb-4">
                    <button class="btn btn-primary btn-lg btn-block" type="submit" value="Submit">Добавить студента</button>
        </form>
    </div>
    </div>
    </div>
    
    <br/><br/><br/>
    
    
    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
            integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
            crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
            integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
            crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
            integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
            crossorigin="anonymous"></script>
    </body>
    </html>
    StudentController.java
    @Controller
    public class StudentController {
    
        @GetMapping("/")
        public String index(Model model) {
            return "index";
        }
    
        @GetMapping("/add_student")
        public String addStudent(Model model) {
            return "add_student";
        }
    }
    index.html
            <div class="col col-lg-3">
                <a th:href="@{~/add_student}" class="btn btn-lg btn-block btn-outline-primary" role="button"
                   aria-disabled="true">Добавить студента</a>
            </div>
    Student.java
    public class Student {
    
        private long id;
        private String lastName;
        private String firstName;
        private String patronymic;
        private String email;
        private String phone;
        private String address;
    
        public long getId() {
            return id;
        }
    
        public void setId(long id) {
            this.id = id;
        }
    
        public String getLastName() {
            return lastName;
        }
    
        public void setLastName(String lastName) {
            this.lastName = lastName;
        }
    
        public String getFirstName() {
            return firstName;
        }
    
        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }
    
        public String getPatronymic() {
            return patronymic;
        }
    
        public void setPatronymic(String patronymic) {
            this.patronymic = patronymic;
        }
    
        public String getEmail() {
            return email;
        }
    
        public void setEmail(String email) {
            this.email = email;
        }
    
        public String getPhone() {
            return phone;
        }
    
        public void setPhone(String phone) {
            this.phone = phone;
        }
    
        public String getAddress() {
            return address;
        }
    
        public void setAddress(String address) {
            this.address = address;
        }
    
        public Student() {}
    
        public Student(String lastName, String firstName, String patronymic, String email, String phone, String address) {
            this.lastName = lastName;
            this.firstName = firstName;
            this.patronymic = patronymic;
            this.email = email;
            this.phone = phone;
            this.address = address;
        }
    }
    StudentController.java
    @Controller
    public class StudentController {
    
        ...
    
        @GetMapping("/add_student")
        public String addStudent(Model model) {
            model.addAttribute("student", new Student());
            return "add_student";
        }
    }
    add_student.html
    <form enctype="multipart/form-data" action="#" th:action="@{/add_student}" th:object="${student}" method="post" class="needs-validation"
          novalidate>
        <div class="row">
            <div class="col-md-12">
                <h4 class="mb-3">Поля для заполнения</h4>
    
                <div class="row">
    
                    <div class="col-md-4 mb-3">
                        <label for="lastName">Фамилия</label>
                        <input th:field="*{lastName}" type="text" class="form-control" id="lastName" placeholder=""
                               value="" required>
                    </div>
    
    
                    <div class="col-md-4 mb-3">
                        <label for="firstName">Имя</label>
                        <input th:field="*{firstName}" type="text" class="form-control" id="firstName" placeholder=""
                               value="" required>
                    </div>
                    <div class="col-md-4 mb-3">
                        <label for="patronymic">Отчество</label>
                        <input th:field="*{patronymic}" type="text" class="form-control" id="patronymic" placeholder=""
                               value="" required>
                    </div>
                </div>
    
                <div class="mb-3">
                    <label for="email">Электронная почта</label>
                    <input th:field="*{email}" type="email" class="form-control" id="email"
                           placeholder="" value="" required>
                </div>
    
                <div class="mb-3">
                    <label for="phone">Телефон</label>
                    <input th:field="*{phone}" type="text" class="form-control" id="phone"
                           placeholder="(ХХХ) ХХХ-ХХХХ" value="" required>
                </div>
    
                <div class="mb-3">
                    <label for="address">Домашний адрес</label>
                    <input th:field="*{address}" type="text" class="form-control" id="address" placeholder="" value=""
                           required>
                </div>
    
                <hr class="mb-4">
                <button class="btn btn-primary btn-lg btn-block" type="submit" value="Submit">Добавить студента</button>
            </div>
        </div>
    </form>
    StudentController.java
    @Controller
    public class StudentController {
    
        ...
    
        @PostMapping("/add_student")
        public String greetingSubmit(@ModelAttribute Student student) {
                return "redirect:/";
        }
    }
    Student.java
    @Entity
    @Table(name = "student")
    public class Student {
    
        @Id
        @GeneratedValue
        private long id;
    
        private String lastName;
        private String firstName;
        private String patronymic;
        private String email;
        private String phone;
        private String address;
    
        public long getId() {
            return id;
        }
    
        public void setId(long id) {
            this.id = id;
        }
    
        public String getLastName() {
            return lastName;
        }
    
        public void setLastName(String lastName) {
            this.lastName = lastName;
        }
    
        public String getFirstName() {
            return firstName;
        }
    
        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }
    
        public String getPatronymic() {
            return patronymic;
        }
    
        public void setPatronymic(String patronymic) {
            this.patronymic = patronymic;
        }
    
        public String getEmail() {
            return email;
        }
    
        public void setEmail(String email) {
            this.email = email;
        }
    
        public String getPhone() {
            return phone;
        }
    
        public void setPhone(String phone) {
            this.phone = phone;
        }
    
        public String getAddress() {
            return address;
        }
    
        public void setAddress(String address) {
            this.address = address;
        }
    
        public Student() {
        }
    
        public Student(String lastName, String firstName, String patronymic, String email, String phone, String address) {
            this.lastName = lastName;
            this.firstName = firstName;
            this.patronymic = patronymic;
            this.email = email;
            this.phone = phone;
            this.address = address;
        }
    }
    StudentRepository.java
    public interface StudentRepository extends JpaRepository<Student,Long> {}
    StudentService.java
    @Service
    public class StudentService {
    
        private StudentRepository repository;
    
        @Autowired
        public void setRepository(StudentRepository repository) {
            this.repository = repository;
        }
    
        public void saveStudent(Student student) {
            repository.save(student);
        }
    }
    StudentController.java
    @Controller
    public class StudentController {
    
        private StudentService service;
    
        @Autowired
        public void setService(StudentService service) {
            this.service = service;
        }
    
        ...
    
        @PostMapping("/add_student")
        public String greetingSubmit(@ModelAttribute Student student) {
            service.saveStudent(student);
            return "redirect:/";
        }
    }
    StudentController.java
    @Controller
    public class StudentController {
    
        private StudentService service;
    
        @Autowired
        public void setService(StudentService service) {
            this.service = service;
        }
        
        @GetMapping("/")
        public String index(Model model) {
            model.addAttribute("students", service.getAllStudents());
            return "index";
        }
        
        ...
    }
    index.html
    <table class="table table-striped">
        <thead>
        <tr>
            <th class="text-justify">#</th>
            <th class="text-justify">Фамилия</th>
            <th class="text-justify">Имя</th>
            <th class="text-justify">Отчество</th>
            <th class="text-justify">Почта</th>
            <th class="text-justify">Телефон</th>
            <th class="text-justify">Адрес</th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="student : ${students}">
            <td class="align-middle"><span th:text="${student.id}"/></td>
            <td class="align-middle"><span th:text="${student.lastName}"/></td>
            <td class="align-middle"><span th:text="${student.firstName}"/></td>
            <td class="align-middle"><span th:text="${student.patronymic}"/></td>
            <td class="align-middle"><span th:text="${student.email}"/></td>
            <td class="align-middle"><span th:text="${student.phone}"/></td>
            <td class="align-middle"><span th:text="${student.address}"/></td>
        </tr>
        </tbody>
    </table>
    index.html
    <form enctype="multipart/form-data" action="#" th:action="@{/}" th:object="${student}" method="post" class="needs-validation">
        <div class="row">
            <div class="col-md-12">
                <h4 class="mb-3">Поля для заполнения</h4>
                <div class="row">
                    <div class="col-md-6 mb-3">
                        <label for="lastName">Фамилия</label>
                        <input th:field="*{lastName}" type="text" class="form-control" id="lastName">
                    </div>
                    <div class="col-md-6 mb-3">
                        <label for="firstName">Имя</label>
                        <input th:field="*{firstName}" type="text" class="form-control" id="firstName">
                    </div>
                </div>
    
                <div class="row">
                    <div class="col-md-6 mb-3">
                        <label for="email">Электронная почта</label>
                        <input th:field="*{email}" type="text" class="form-control" id="email">
                    </div>
                </div>
    
                <div class="row">
                    <div class="col-md-6 mb-3">
                        <label for="age">Возраст</label>
                        <input th:field="*{age}" type="number" class="form-control" id="age">
                    </div>
                </div>
    
                <hr class="mb-4">
                <button class="btn btn-primary btn-lg btn-block" type="submit" value="Submit">Добавить студента</button>
            </div>
        </div>
    </form>
    pom.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <project ...>
    
        ...
    
        <dependencies>
        
        ...
    
            <dependency>
                <groupId>javax.validation</groupId>
                <artifactId>validation-api</artifactId>
                <version>2.0.1.Final</version>
            </dependency>
        </dependencies>
    
    ...
    
    </project>
    Student.java
    public class Student {
    
        // Имя должно быть длиной от 2 до 50 символов
        @Size(min = 2, max= 50, message = "First name should be from 2 to 50 characters")
        private String firstName;
        
        // Фамилия должна быть длиной от 2 до 50 символов
        @Size(min = 2, max= 50, message = "Last name should be from 2 to 50 characters")
        private String lastName;
        
        // Возраст должен быть целым числом от 13 до 65
        @Range(min = 13, max = 65, message = "Student age should be from 13 to 65 years")
        private int age;
        
        // Для валидации электронной почты используем регулярное выражение
        @Pattern(regexp = "^[\\\\w!#$%&’*+/=?`{|}~^-]+(?:\\\\.[\\\\w!#$%&’*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\\\.)+[a-zA-Z]{2,6}$",
                message = "Invalid email format")
        private String email;
        
        ...
    }
        @GetMapping("/")
        public String addStudent(Model model) {
            model.addAttribute("student", new Student());
            return "index";
        }
    
        @PostMapping("/")
        public String processAddStudentForm(@Valid Student student, BindingResult bindingResult) {
    
            if (bindingResult.hasErrors()) {
                System.out.println("Validation has been failed!");
                return "index";
            }
    
            System.out.println(student);
    
            list.add(student);
            return "redirect:/";
        }
    index.html
    <div class="col-md-6 mb-3">
        <label for="lastName">Фамилия</label>
        <input th:field="*{lastName}" type="text" class="form-control" id="lastName">
        <small class="text-danger" th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}"/>
    </div>
    <!doctype html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
              integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
        <title>Добавление студента</title>
    </head>
    <body class="bg-light">
    <div class="container">
        <div class="py-5 text-center">
            <h2>Добавление студента</h2>
            <p class="lead">Заполните поля и нажмите кнопку 'Добавить студента'</p>
        </div>
    
        <form enctype="multipart/form-data" action="#" th:action="@{/}" th:object="${student}" method="post">
            <div class="row">
                <div class="col-md-12">
                    <h4 class="mb-3">Поля для заполнения</h4>
                    <div class="row">
                        <div class="col-md-6 mb-3">
                            <label for="lastName">Фамилия</label>
                            <input th:field="*{lastName}" type="text" class="form-control" id="lastName">
                            <small class="text-danger" th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}"/>
                        </div>
                        <div class="col-md-6 mb-3">
                            <label for="firstName">Имя</label>
                            <input th:field="*{firstName}" type="text" class="form-control" id="firstName">
                            <small class="text-danger" th:if="${#fields.hasErrors('firstName')}" th:errors="*{firstName}"/>
                        </div>
                    </div>
    
                    <div class="row">
                        <div class="col-md-6 mb-3">
                            <label for="email">Электронная почта</label>
                            <input th:field="*{email}" type="text" class="form-control" id="email">
                            <small class="text-danger" th:if="${#fields.hasErrors('email')}" th:errors="*{email}"/>
                        </div>
                    </div>
    
                    <div class="row">
                        <div class="col-md-6 mb-3">
                            <label for="age">Возраст</label>
                            <input th:value="${student.age > 0} ? ${student.age} : ''" th:field="*{age}" type="number" class="form-control" id="age">
                            <small class="text-danger" th:if="${#fields.hasErrors('age')}" th:errors="*{age}"/>
                        </div>
                    </div>
    
                    <hr class="mb-4">
                    <button class="btn btn-primary btn-lg btn-block" type="submit" value="Submit">Добавить студента</button>
                </div>
            </div>
        </form>
    </div>
    </div>
    </div>
    
    <br/><br/><br/>
    
    
    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
            integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
            crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
            integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
            crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
            integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
            crossorigin="anonymous"></script>
    </body>
    </html>
    public class Student {
    
        // Имя должно быть длиной от 2 до 50 символов
        @Size(min = 2, max= 50, message = "First name should be from 2 to 50 characters")
        private String firstName;
    
        // Фамилия должна быть длиной от 2 до 50 символов
        @Size(min = 2, max= 50, message = "Last name should be from 2 to 50 characters")
        private String lastName;
    
        // Возраст должен быть целым числом от 13 до 65
        @Range(min = 13, max = 65, message = "Student age should be from 13 to 65 years")
        private int age;
    
        // Для валидации электронной почты используем регулярное выражение
        @Pattern(regexp = "^[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$",
                message = "Invalid email format")
        private String email;
    
        public Student(String firstName, String lastName, int age, String email) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.age = age;
            this.email = email;
        }
    
        public Student() {
        }
    
        public String getFirstName() {
            return firstName;
        }
    
        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }
    
        public String getLastName() {
            return lastName;
        }
    
        public void setLastName(String lastName) {
            this.lastName = lastName;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public String getEmail() {
            return email;
        }
    
        public void setEmail(String email) {
            this.email = email;
        }
    }
    @Controller
    public class StudentController {
    
        private List<Student> list = new ArrayList<>();
    
        @GetMapping("/")
        public String addStudent(Model model) {
            model.addAttribute("student", new Student());
            return "index";
        }
    
        @PostMapping("/")
        public String processAddStudentForm(@Valid Student student, BindingResult bindingResult) {
            if (bindingResult.hasErrors()) {
                return "index";
            }
    
            System.out.println(student);
    
            list.add(student);
            return "redirect:/";
        }
    }
    <!doctype html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
              integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
        <title>Добавление студента</title>
    </head>
    <body class="bg-light">
    <div class="container">
        <div class="py-5 text-center">
            <h2>Добавление студента</h2>
            <p class="lead">Заполните поля и нажмите кнопку 'Добавить студента'</p>
        </div>
    
        <form enctype="multipart/form-data" action="#" th:action="@{/}" th:object="${student}" method="post">
            <div class="row">
                <div class="col-md-12">
                    <h4 class="mb-3">Поля для заполнения</h4>
                    <div class="row">
                        <div class="col-md-6 mb-3">
                            <label for="lastName">Фамилия</label>
                            <input th:field="*{lastName}" type="text" class="form-control" id="lastName">
                            <small class="text-danger" th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}"/>
                        </div>
                        <div class="col-md-6 mb-3">
                            <label for="firstName">Имя</label>
                            <input th:field="*{firstName}" type="text" class="form-control" id="firstName">
                            <small class="text-danger" th:if="${#fields.hasErrors('firstName')}" th:errors="*{firstName}"/>
                        </div>
                    </div>
    
                    <div class="row">
                        <div class="col-md-6 mb-3">
                            <label for="email">Электронная почта</label>
                            <input th:field="*{email}" type="text" class="form-control" id="email">
                            <small class="text-danger" th:if="${#fields.hasErrors('email')}" th:errors="*{email}"/>
                        </div>
                    </div>
    
                    <div class="row">
                        <div class="col-md-6 mb-3">
                            <label for="age">Возраст</label>
                            <input th:value="${student.age > 0} ? ${student.age} : ''" th:field="*{age}" type="number" class="form-control" id="age">
                            <small class="text-danger" th:if="${#fields.hasErrors('age')}" th:errors="*{age}"/>
                        </div>
                    </div>
    
                    <div class="form-group">
                        <label for="file">Choose file:</label>
                        <input type="file" name="file" class="form-control-file" id="file">
                    </div>
    
                    <hr class="mb-4">
                    <button class="btn btn-primary btn-lg btn-block" type="submit" value="Submit">Добавить студента</button>
                </div>
            </div>
        </form>
    </div>
    </div>
    </div>
    
    <br/><br/><br/>
    
    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
            integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
            crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
            integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
            crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
            integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
            crossorigin="anonymous"></script>
    </body>
    </html>
    @Controller
    public class StudentController {
    
        private List<Student> list = new ArrayList<>();
    
        @GetMapping("/")
        public String addStudent(Model model) {
            model.addAttribute("student", new Student());
            return "index";
        }
    
        @PostMapping("/")
        public String processAddStudentForm(@Valid Student student, @RequestParam("file") MultipartFile file, BindingResult bindingResult) {
            if (bindingResult.hasErrors()) {
                return "index";
            }
    
            System.out.println(student);
    
            list.add(student);
    
            // normalize the file path
            String fileName = StringUtils.cleanPath(file.getOriginalFilename());
    
            // save the file on the local file system
            try {
                Path path = Paths.get("c:\\temp\\" + fileName);
                Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            return "redirect:/";
        }
    }
    spring.servlet.multipart.max-file-size=128KB
    spring.servlet.multipart.max-request-size=128KB
    
    spring.servlet.multipart.enabled=true
            <dependency>
                <groupId>commons-fileupload</groupId>
                <artifactId>commons-fileupload</artifactId>
                <version>1.4</version>
            </dependency>
    public interface Sender {
        void sendMessage(String message, String target);
    }
    public class TwitterSender implements Sender {
    
        public void sendMessage(String message, String target) {
            System.out.println("Tweet: " + message + " is sending to " + target);
        }
    }
    public class User {
    
        private Sender sender;
    
        public User(Sender sender) {
            this.sender = sender;
        }
    
        public void setSender(Sender sender) {
            this.sender = sender;
        }
    
        public void send(String message, String target) throws NullPointerException {
            if (sender != null) {
                sender.sendMessage(message, target);
            } else {
                throw new NullPointerException("Sender object is null");
            }
        }
    }
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.7.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>5.0.7.RELEASE</version>
    </dependency>
    @Configuration
    public class AppConfig {...}
    @Configuration
    public class AppConfig {
    
        @Bean
        public TwitterSender twitterSender() {
            return new TwitterSender();
        }
    }
    @Configuration
    public class AppConfig {
    
        @Bean
        public User user() {
            return new User(twitterSender());
        }
    
        @Bean
        public TwitterSender twitterSender() {
            return new TwitterSender();
        }
    }
    public class Main {
    
        public static void main(String[] args) {
    
            AnnotationConfigApplicationContext context
                    = new AnnotationConfigApplicationContext(AppConfig.class);
    
            User user = context.getBean(User.class);
            user.send("Hello!", "Nick");
    
        }
    }
    июл 01, 2018 2:37:09 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
    INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@4534b60d: startup date [Sun Jul 01 14:37:09 EEST 2018]; root of context hierarchy
    
    Tweet: Hello! is sending to Nick
    @Component
    public class TwitterSender implements Sender {...}
    
    @Component
    public class User {...}
    @Component
    public class User {
    
        private Sender sender;
    
        @Autowired
        public void setSender(Sender sender) {
            this.sender = sender;
        }
    }
    @Configuration
    @ComponentScan("app")
    public class AppConfig {}
    июл 01, 2018 4:13:21 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
    INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@2d8e6db6: startup date [Sun Jul 01 16:13:21 EEST 2018]; root of context hierarchy
    
    Tweet: Hello! is sending to Nick
    @Component
    public class EmailSender implements Sender {
    
        public void sendMessage(String message, String target) {
            System.out.println("Email: " + message + " to: " + target);
        }
    }
    	at app.Main.main(Main.java:28)
    Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'app.model.Sender' available: expected single matching bean but found 2: emailSender,twitterSender
    sender.type = email
    public class Main {
    
        public static final Properties config = new Properties();
    
        static {
            ClassLoader loader = Thread.currentThread().getContextClassLoader();
    
            try (InputStream resourceStream = loader.getResourceAsStream("app.properties")) {
                config.load(resourceStream);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public class TwitterSenderCondition implements Condition {
        @Override
        public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
            return Main.config.getProperty("sender.type").matches("twitter");
        }
    }
    
    public class EmailSenderCondition implements Condition {
        @Override
        public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
            return Main.config.getProperty("sender.type").matches("email");
        }
    }
    @Component
    @Conditional(value = TwitterSenderCondition.class)
    public class TwitterSender implements Sender {...}
    
    @Component
    @Conditional(value = EmailSenderCondition.class)
    public class EmailSender implements Sender {...}
    июл 01, 2018 4:57:53 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
    INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@2d8e6db6: startup date [Sun Jul 01 16:57:53 EEST 2018]; root of context hierarchy
    
    Email: Hello! to: Nick
    июл 01, 2018 4:58:56 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
    INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@2d8e6db6: startup date [Sun Jul 01 16:58:56 EEST 2018]; root of context hierarchy
    
    Tweet: Hello! is sending to Nick
    application.yml
    spring:
      jpa:
        database: POSTGRESQL
        show-sql: true
        hibernate:
          ddl-auto: create-drop
        properties:
          hibernate:
            dialect: org.hibernate.dialect.PostgreSQLDialect
      datasource:
        platform: postgres
        url: jdbc:postgresql://ec2-176-34-183-20.eu-west-1.compute.amazonaws.com:5432/d60ukqmkatvcpg
        username: xvlmxbawwxafdu
        password: a1439fbcffbfdd6ad208df91670a37c73d8ed12d18cd6de91c1485c5841774b9
        driverClassName: org.postgresql.Driver
    Microsoft Windows [Version 10.0.18362.476]
    (c) Корпорация Майкрософт (Microsoft Corporation), 2019. Все права защищены.
    
    C:\Users\nickg>git config --global user.name "Mykola Hodovychenko"
    
    C:\Users\nickg>git config --global user.email hodovychenko@opu.ua
    
    C:\Users\nickg>git config --list
    http.sslcainfo=C:/Program Files/Git/mingw64/ssl/certs/ca-bundle.crt
    http.sslbackend=openssl
    diff.astextplain.textconv=astextplain
    core.autocrlf=true
    core.fscache=true
    core.symlinks=false
    core.editor="C:\\Program Files\\Notepad++\\notepad++.exe" -multiInst -notabbar -nosession -noPlugin
    credential.helper=manager
    user.name=Mykola Hodovychenko
    user.email=hodovychenko@opu.ua
    gui.recentrepo=E:/spring/testproject3
    
    C:\Users\nickg>
    C:\Users\nickg>heroku login
    heroku: Press any key to open up the browser to login or q to exit:
    Opening browser to https://cli-auth.heroku.com/auth/browser/630e1a02-f90e-4217-8170-cd71d7fc700e
    Logging in... done
    Logged in as hodovychenko@opu.ua
    C:\Users\nickg>cd d:\springdemo\ejo
    
    d:\springdemo\ejo>git init
    Initialized empty Git repository in d:/springdemo/ejo/.git/
    d:\springdemo\ejo>heroku git:remote -a opnu-ej
    set git remote heroku to https://git.heroku.com/opnu-ej.git
    d:\springdemo\ejo>git add .
    warning: LF will be replaced by CRLF in .gitignore.
    The file will have its original line endings in your working directory
    warning: LF will be replaced by CRLF in .mvn/wrapper/MavenWrapperDownloader.java.
    The file will have its original line endings in your working directory
    warning: LF will be replaced by CRLF in .mvn/wrapper/maven-wrapper.properties.
    The file will have its original line endings in your working directory
    warning: LF will be replaced by CRLF in mvnw.
    The file will have its original line endings in your working directory
    warning: LF will be replaced by CRLF in mvnw.cmd.
    The file will have its original line endings in your working directory
    warning: LF will be replaced by CRLF in pom.xml.
    The file will have its original line endings in your working directory
    warning: LF will be replaced by CRLF in src/main/java/com/example/ejo/EjoApplication.java.
    The file will have its original line endings in your working directory
    warning: LF will be replaced by CRLF in src/test/java/com/example/ejo/EjoApplicationTests.java.
    The file will have its original line endings in your working directory
    
    d:\springdemo\ejo>git commit -am "initial commit"
    [master (root-commit) 162bc53] initial commit
     16 files changed, 886 insertions(+)
     create mode 100644 .gitignore
     create mode 100644 .mvn/wrapper/MavenWrapperDownloader.java
     create mode 100644 .mvn/wrapper/maven-wrapper.jar
     create mode 100644 .mvn/wrapper/maven-wrapper.properties
     create mode 100644 mvnw
     create mode 100644 mvnw.cmd
     create mode 100644 pom.xml
     create mode 100644 src/main/java/com/example/ejo/Controller.java
     create mode 100644 src/main/java/com/example/ejo/EjoApplication.java
     create mode 100644 src/main/java/com/example/ejo/Group.java
     create mode 100644 src/main/java/com/example/ejo/GroupRepository.java
     create mode 100644 src/main/java/com/example/ejo/Service.java
     create mode 100644 src/main/java/com/example/ejo/Student.java
     create mode 100644 src/main/java/com/example/ejo/StudentRepository.java
     create mode 100644 src/main/resources/application.yml
     create mode 100644 src/test/java/com/example/ejo/EjoApplicationTests.java
    d:\springdemo\ejo>git push heroku master
    Enumerating objects: 32, done.
    Counting objects: 100% (32/32), done.
    Delta compression using up to 6 threads
    Compressing objects: 100% (23/23), done.
    Writing objects: 100% (32/32), 54.39 KiB | 9.06 MiB/s, done.
    Total 32 (delta 1), reused 0 (delta 0)
    remote: Compressing source files... done.
    remote: Building source:
    remote:
    remote: -----> Java app detected
    remote: -----> Installing JDK 1.8... done
    remote: -----> Executing: ./mvnw -DskipTests clean dependency:list install
    remote:        [INFO] Scanning for projects...
    
    ...
    
    remote:        [INFO] Replacing main artifact with repackaged archive
    remote:        [INFO]
    remote:        [INFO] --- maven-install-plugin:2.5.2:install (default-install) @ ejo ---
    remote:        [INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-shared-utils/0.4/maven-shared-utils-0.4.pom
    remote:        [INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-shared-utils/0.4/maven-shared-utils-0.4.pom (4.0 kB at 450 kB/s)
    remote:        [INFO] Downloading from central: https://repo.maven.apache.org/maven2/classworlds/classworlds/1.1-alpha-2/classworlds-1.1-alpha-2.jar
    remote:        [INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-shared-utils/0.4/maven-shared-utils-0.4.jar
    remote:        [INFO] Downloaded from central: https://repo.maven.apache.org/maven2/classworlds/classworlds/1.1-alpha-2/classworlds-1.1-alpha-2.jar (38 kB at 1.1 MB/s)
    remote:        [INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-utils/3.0.15/plexus-utils-3.0.15.jar
    remote:        [INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-shared-utils/0.4/maven-shared-utils-0.4.jar (155 kB at 2.0 MB/s)
    remote:        [INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-utils/3.0.15/plexus-utils-3.0.15.jar (239 kB at 3.0 MB/s)
    remote:        [INFO] Installing /tmp/build_bee447e74484e663663b1d166f8e9456/target/ejo-0.0.1-SNAPSHOT.jar to /app/tmp/cache/.m2/repository/com/example/ejo/0.0.1-SNAPSHOT/ejo-0.0.1-SNAPSHOT.jar
    remote:        [INFO] Installing /tmp/build_bee447e74484e663663b1d166f8e9456/pom.xml to /app/tmp/cache/.m2/repository/com/example/ejo/0.0.1-SNAPSHOT/ejo-0.0.1-SNAPSHOT.pom
    remote:        [INFO] ------------------------------------------------------------------------
    remote:        [INFO] BUILD SUCCESS
    remote:        [INFO] ------------------------------------------------------------------------
    remote:        [INFO] Total time:  20.474 s
    remote:        [INFO] Finished at: 2019-11-26T21:46:52Z
    remote:        [INFO] ------------------------------------------------------------------------
    remote: -----> Discovering process types
    remote:        Procfile declares types     -> (none)
    remote:        Default types for buildpack -> web
    remote:
    remote: -----> Compressing...
    remote:        Done: 84.1M
    remote: -----> Launching...
    remote:        Released v5
    remote:        https://opnu-ej.herokuapp.com/ deployed to Heroku
    remote:
    remote: Verifying deploy... done.
    To https://git.heroku.com/opnu-ej.git
     * [new branch]      master -> master
    
    d:\springdemo\ejo>
    pom.xml
    <dependencies>
    
        ...
        
        <!-- jpa, crud repository -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
    
        <!-- PostgreSQL -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
        </dependency>
    </dependencies>
    application.yml
    spring:
      jpa:
        database: POSTGRESQL
        show-sql: true
        hibernate:
          ddl-auto: create-drop
        properties:
          hibernate:
            dialect: org.hibernate.dialect.PostgreSQLDialect
      datasource:
        platform: postgres
        url: jdbc:postgresql://localhost:5432/ejournal
        username: ejournal_user
        password: 123456
        driverClassName: org.postgresql.Driver
    Student.java
    @Entity
    @Table(name = "students")
    public class Student {
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private int id;
    
        @Column(name = "first_name")
        private String firstName;
    
        @Column(name = "last_name")
        private String lastName;
    
        private int age;
    }
    pom.xml
    <dependencies>
    
        ...
    
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
    Student.java
    @Entity
    @Table(name = "students")
    @Data
    public class Student {
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private int id;
    
        @Column(name = "first_name")
        private String firstName;
    
        @Column(name = "last_name")
        private String lastName;
    
        private int age;
    }
    Service.java
    @org.springframework.stereotype.Service
    public class Service {
    
        public void addStudent(Student student, int id) {
            // Добавление нового студента
        }
    
        public List<Student> getAllStudents() {
            // Получение списка студентов
        }
    }
    Controller.java
    @RestController
    public class Controller {
    
        @Autowired
        private Service service;
    
        @PostMapping("/student")
        public void addStudent(@RequestBody Student student) {
            service.addStudent(student);
        }
    
        @GetMapping("/student")
        public List<Student> getAllStudents() {
            return service.getAllStudents();
        }
    }
    @Entity
    @Table(name = "groups")
    @Data
    public class Group {
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private int id;
        private String name;
    
        @OneToMany(mappedBy = "group")
        private List<Student> studentList;
    }
    StudentRepository.java
    public interface StudentRepository extends JpaRepository<Student, Integer> {
    }
    Service.java
    @org.springframework.stereotype.Service
    public class Service {
    
        @Autowired
        private StudentRepository studentRepo;
    
        public void addStudent(Student student) {
            studentRepo.save(student);
        }
    
        public List<Student> getAllStudents() {
            return studentRepo.findAll();
        }
    }
    Student.java
    @Entity
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Student {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int id;
        private String firstname;
        private String lastname;
        private int age;
    
        @ManyToOne (fetch = FetchType.LAZY,optional = false)
        @JoinColumn(name = "group_id",nullable = false)
        @JsonIgnore
        private Group group;
    }
    Group.java
    @Entity
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Table(name = "groups")
    public class Group {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int id;
        private String name;
    
        @OneToMany(mappedBy = "group", cascade = CascadeType.ALL)
        private List<Student> students;
    
        public void addStudent(Student student) {
            students.add(student);
        }
    }
    Controller.java
    @RestController
    public class Controller {
    
        @Autowired
        private Service service;
    
        @PostMapping("/student/{group_id}")
        public void addStudent(@RequestBody Student student, @PathVariable(name = "group_id") int group_id) {
            service.addStudent(student, group_id);
        }
    
        @GetMapping("/student")
        public List<Student> getAllStudents() {
            return service.getAllStudents();
        }
    
        @PostMapping("/group")
        public void addGroup(@RequestBody Group group) {
            service.addGroup(group);
        }
    
        @GetMapping("/group")
        public List<Group> getAllGroups() {
            return service.getAllGroups();
        }
    
    }
    GroupRepository.java
    public interface GroupRepository extends JpaRepository<Group, Integer> {}
    Service.java
    @org.springframework.stereotype.Service
    public class Service {
    
        @Autowired
        private StudentRepository studentRepo;
    
        @Autowired
        private GroupRepository groupRepo;
    
        public void addStudent(Student student, int id) {
            Group g = groupRepo.getOne(id);
            student.setGroup(g);
            studentRepo.save(student);
        }
    
        public List<Student> getAllStudents() {
            return studentRepo.findAll();
        }
    
        public void addGroup(Group group) {
            groupRepo.saveAndFlush(group);
        }
    
        public List<Group> getAllGroups() {
            return groupRepo.findAll();
        }
    }
    pom.xml
    <dependencies>
        ...
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        
        ...
        
    </dependencies>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    
    <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.3.1</version>
    </dependency>
    @Service
    public class JwiUtil {
    
        private String PRIVATE_KEY = "670d4918c206e1bfec75bcaf637dc5b8";
    
        private String generateToken(UserDetails userDetails) {
            Map<String, Object> claims = new HashMap<>();
            return createToken(claims, userDetails.getUsername());
        }
    
        private String createToken(Map<String, Object> claims, String subject) {
            return Jwts.builder()
                    .setClaims(claims)
                    .setSubject(subject)
                    .setIssuedAt(new Date(System.currentTimeMillis()))
                    .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 8))
                    .signWith(SignatureAlgorithm.HS512, PRIVATE_KEY).compact();
        }
    }
    @Service
    public class JwiUtil {
    
        private String PRIVATE_KEY = "670d4918c206e1bfec75bcaf637dc5b8";
    
        public String generateToken(UserDetails userDetails) {
            Map<String, Object> claims = new HashMap<>();
            return createToken(claims, userDetails.getUsername());
        }
    
        private String createToken(Map<String, Object> claims, String subject) {
            return Jwts.builder()
                    .setClaims(claims)
                    .setSubject(subject)
                    .setIssuedAt(new Date(System.currentTimeMillis()))
                    .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 8))
                    .signWith(SignatureAlgorithm.HS512, PRIVATE_KEY).compact();
        }
    
        public boolean validateToken(String token, UserDetails userDetails) {
            final String username = extractUsername(token);
            return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
        }
    
        private boolean isTokenExpired(String token) {
            return extractExpiration(token).before(new Date());
        }
    
        private String extractUsername(String token) {
            return extractClaim(token, Claims::getSubject);
        }
    
        private Date extractExpiration(String token) {
            return extractClaim(token, Claims::getExpiration);
        }
    
        private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
            final Claims claims = extractAllClaims(token);
            return claimsResolver.apply(claims);
        }
    
        private Claims extractAllClaims(String token) {
            return Jwts.parser().setSigningKey(PRIVATE_KEY).parseClaimsJws(token).getBody();
        }
    }
    @Data
    public class AuthRequest {
        private String username;
        private String password;
    }
    @Data
    public class AuthResponse {
        private final String jwt;
    }
    создает пользователя по умолчанию и генерирует пароль.
    Возвращаем токен как объект AuthResponse.
    Извлекаем из БД пользователя по полученному username и осуществляем валидацию токена;
  • Если jwt провел валидацию, то аутентифицируем пользователя;

  • Передаем управление дальше по цепочке фильтров.

  • resources/application.yml
    spring:
      security:
        user:
          name: nick
          password: 1234
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        
    }
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            super.configure(auth);
        }
    }
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("foo")
                    .password("bar")
                    .roles("USER");
        }
    }
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("foo")
                    .password("bar")
                    .roles("USER")
                    .and()
                    .withUser("nick")
                    .roles("MODERATOR");
        }
    }
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("foo")
                    .password("$2y$12$kElDOfhm4WgdsDc4UQjgtuz0VEi5MOqVVhXaMoD1F2lhLivokhCqe")
                    .roles("USER")
                    .and()
                    .withUser("nick")
                    .roles("MODERATOR");
        }
    
        @Bean
        public PasswordEncoder getPasswordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("foo")
                    .password("{bcrypt}$2y$12$.JzV1A3qlof1.NzZpGaTYO1b26JGHevg0900QvwrSOdU3U9.g4hta")
                    .roles("USER");
        }
    
    //    @Bean
    //    public PasswordEncoder getPasswordEncoder() {
    //        return new BCryptPasswordEncoder();
    //    }
    }
    @Controller
    public class IndexController {
    
        @GetMapping("/")
        public String index() {
            return "index";
        }
    
        @GetMapping("/user/index")
        public String user_index() {
            return "/user/index";
        }
    
        @GetMapping("/admin/index")
        public String admin_index() {
            return "/admin/index";
        }
    }
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("foo")
                    .password("$2y$12$kElDOfhm4WgdsDc4UQjgtuz0VEi5MOqVVhXaMoD1F2lhLivokhCqe")
                    .roles("USER")
                    .and()
                    .withUser("admin")
                    .password("$2y$12$DQlTV6V1wMKEoCIW5lo1huAn2/bRk4hULDmRS5Jw6YW7HayHV4K66")
                    .roles("ADMIN");
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/", "static/css", "static/js").permitAll()
                    .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                    .antMatchers("/admin/**").hasRole("ADMIN")
                    .and().formLogin();
        }
    
        @Bean
        public PasswordEncoder getPasswordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    @Service
    public class MyUserDetailsService implements UserDetailsService {
    
        @Autowired
        UserRepository repository;
    
        @Override
        public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
            return new MyUserDetails(s);
        }
    }
    public class MyUserDetails implements UserDetails {
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
        }
    
        private String userName;
    
        public MyUserDetails(String userName) {
            this.userName = userName;
        }
    
        public MyUserDetails() {
        }
    
        @Override
        public String getPassword() {
            return "$2y$12$8fKhxW71f4DzkzXYCh592.I.cd1uKkMrNwrHAApR1x5KHJ3qy1IjS";
        }
    
        @Override
        public String getUsername() {
            return userName;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    
    <!-- PostgreSQL -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
    </dependency>
    application.yml
      jpa:
        database: POSTGRESQL
        show-sql: true
        hibernate:
          ddl-auto: create-drop
        properties:
          hibernate:
            dialect: org.hibernate.dialect.PostgreSQLDialect
      datasource:
        platform: postgres
        url: jdbc:postgresql://localhost:5432/ejournal
        username: ejournal_user
        password: 123456
        driverClassName: org.postgresql.Driver
    @Entity
    @Table(name = "profile")
    @Data
    public class User {
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private int id;
    
        @Column(name = "user_name")
        private String userName;
        private String password;
        private boolean active;
        private String roles;
    }
    @Service
    public class MyUserDetailsService implements UserDetailsService {
    
        @Autowired
        UserRepository repository;
    
        @Override
        public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
            Optional<User> user = repository.findByUserName(s);
    
            user.orElseThrow(() -> new UsernameNotFoundException("User not found: " + s));
    
            return user.map(MyUserDetails::new).get();
        }
    }
    public interface UserRepository extends JpaRepository<User, Integer> {
    
        Optional<User> findByUserName(String userName);
    }
    public class MyUserDetails implements UserDetails {
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorityList;
        }
    
        private String userName;
        private String password;
        private boolean active;
        private List<GrantedAuthority> authorityList;
    
        public MyUserDetails(User userName) {
            this.userName = userName.getUserName();
            this.password = userName.getPassword();
            this.active = userName.isActive();
            this.authorityList = Arrays.stream(userName.getRoles().split(",")).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        }
    
        public MyUserDetails() {
        }
    
        @Override
        public String getPassword() {
            return password;
        }
    
        @Override
        public String getUsername() {
            return userName;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return active;
        }
    }
    @org.springframework.web.bind.annotation.RestController
    public class RestController {
    
        @Autowired
        private AuthenticationManager authManager;
    
        @Autowired
        private JwtUtil jwiUtil;
    
        @Autowired
        MyUserDetailsService userDetailsService;
    
        @PostMapping(value = "/auth")
        public ResponseEntity<?> createAuthToken(@RequestBody AuthRequest request) throws BadCredentialsException {
            try {
                authManager.authenticate(
                        new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
                );
            } catch (BadCredentialsException ex) {
                ex.printStackTrace();
            }
    
            final UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
            final String jwt = jwiUtil.generateToken(userDetails);
    
            return ResponseEntity.ok(new AuthResponse(jwt));
        }
    }
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        MyUserDetailsService userDetailsService;
    
        @Autowired
        JwtRequestFilter filter;
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService);
        }
    
        // Авторизация
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                    .authorizeRequests()
                    .antMatchers("/", "static/css", "static/js").permitAll()
                    .antMatchers("/auth").permitAll()
                    .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                    .antMatchers("/admin/**").hasRole("ADMIN")
                    .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    
        @Bean
        public PasswordEncoder getPasswordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Override
        @Bean
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    }
    @GetMapping(value = "/helloworld")
    public String helloWorldRequest(Principal principal) {
        final UserDetails userDetails = userDetailsService.loadUserByUsername(principal.getName());
        return "<h1>Hello " + userDetails.getAuthorities().toString() + " </h1>";
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/", "static/css", "static/js").permitAll()
                .antMatchers("/auth").permitAll()
                .antMatchers("/helloworld").authenticated()
                .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .antMatchers("/admin/**").hasRole("ADMIN")
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
    @Component
    public class JwtRequestFilter extends OncePerRequestFilter {
    
        @Autowired
        private JwtUtil jwtUtil;
    
        @Autowired
        MyUserDetailsService userDetailsService;
    
        @Override
        protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
    
            final String authHeader = httpServletRequest.getHeader("Authorization");
    
            String jwt = null;
            String username = null;
    
            if (authHeader != null && authHeader.startsWith("Bearer ")) {
                jwt = authHeader.substring(7);
                username = jwtUtil.extractUsername(jwt);
            }
    
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
    
                if (jwtUtil.validateToken(jwt, userDetails)) {
                    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(token);
                }
            }
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        }
    }
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        // Авторизация
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                    .authorizeRequests()
                    .antMatchers("/", "static/css", "static/js").permitAll()
                    .antMatchers("/auth").permitAll()
                    .antMatchers("/helloworld","/me").authenticated()
                    .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                    .antMatchers("/admin/**").hasRole("ADMIN")
                    .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    
            http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
        }
    }