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

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

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

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

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

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

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

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

  • DispatcherServlet на основании полученного ModelAndView, должен определить, какое представление будет выводить данные. Для этого используется арбитр представлений (View Resolver), который на основании полученного логического имени представления возвращает ссылку на файл View;

  • в представление передаются данные (Model) и обратно, если необходимо, посылается ответ от представления.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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>

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

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

Создадим две 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>

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

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 добавим ссылку для кнопки "Добавить студента" . Для формирования ссылки используем тег th:href. Для указания пути относительно домена используем комбинацию @{~}.

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>

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

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

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

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

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

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

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

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

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

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;
    }
}

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

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. В полях формы добавим привязку к полям объекта, в теге формы укажем название объекта (исходя из метода контроллера он должен называться "student", а также укажем URL для отправки данных формы).

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>

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

StudentController.java
@Controller
public class StudentController {

    ...

    @PostMapping("/add_student")
    public String greetingSubmit(@ModelAttribute Student student) {
            return "redirect:/";
    }
}

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

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> {}

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

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:/";
    }
}

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

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

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

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

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

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

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. Добавим вывод полей каждого объекта типа Student в таблице

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

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

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

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

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

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

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

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. Добавим необходимые ограничения для полей сущности.

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;
    
    ...
}

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

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

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

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

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

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

    @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.

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

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>

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

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

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

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

<!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>

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

<!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>

Last updated