在许多新功能中, Bean Validation 1.1引入了使用统一表达式语言(EL)表达式的错误消息插值。 这允许基于条件逻辑来定义错误消息,还可以启用高级格式化选项 。 添加到Spring MVC应用程序后,您可以非常简单地显示更友好的错误消息。
在本文的第一部分中,我将简短描述使用EL表达式的消息插值,在第二部分中,我们将使用Spring Boot和Thymeleaf构建一个运行在Tomcat 8上的简单Web应用程序。
消息中的EL表达式–示例
为了可视化用EL表达式更好地进行消息内插的一些可能性,我将使用以下类:
public class Bid {
private String bidder;
private Date expiresAt;
private BigDecimal price;
}
示例1:当前验证的值
验证引擎将当前已验证的值在EL上下文中提供为validatedValue
:
@Size(min = 5, message = "\"${validatedValue}\" is too short.")
private String bidder;
对于等于“约翰”的竞标者,错误消息将是:
“约翰”太短了。
示例2:条件逻辑
错误消息中可能带有EL表达式的条件逻辑。 在以下示例中,如果经过验证的竞标者的长度短于2,我们将显示不同的消息:
@Size(min = 5, message = "\"${validatedValue}\" is ${validatedValue.length() < 2 ? 'way' : ''} too short.")
private String bidder;
当投标人等于“ J”时,消息将是:
“ J”太短了。
当出价者等于“ John”时,消息将是:
“约翰”太短了。
示例3:格式化程序
验证引擎使formatter
对象在EL上下文中可用。 formatter
行为为java.util.Formatter.format(String format, Object... args)
。 在下面的示例中,日期格式为ISO日期:
@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
使用了java.util.Date
。 Hibernate Validator 5.1.1尚不支持对新的Date-Time类型的验证。 它将在Hibernate Validator 5.2中引入。 请参见Hibernate Validator路线图 。
创建Spring MVC应用程序
为了可视化如何将Bean Validation 1.1与Spring MVC一起使用,我们将使用Spring Boot构建一个简单的Web应用程序。
首先,我们需要创建一个Spring Boot项目。 我们可以从Spring Initializr开始,并生成具有以下特征的项目:
- 组 :pl.codeleak.beanvalidation11-demo
- 工件 :beanvalidation11-demo
- 名称 :Bean验证1.1演示
- 软件包名称:pl.codeleak.demo
- 风格 :网,胸腺
- 类型 :Maven项目
- 包装 :战争
- Java版本 :1.8
- 语言 :Java
单击生成后,将下载文件。 生成的项目的结构如下:
src
├───main
│ ├───java
│ │ └───pl
│ │ └───codeleak
│ │ └───demo
│ └───resources
│ ├───static
│ └───templates
└───test
└───java
└───pl
└───codeleak
└───demo
截至2014年6月,生成的POM如下所示:
<?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"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<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验证1.1由Hibernate Validator 5.x实现 。 我们将使用Hibernate Validator 5.1.1,因此我们需要将其添加到我们的项目中,并且在Spring Boot 1.1.1中,RELEASE使用Hibernate Validator 5.0.3时,我们将需要修改POM属性之一:
<properties>
<hibernate-validator.version>5.1.1.Final</hibernate-validator.version>
</properties>
在该项目中,我们将使用Tomcat8。但是为什么不能使用Tomcat 7? Hibernate Validator 5.x需要Expression EL API 2.2.4及其实现。 并且该实现在Tomcat 8中提供。要在Tomcat 8上运行Spring Boot应用程序,我们将需要添加另一个属性:
<properties>
<tomcat.version>8.0.8</tomcat.version>
</properties>
创建出价:控制器
为了创建出价,我们需要一个控制器。 控制器有两种方法:显示表单和创建出价:
@Controller
public 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
捆绑文件( src/main/resources
ValidationMessages.properties
)。
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
。
<form
class="form-narrow form-horizontal" method="post"
th:action="@{/}" th:object="${bid}">
[...]
</form>
如果发生验证错误,我们将显示一条常规消息:
<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>
每个表单字段都将标记为红色,并显示相应的消息:
<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
:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class BidControllerTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
}
测试存根已准备就绪。 现在该进行一些测试了。 首先,通过验证模型是否包含出价对象以及视图名称是否等于index
检查表单是否正确“显示”:
@Test
public void displaysABidForm() throws Exception {
this.mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(model().attribute("bid", any(Bid.class)))
.andExpect(view().name("index"));
}
在下一个测试中,我们将验证,如果输入了正确的数据,则表格中不包含错误消息(快乐流程情况)。 请注意,使用Thymeleaf作为视图引擎,我们可以简单地验证生成的视图。
@Test
public 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.")
)
)
);
}
在接下来的几个测试中,我们将检查某些对象的有效性。 测试的名称应具有足够的描述性,因此无需进一步说明。 看一下代码:
@Test
public 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")
)
)
);
}
@Test
public 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")
)
)
);
}
@Test
public 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")
)
)
);
}
@Test
public 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")
)
)
);
}
@Test
public 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!")
)
)
);
}
@Test
public 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
,并在pom.xml
中将spring-boot-starter-tomcat
依赖范围设置为default( compile
):
[...]
<packaging>jar</packaging>
[...]
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
[...]
现在,您可以使用mvn clean package
创建一个包,并使用java -jar
命令运行生成的jar文件。 当然,您也可以通过运行pl.codeleak.demo.Application
类从IDE中运行项目。
摘要
如果您有兴趣查看所提供示例的完整源代码,请检查我的GitHub存储库: spring-mvc-beanvalidation11-demo 。
阅读本文后,您应该知道:
- 如何在带有Tomcat 8的Spring MVC应用程序中使用Bean验证1.1
- 如何使用EL表达式改善错误消息
- 如何使用Spring Boot从头开始构建应用程序
- 如何使用Spring Test测试验证
您可能对我以前的有关使用Thymeleaf和Maven引导Spring MVC应用程序的帖子感兴趣: HOW-TO:使用Maven引导Spring MVC 和Thymeleaf 。
您可能还想看看我过去写的一些其他有关验证的文章: