Перейти к основному содержимому

Отображение сообщений об ошибках с Thymeleaf весной

· 7 мин. чтения

1. Обзор

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

В наших демонстрационных целях мы создадим простое приложение Spring Boot User Registration и проверим отдельные поля ввода. Кроме того, мы увидим пример того, как обрабатывать ошибки глобального уровня.

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

2. Пример приложения Spring Boot

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

Однако еще до этого мы должны добавить зависимости Maven.

2.1. Зависимость от Maven

Давайте добавим все стартеры Spring Boot, которые нам понадобятся — Web для бита MVC, Validation для проверки объекта в спящем режиме, Thymeleaf для пользовательского интерфейса и JPA для репозитория. Кроме того, нам понадобится зависимость H2 , чтобы иметь базу данных в памяти:

<dependency> 
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
<version>1.4.200</version>
</dependency>

2.2. Лицо

Вот наша сущность пользователя :

@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@NotEmpty(message = "User's name cannot be empty.")
@Size(min = 5, max = 250)
private String fullName;

@NotEmpty(message = "User's email cannot be empty.")
private String email;

@NotNull(message = "User's age cannot be null.")
@Min(value = 18)
private Integer age;

private String country;

private String phoneNumber;

// getters and setters
}

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

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

2.3. Репозиторий

Мы будем использовать простой репозиторий JPA для нашего основного варианта использования:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {}

2.4. Контроллер

Наконец, чтобы связать все вместе на бэкенде, давайте соберем UserController :

@Controller
public class UserController {

@Autowired
private UserRepository repository;
@GetMapping("/add")
public String showAddUserForm(User user) {
return "errors/addUser";
}

@PostMapping("/add")
public String addUser(@Valid User user, BindingResult result, Model model) {
if (result.hasErrors()) {
return "errors/addUser";
}
repository.save(user);
model.addAttribute("users", repository.findAll());
return "errors/home";
}
}

Здесь мы определяем GetMapping по пути /add для отображения регистрационной формы. Наш PostMapping по тому же пути занимается валидацией при отправке формы, с последующим сохранением в репозиторий, если все пойдет хорошо.

3. Шаблоны Thymeleaf с сообщениями об ошибках

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

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

3.1. Отображение ошибок поля

Thymeleaf предлагает встроенный метод field.hasErrors , который возвращает логическое значение в зависимости от того, существуют ли какие-либо ошибки для данного поля. Объединив его с th:if, мы можем выбрать отображение ошибки, если она существует:

<p th:if="${#fields.hasErrors('age')}">Invalid Age</p>

Далее, если мы хотим добавить какие-либо стили, мы можем условно использовать th :class :

<p  th:if="${#fields.hasErrors('age')}" th:class="${#fields.hasErrors('age')}? error">
Invalid Age</p>

Наша простая встроенная ошибка класса CSS окрашивает элемент в красный цвет:

<style>
.error {
color: red;
}
</style>

Другой атрибут Thymeleaf th:errors дает нам возможность отображать все ошибки в указанном селекторе, скажем, в электронной почте:

<div>
<label for="email">Email</label> <input type="text" th:field="*{email}" />
<p th:if="${#fields.hasErrors('email')}" th:errorclass="error" th:errors="*{email}" />
</div>

В приведенном выше фрагменте мы также можем увидеть вариант использования стиля CSS. Здесь мы используем th:errorclass , что избавляет нас от необходимости использовать какие-либо условные атрибуты для применения CSS .

В качестве альтернативы мы можем перебирать все сообщения проверки в данном поле, используя th:each :

<div>
<label for="fullName">Name</label> <input type="text" th:field="*{fullName}"
id="fullName" placeholder="Full Name">
<ul>
<li th:each="err : ${#fields.errors('fullName')}" th:text="${err}" class="error" />
</ul>
</div>

Примечательно, что здесь мы использовали другой метод Thymeleaf fields.errors() для сбора всех сообщений проверки, возвращаемых нашим серверным приложением для поля fullName .

Теперь, чтобы проверить это, давайте запустим наше загрузочное приложение и перейдем к конечной точке http://localhost:8080/add .

Вот как выглядит наша страница, когда мы вообще не вводим никаких данных:

./3989e33d6a67abbe16cc0e5e44d302b9.png

3.2. Отображение всех ошибок сразу

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

Для этого мы будем использовать метод fields.hasAnyErrors() Thymeleaf :

<div th:if="${#fields.hasAnyErrors()}">
<ul>
<li th:each="err : ${#fields.allErrors()}" th:text="${err}" />
</ul>
</div>

Как мы видим, здесь мы использовали другой вариант fields.allErrors() для перебора всех ошибок во всех полях HTML-формы.

Вместо fields.hasAnyErrors() мы могли бы использовать #fields.hasErrors('*') . Точно так же #fields.errors('*') является альтернативой #fields.allErrors() , которая использовалась выше.

Вот эффект:

./59d88403fd82ffd80da823c07273399c.png

3.3. Отображение ошибок вне форм

Следующий. давайте рассмотрим сценарий, в котором мы хотим отображать сообщения проверки вне HTML-формы.

В этом случае вместо выбора или (*{….}) нам просто нужно использовать полное имя переменной в формате (${….}) :

<h4>Errors on a single field:</h4>
<div th:if="${#fields.hasErrors('${user.email}')}"
th:errors="*{user.email}"></div>
<ul>
<li th:each="err : ${#fields.errors('user.*')}" th:text="${err}" />
</ul>

Это отобразит все сообщения об ошибках в поле электронной почты .

Теперь давайте посмотрим, как мы можем отобразить все сообщения сразу :

<h4>All errors:</h4>
<ul>
<li th:each="err : ${#fields.errors('user.*')}" th:text="${err}" />
</ul>

И вот что мы видим на странице:

./3665beb2e432329229407c26d688d4d9.png

3.4. Отображение глобальных ошибок

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

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

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

Во-первых, мы добавим сервис для выполнения этой проверки:

@Service
public class UserValidationService {
public String validateUser(User user) {
String message = "";
if (user.getCountry() != null && user.getPhoneNumber() != null) {
if (user.getCountry().equalsIgnoreCase("India")
&& !user.getPhoneNumber().startsWith("91")) {
message = "Phone number is invalid for " + user.getCountry();
}
}
return message;
}
}

Как видим, мы добавили тривиальный случай. Для страны Индия номер телефона должен начинаться с префикса 91 .

Во-вторых, нам понадобится настроить PostMapping нашего контроллера :

@PostMapping("/add")
public String addUser(@Valid User user, BindingResult result, Model model) {
String err = validationService.validateUser(user);
if (!err.isEmpty()) {
ObjectError error = new ObjectError("globalError", err);
result.addError(error);
}
if (result.hasErrors()) {
return "errors/addUser";
}
repository.save(user);
model.addAttribute("users", repository.findAll());
return "errors/home";
}

Наконец, в шаблоне Thymeleaf мы добавим глобальную константу для отображения такого типа ошибки :

<div th:if="${#fields.hasErrors('global')}">
<h3>Global errors:</h3>
<p th:each="err : ${#fields.errors('global')}" th:text="${err}" class="error" />
</div>

В качестве альтернативы вместо константы мы можем использовать методы #fields.hasGlobalErrors() и #fields.globalErrors() для достижения того же.

Вот что мы видим при вводе неверного ввода:

./8cdcd641f17fa4e74ca92b191941a2eb.png

4. Вывод

В этом руководстве мы создали простое приложение Spring Boot, чтобы продемонстрировать, как отображать различные типы ошибок в Thymeleaf .

Мы рассмотрели отображение ошибок полей по одной, а затем все сразу, ошибки вне HTML-форм и глобальные ошибки.

Как всегда, исходный код доступен на GitHub .