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
.
Вот как выглядит наша страница, когда мы вообще не вводим никаких данных:
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()
, которая использовалась выше.
Вот эффект:
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>
И вот что мы видим на странице:
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()
для достижения того же.
Вот что мы видим при вводе неверного ввода:
4. Вывод
В этом руководстве мы создали простое приложение Spring Boot, чтобы продемонстрировать, как отображать различные типы ошибок в Thymeleaf .
Мы рассмотрели отображение ошибок полей по одной, а затем все сразу, ошибки вне HTML-форм и глобальные ошибки.
Как всегда, исходный код доступен на GitHub .