5. Работа со Spring MVC, часть 2

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

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

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

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>

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

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