Bean Validation 1.1 , среди многих новых функций, ввел интерполяцию сообщений об ошибках с использованием выражений Unified Expression Language (EL). Это позволяет определять сообщения об ошибках на основе условной логики, а также включает расширенные параметры форматирования . Добавленный в приложение Spring MVC, позволяет отображать более понятные сообщения об ошибках довольно просто.
В первой части этой статьи я кратко опишу интерполяцию сообщений с помощью выражений EL, во второй части мы создадим простое веб-приложение с Spring Boot и Thymeleaf, которое работает на Tomcat 8.
EL выражения в сообщениях — примеры
Чтобы визуализировать некоторые возможности лучшей интерполяции сообщений с помощью выражений EL, я буду использовать следующий класс:
|
1
2
3
4
5
|
public class Bid { private String bidder; private Date expiresAt; private BigDecimal price;} |
Пример 1: текущее проверенное значение
Механизм проверки делает текущее проверенное значение доступным в контексте EL как validatedValue :
|
1
2
|
@Size(min = 5, message = "\"${validatedValue}\" is too short.")private String bidder; |
Сообщение об ошибке, когда для участника, равного «Джон», будет:
«Джон» слишком короткий.
Пример 2: условная логика
Условная логика с выражением EL возможна в сообщениях об ошибках. В приведенном ниже примере, если длина подтвержденного участника торгов меньше 2, мы отображаем другое сообщение:
|
1
2
|
@Size(min = 5, message = "\"${validatedValue}\" is ${validatedValue.length() < 2 ? 'way' : ''} too short.")private String bidder; |
Когда претендент равен «J», сообщение будет:
«J» слишком короткий.
Когда претендент равен «Джон», сообщение будет:
«Джон» слишком короткий.
Пример 3: Форматер
Механизм проверки делает объект formatter доступным в контексте EL. formatter ведет себя java.util.Formatter.format(String format, Object... args) . В приведенном ниже примере дата отформатирована в ISO Date:
|
1
2
|
@Future(message = "The value \"${formatter.format('%1$tY-%1$tm-%1$td', validatedValue)}\" is not in future!")private Date expiresAt; |
Когда срок годности равен 2001-01-01, сообщение будет:
Значение «2001-01-01» не в будущем!
Обратите внимание, что в этом примере используется java.util.Date . Hibernate Validator 5.1.1 пока не поддерживает проверку новых типов даты и времени. Он будет представлен в Hibernate Validator 5.2. См. Дорожная карта валидатора Hibernate .
Создание приложения Spring MVC
Чтобы представить, как Bean Validation 1.1 можно использовать с Spring MVC, мы создадим простое веб-приложение с использованием Spring Boot.
Во-первых, нам нужно создать проект Spring Boot. Мы можем начать с Spring Initializr и создать проект со следующими характеристиками:
- Группа : pl.codeleak.beanvalidation11-демо
- Артефакт : beanvalidation11-demo
- Название : Bean Validation 1.1 Демо
- Название пакета : pl.codeleak.demo
- Стили : паутина, тимелист
- Тип : Maven Project
- Упаковка : война
- Версия Java : 1.8
- Язык : Java
После нажатия кнопки «Создать» файл будет загружен. Структура сгенерированного проекта следующая:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
src├───main│ ├───java│ │ └───pl│ │ └───codeleak│ │ └───demo│ └───resources│ ├───static│ └───templates└───test └───java └───pl └───codeleak └───demo |
По состоянию на июнь 2014 года сгенерированный POM выглядел следующим образом:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <modelVersion>4.0.0</modelVersion> <groupId>pl.codeleak.beanvalidation11-demo</groupId> <artifactId>beanvalidation11-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <name>Bean Validation 1.1 Demo</name> <description></description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.1.1.RELEASE</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <start-class>pl.codeleak.demo.Application</start-class> <java.version>1.8</java.version> </properties> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project> |
Это было быстро! Spring Initializr действительно удобен! Создав проект, вы можете импортировать его в свою любимую среду IDE.
Изменение свойств проекта
Bean Validation 1.1 реализован в Hibernate Validator 5.x. Мы будем использовать Hibernate Validator 5.1.1, поэтому нам нужно будет добавить его в наш проект, и, поскольку Spring Boot 1.1.1. RELEASE использует Hibernate Validator 5.0.3, нам потребуется изменить одно из свойств POM:
|
1
2
3
|
<properties> <hibernate-validator.version>5.1.1.Final</hibernate-validator.version></properties> |
В проекте мы будем использовать Tomcat 8. Но почему мы не можем работать с Tomcat 7? Hibernate Validator 5.x требует Expression EL API 2.2.4 и его реализации. А реализация предоставлена в Tomcat 8. Чтобы запустить приложение Spring Boot на Tomcat 8, нам нужно добавить еще одно свойство:
|
1
2
3
|
<properties> <tomcat.version>8.0.8</tomcat.version></properties> |
Создание ставки: Контроллер
Для создания ставки нам понадобится контроллер. Контроллер имеет два метода: отобразить форму и создать ставку:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
@Controllerpublic class BidController { @RequestMapping(value = "/") public String index(Model model) { model.addAttribute("bid", new Bid("John", new Date(), BigDecimal.valueOf(5.00))); return "index"; } @RequestMapping(value = "/", method = RequestMethod.POST) public String create(@ModelAttribute @Valid Bid bid, Errors errors) { if (errors.hasErrors()) { return "index"; } // create a bid here return "redirect:/"; }} |
Окончательный код класса Bid ниже. Обратите внимание, что сообщения не указываются напрямую в классе Bid . Я переместил их в файл пакета ValidationMessages ( ValidationMessages.properties в src/main/resources ).
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
public class Bid { @Size.List({ @Size(min = 5, message = "{bid.bidder.min.message}"), @Size(max = 10, message = "{bid.bidder.max.message}") }) private String bidder; @NotNull @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @Future(message = "{bid.expiresAt.message}") private Date expiresAt; @NotNull @DecimalMin(value = "10.00", message = "{bid.price.message}") @NumberFormat(style = NumberFormat.Style.CURRENCY) private BigDecimal price; protected Bid() {} public Bid(String bidder, Date expiresAt, BigDecimal price) { this.bidder = bidder; this.expiresAt = expiresAt; this.price = price; } public String getBidder() { return bidder; } public Date getExpiresAt() { return expiresAt; } public BigDecimal getPrice() { return price; } public void setBidder(String bidder) { this.bidder = bidder; } public void setExpiresAt(Date expiresAt) { this.expiresAt = expiresAt; } public void setPrice(BigDecimal price) { this.price = price; }} |
Создание ставки: просмотр
Теперь мы создадим простую страницу в Thymeleaf, которая содержит нашу форму заявки. Страница будет index.html и перейдет в src/main/resources/templates .
|
1
2
3
4
5
6
7
|
<form class="form-narrow form-horizontal" method="post" th:action="@{/}" th:object="${bid}">[...]</form> |
В случае ошибки проверки мы выведем общее сообщение:
|
1
2
3
4
5
6
|
<th:block th:if="${#fields.hasErrors('${bid.*}')}"> <div class="alert alert-dismissable" th:classappend="'alert-danger'"> <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> <span th:text="Form contains errors. Please try again.">Test</span> </div></th:block> |
Каждое поле формы будет помечено красным цветом и будет отображено соответствующее сообщение:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
<div class="form-group" th:classappend="${#fields.hasErrors('bidder')}? 'has-error'"> <label for="bidder" class="col-lg-4 control-label">Bidder</label> <div class="col-lg-8"> <input type="text" class="form-control" id="bidder" th:field="*{bidder}" /> <span class="help-block" th:if="${#fields.hasErrors('bidder')}" th:errors="*{bidder}"> Incorrect </span> </div></div> |
Создание некоторых тестов
На этом этапе мы могли бы запустить приложение, но вместо этого мы создадим несколько тестов, чтобы проверить, работает ли проверка как ожидалось. Для этого мы создадим BidControllerTest :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
@RunWith(SpringJUnit4ClassRunner.class)@SpringApplicationConfiguration(classes = Application.class)@WebAppConfigurationpublic class BidControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); }} |
Тестовая заглушка готова. Настало время для некоторых испытаний. Давайте сначала проверим, правильно ли отображается «форма», проверив, что модель содержит объект предложения и имя представления равно index :
|
1
2
3
4
5
6
7
|
@Testpublic void displaysABidForm() throws Exception { this.mockMvc.perform(get("/")) .andExpect(status().isOk()) .andExpect(model().attribute("bid", any(Bid.class))) .andExpect(view().name("index"));} |
В следующем тесте мы проверим, что, если введены правильные данные, форма не содержит сообщения об ошибке (сценарий счастливого потока). Обратите внимание, что с Thymeleaf в качестве движка просмотра мы можем просто проверить сгенерированное представление.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
@Testpublic void postsAValidBid() throws Exception { this.mockMvc.perform(post("/") .param("bidder", "John Smith") .param("expiresAt", "2020-01-01") .param("price", "11.88")) .andExpect(content().string( not( containsString("Form contains errors. Please try again.") ) ) );} |
В следующих нескольких тестах мы будем проверять валидацию определенных объектов. Названия тестов должны быть достаточно наглядными, поэтому дальнейшее объяснение не требуется. Посмотрите на код:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
@Testpublic void postsABidWithBidderTooShort() throws Exception { this.mockMvc.perform(post("/").param("bidder", "John")) // too short .andExpect(content().string( allOf( containsString("Form contains errors. Please try again."), containsString(""John" is too short. Should not be shorter than 5") ) ) );}@Testpublic void postsABidWithBidderWayTooShort() throws Exception { this.mockMvc.perform(post("/").param("bidder", "J")) // way too short .andExpect(content().string( allOf( containsString("Form contains errors. Please try again."), containsString(""J" is way too short. Should not be shorter than 5") ) ) );}@Testpublic void postsABidWithBidderTooLong() throws Exception { this.mockMvc.perform(post("/").param("bidder", "John S. Smith")) // too long .andExpect(content().string( allOf( containsString("Form contains errors. Please try again."), containsString(""John S. Smith" is too long. Should not be longer than 10") ) ) );}@Testpublic void postsABidWithBidderWayTooLong() throws Exception { this.mockMvc.perform(post("/").param("bidder", "John The Saint Smith")) .andExpect(content().string( allOf( containsString("Form contains errors. Please try again."), containsString(""John The Saint Smith" is way too long. Should not be longer than 10") ) ) );}@Testpublic void postsABidWithExpiresAtInPast() throws Exception { this.mockMvc.perform(post("/").param("expiresAt", "2010-01-01")) .andExpect(content().string( allOf( containsString("Form contains errors. Please try again."), containsString("Value "2010-01-01" is not in future!") ) ) );}@Testpublic void postsABidWithPriceLowerThanFive() throws Exception { this.mockMvc.perform(post("/").param("price", "4.99")) .andExpect(content().string( allOf( containsString("Form contains errors. Please try again."), containsString("Value "4.99" is incorrect. Must be greater than or equal to 10.00") ) ) );} |
Довольно просто.
Запуск приложения
Поскольку приложение имеет тип war , вам может понадобиться загрузить сервер Tomcat 8.0.8, создать пакет с пакетом mvn clean package и развернуть приложение на сервере.
Чтобы использовать встроенный бегунок Tomcat, вам нужно изменить тип упаковки на jar и установить область зависимостей spring-boot-starter-tomcat по умолчанию ( compile ) в pom.xml :
|
01
02
03
04
05
06
07
08
09
10
11
12
|
[...]<packaging>jar</packaging>[...]<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId></dependency>[...] |
Теперь вы можете создать пакет с помощью пакета mvn clean package и запустить сгенерированный файл jar с помощью команды java -jar . Конечно, вы также можете запустить проект из IDE, запустив класс pl.codeleak.demo.Application .
Резюме
Если вы заинтересованы в том, чтобы увидеть полный исходный код представленного примера, проверьте мой репозиторий GitHub: spring-mvc-beanvalidation11-demo .
После прочтения этой статьи вы должны знать:
- Как использовать Bean Validation 1.1 в приложении Spring MVC с Tomcat 8
- Как улучшить сообщения об ошибках, используя выражения EL
- Как создать приложение с нуля с помощью Spring Boot
- Как проверить валидацию с помощью Spring Test
Возможно, вас заинтересует мой предыдущий пост о начальной загрузке приложения Spring MVC с Thymeleaf и Maven: HOW-TO: Spring Boot и Thymeleaf с Maven .
Вы также можете посмотреть другие посты о валидации, которые я написал в прошлом:
| Ссылка: | Лучшие сообщения об ошибках с Bean Validation 1.1 в приложении Spring MVC от нашего партнера по JCG Рафаля Боровца в блоге Codeleak.pl . |