简介
在前一篇文章中,我简单讲述了一下spring mvc的结构和mvc模式在该框架中的应用。对于一个普通的web页面来说,通常的交互无非为两种。一种是数据的读取和展示,另外一种就是数据的提交和保存。在前面已经提到过怎么显示一些内容到页面上。和显示内容不同,本文重点讲述怎么提交form表单到服务器。提交表单的过程相对于纯展示数据要复杂得多。通常需要考虑到我们要提交哪些数据,通过什么样的方式提交,怎么样保证提交的数据内容是合法的以及怎么保存提交的数据。这些就是本文要讨论的重点。由于要讨论的内容比较多,本文会比较长。
领域对象定义
在讨论具体对象展示和创建之前,我们先假定一个我们需要操作的领域对象:
public class Product { private Long id; private String productId; private String name; private BigDecimal unitPrice; private String description; private String manufacturer; private String category; private long unitsInStock; private long unitsInOrder; private boolean discontinued; private String condition; private MultipartFile productImage; // get set methods ignored public Product() { super(); } public Product(String productId, String name, BigDecimal unitPrice) { this.productId = productId; this.name = name; this.unitPrice = unitPrice; } }
假定我们需要操作的对象为Product。我们后面需要做的就是在页面里展示Product的内容以及提交新的Product对象。处于篇幅的限制,这里省略了一些get, set方法。
表单展示和元素绑定
为了显示表单数据,首先需要定义controller,假设这里映射的路径是/products/add,那么该方法的定义如下:
@Controller @RequestMapping("/products") public class ProductController { @RequestMapping(value = "/add", method = RequestMethod.GET) public String getAddNewProductForm(@ModelAttribute("newProduct") Product newProduct, Model model) { return "addProduct"; } }
这里需要注意的地方是,我们定义了一个ModelAttribute的annotation。它映射到表单中对应的字段。方法返回的结果addProduct对应的表示要显示的页面使用addProduct.jsp文件。
对应的addProduct.jsp文件的内容如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <spring:url var="addUrl" value="/products/add"/> <spring:message var="productIdLabel" code="addProduct.form.productId.label"/> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"> <title>Products</title> </head> <body> <section> <div class="jumbotron"> <div class="container"> <h1>Products</h1> <p>Add products</p> </div> </div> </section> <section class="container"> <form:form modelAttribute="newProduct" class="form-horizontal" enctype="multipart/form-data"> <fieldset> <legend>Add new product</legend> <div class="form-group"> <label class="control-label col-lg-2 col-lg-2" for="productId"><spring:message code="addProduct.form.productId.label"/></label> <div class="col-lg-10"> <form:input id="productId" path="productId" type="text" class="form:input-large"/> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="name">Product Id</label> <div class="col-lg-10"> <form:input id="name" path="name" type="text" class="form:input-large"/> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="unitPrice">name</label> <div class="col-lg-10"> <div class="form:input-prepend"> <form:input id="unitPrice" path="unitPrice" type="text" class="form:input-large"/> </div> </div> </div> <div class="form-group"> <div class="col-lg-offset-2 col-lg-10"> <input type="submit" id="btnAdd" class="btn btn-primary" value ="Add"/> </div> </div> </fieldset> </form:form> </section> </body> </html>
在开头的地方,我们引用了form的一些标准库,比如<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>。在后面的form使用中,最开始表单的定义部分如下:<form:form modelAttribute="newProduct"/> 需要注意到的就是这里并不是用的默认的html form,而是spring taglib里的form,而且设定的modelAttribute为newProduct,这和前面ProductController方法里ModelAttribute参数指定的值必须一致。
这样做有什么意义呢?一般来说,我们定义的领域对象一般是默认的java对象(POJO) 。当我们需要将这些对象显示到页面上去的时候,就存在一个java对象元素到页面元素的映射。也就是说,我java对象里定义的某个元素要对应到页面里哪个项来显示。于是在spring里有一种方式,叫做表单元素绑定。也就是我们前面定义的modelAttribute,这样它就自动将java对象给绑定到页面元素了。
当然,只是定义了modelAttribute还是不够的,具体对应到哪一个还需要具体的定义。比如说,我们希望页面元素里的ProductId对应java对象里的ProductId,那么在页面里的定义则如下:
<form:input id="productId" path="productId" type="text" class="form:input-large"/> id表示页面元素名称,而path对应的是java对象的属性名称。
这个时候,如果我们运行程序,打开如下页面:http://localhost:8080/SampleWebStore/products/add,将看到如下的页面:
在前面的讨论中,我们提到了从服务器端定义的java对象映射到页面显示元素的过程。我们可以称其为表单的outbound。在另一方面,如果我们从表单提交一组数据到服务器端,将这些数据映射到对应的java对象。这个过程可以称其为表单的inbound。它们之间的关系可以用如下的图来描述:
实际上,不管是inbound还是outbound,将表单元素和我们定义的领域对象进行映射的一个基本方法就是在页面和controller方法里定义同样的modelAttribute属性。
上述jsp页面里还有一个值得我们注意的地方,就是既然这是一个表单,那么它就需要被提交。该怎么定义它提交的目标方呢?在前面的定义里我们并没有定义类似于传统html form里的action部分。 那么当我们在页面上提交表单,它将被提交到哪里呢?
在spring定义的taglib里,它的form默认提交给当前url。比如我们当前的路径是/products/add,那么当点击提交按钮的时候,相当于给该路径发送http post请求。在实际应用中,我们可能会根据需要提交表单到不同的路径,那么该怎么做呢?一种典型的办法就是定义一个路径的变量,再将其传递过来。比如如下部分:
<c:url var="submitAddProductUrl" value="/products/add" /> <form:form modelAttribute="newProduct" action="${submitAddProductUrl}" class="form-horizontal">
因为spring taglib里不支持在一种元素里嵌套其他元素,所以必须采用上述的方式。这样也不会导致解析的时候出现和期望不一致。
定义上述action url的方式也可以采用除了java core taglib以外的,比如:
<spring:url var="addUrl" value="/products/add"/>
总之,概括起来就是最好使用spring带的这一套表单和路径定义,它总体来说还是比较符合我们的直觉。 这样,表格元素的展示就已经基本讨论完了。在提交后该怎么处理,就需要在controller里专门定义方法来处理。具体的处理在后面部分会继续详细讨论。
externalize界面显示元素
在前面的页面里,我们将所有页面显示的样式都是硬编码在页面上的。比如我们要显示一个product id的元素,就在页面上显示product id这个部分。这样做虽然简单但是缺少一点灵活性。假如我们在页面里需要加入多语言支持,那该怎么办呢?于是在spring mvc里就有了对页面元素的externalize支持,也相当于是内容和显示的分离。
要实现上述功能的步骤也比较简单:
1. 在前面配置文件dispatcher-servlet.xml里添加如下部分内容:
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basename" value="messages"/> </bean>
这部分bean的定义相当于指定了用哪个配置文件来保存对应的显示内容。在这里对应的是messages.properties文件。
2. 为了支持显示的内容可以定制化,于是我们需要在classpath的路径下创建文件messages.properties:
addProduct.form.productId.label = New Product ID addProduct.form.name.label = Name addProduct.form.unitPrice.label = Unit Price addProduct.form.description.label = Description addProduct.form.manufacturer.label = Manufacturer addProduct.form.category.label = Category addProduct.form.unitsInStock.label = Units in stock addProduct.form.condition.label = Product condition addProduct.form.productImage.label = Product image
3. 还有一个需要修改的地方就是要显示内容的jsp页面,修改后的页面如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"> <title>Products</title> </head> <body> <section> <div class="jumbotron"> <div class="container"> <h1>Products</h1> <p>Add products</p> </div> </div> </section> <section class="container"> <form:form modelAttribute="newProduct" class="form-horizontal"> <fieldset> <legend>Add new product</legend> <div class="form-group"> <label class="control-label col-lg-2 col-lg-2" for="productId"><spring:message code="addProduct.form.productId.label"/></label> <div class="col-lg-10"> <form:input id="productId" path="productId" type="text" class="form:input-large"/> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="name"><spring:message code="addProduct.form.name.label"/></label> <div class="col-lg-10"> <form:input id="name" path="name" type="text" class="form:input-large"/> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="unitPrice"><spring:message code="addProduct.form.unitPrice.label"/></label> <div class="col-lg-10"> <div class="form:input-prepend"> <form:input id="unitPrice" path="unitPrice" type="text" class="form:input-large"/> </div> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="description"><spring:message code="addProduct.form.description.label"/></label> <div class="col-lg-10"> <form:textarea id="description" path="description" rows = "2"/> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="manufacturer"><spring:message code="addProduct.form.manufacturer.label"/></label> <div class="col-lg-10"> <form:input id="manufacturer" path="manufacturer" type="text" class="form:input-large"/> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="category"><spring:message code="addProduct.form.category.label"/></label> <div class="col-lg-10"> <form:input id="category" path="category" type="text" class="form:input-large"/> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="unitsInStock"><spring:message code="addProduct.form.unitsInStock.label"/></label> <div class="col-lg-10"> <form:input id="unitsInStock" path="unitsInStock" type="text" class="form:input-large"/> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="condition"><spring:message code="addProduct.form.condition.label"/></label> <div class="col-lg-10"> <form:radiobutton path="condition" value="New" />New <form:radiobutton path="condition" value="Old" />Old <form:radiobutton path="condition" value="Refurbished" />Refurbished </div> </div> <div class="form-group"> <div class="col-lg-offset-2 col-lg-10"> <input type="submit" id="btnAdd" class="btn btn-primary" value ="Add"/> </div> </div> </fieldset> </form:form> </section> </body> </html>
里面的内容稍微有点多,重点需要关注的就是原来显示纯页面文字内容的地方被一种如下形式的内容给替代了:<spring:message code="xxx">。这里code=所显示的内容正对应了messages.properties文件里属性定义key那部分。
这种将显示内容分离出来的方法有几个好处,以后维护内容的时候比较方便,只需要修改配置文件就可以了。另外,以后如果支持多语言,也就是国际化也很方便。在后续的文章里我们还会对国际化做详细的讨论。
这个时候,如果我们启动页面将看到如下的内容:
表单数据验证
在前面我提到过,完成了那几步之后,算是表单设置基本完成了。但是离真正完成还是有一段距离。一个需要做的重要事情就是,表单数据验证。这也是一个很复杂的部分。因为表单数据验证针对的是提交数据到服务器端。对于外部提交的数据,我们需要验证它的合法性,比如提交的数据是否为空,是否有额外提交一些服务器不需要的,提交的内容是否为我们要求的合法值等等。
总的来说,对于表单数据提交验证可以分为两种,一种是字段过滤,一种是字段验证。spring里提供了多种验证的手段,它们可以用如下图来概括:
针对上述的各种验证方式,我们逐一来讨论。
字段过滤
在前面的form inbound, outbound部分我们可以看到,如果我们设置了相关的属性,spring mvc会自动将定义的pojo对象和表单字段映射起来。这确实带来了很多的便利,不用开发者自己手动将它们来回的映射,同时也有一个问题。因为它默认将所有表单的字段都映射过来,有可能一些恶意的攻击者会提交一些我们不需要的值设置到某些对象属性上。这个时候就需要一个办法来过滤表单交互需要的字段。在spring mvc里有一个@InitBinder修饰的方法,通过它来修饰一个包含参数为WebDataBdiner的方法。比如如下的方法:
@InitBinder public void initialiseBinder(WebDataBinder binder) { binder.setAllowedFields("productId","name","unitPrice","description", "manufacturer","category","unitsInStock", "condition"); }
在上面的代码里相当于设置了一个白名单的方式,所有在表单里为以上名字的字段将被允许访问,其他的则不行。WebDataBinder也提供了setDisallowedFields方法,类似于黑名单的方式。在实际情况中,因为需要屏蔽的字段理论上有无限多个,而需要绑定的字段是有限个的,所以用白名单的方式会比较常见。
字段验证
前面的字段过滤是保证提交的表单里不包含有不需要的字段,以防止有人恶意的注入值。但是仅仅是字段的过滤还是远远不够的。如果在允许绑定的字段里没有任何限制的话,这样如果在允许的字段里提供非法的赋值,也会导致程序的被破坏。于是也需要一些机制来做字段验证。在日常的应用里有几种方式来验证字段,它们不是互相排斥的。相反,它们各有所长,适合结合起来处理应用的逻辑。
JSR 303
最常用的一种字段验证方式就是JSR 303,它是一个定义的规范,针对它的实现有若干个,一个比较典型的就是hibernate validator。在示例里为了能够支持该规范,需要引入对hibernate validator的支持。因此需要在maven pom.xml文件里添加如下内容:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.2.1.Final</version> </dependency>
下一步就是需要在我们定义的domain model里添加验证的annotation。这种规范本身也是通过一种annotation来验证字段合法性。它的思路和aop很像。我们需要将domian对象里的一些字段添加一些限制,比如说如下代码:
@NotNull @Length(min = 1, max = 40) public String getProductId() { return productId; } public void setProductId(String productId) { this.productId = productId; } @NotNull @Size(min = 4, max = 50, message = "{Size.Product.name.validation}") public String getName() { return name; } public void setName(String name) { this.name = name; } @NotNull(message = "{NotNull.Product.unitPrice.validation}") @Min(value = 0, message = "{Min.Product.unitPrice.validation}") @Digits(integer = 8, fraction = 2, message = "{Digits.Product.unitPrice.validation}") public BigDecimal getUnitPrice() { return unitPrice; }
这里在属性的get方法上添加了一些annotation,比如说@NotNull,表示该字段不能为空。而里面的message属性则表示如果该项验证失败了,显示的验证信息内容是什么。这样类推,像@Length则表示限制该字段的长度。具体的各种验证annotation可以参考相关的官方文档。
在添加完这些验证annotation之后需要的就是修改对应的controller方法,我们需要在方法里添加一个处理post请求的方法以及对应验证的字段。具体的实现如下:
@RequestMapping(value = "/add", method = RequestMethod.POST) public String processAddNewProductForm(@ModelAttribute("newProduct") @Valid Product productToBeAdded, BindingResult result) { if(result.hasErrors()) { return "addProduct"; } return "redirect:/products"; }
上述的方法里有两个地方的改变。一个是Product参数前面增加了一个@Valid的annotation,通过这个方式,所有前面model里定义的字段验证都会对应到Product这个参数上。另外一个就是后面的参数BindingResult。它是用于保存和判断表单字段绑定时的错误。在前面的代码里可以通过result.hasErrors()来判断是否存在错误,并以此来判断页面跳转逻辑。
在前面字段验证的时候,我们设定了message的属性,这些用于展示具体验证错误信息的内容需要被定义到某个地方,而且适当的时候还可以被国际化。在这个示例里,需要将下面的内容加到messages.properties文件里:
Pattern.Product.productId.validation = Invalid product ID. It should start with character P followed by number. Size.Product.name.validation = Invalid product name. It should be minimum 4 characters to maximum 50 characters long. Min.Product.unitPrice.validation = Unit price is Invalid. It cannot have negative values. Digits.Product.unitPrice.validation = Unit price is Invalid.It can have maximum of 2 digit fraction and 8 digit integer. NotNull.Product.unitPrice.validation = Unit price is Invalid. It cannot be empty.
从服务器端验证逻辑来说,上面的修改已经差不多了。不过从交互的角度来说,这还是不够的。因为如果验证提交的字段出错了,需要有错误信息显示和提示。那么这部分内容也必须添加到页面上,于是我们需要在页面中添加对应的error信息。对应修改后的页面如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"> <title>Products</title> </head> <body> <section> <div class="jumbotron"> <div class="container"> <h1>Products</h1> <p>Add products</p> </div> </div> </section> <section class="container"> <form:form modelAttribute="newProduct" class="form-horizontal"> <fieldset> <legend>Add new product</legend> <form:errors path="*" cssClass="alert alert-danger" element="div"/> <div class="form-group"> <label class="control-label col-lg-2 col-lg-2" for="productId"><spring:message code="addProduct.form.productId.label"/></label> <div class="col-lg-10"> <form:input id="productId" path="productId" type="text" class="form:input-large"/> <form:errors path="productId" cssClass="text-danger"/> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="name"><spring:message code="addProduct.form.name.label"/></label> <div class="col-lg-10"> <form:input id="name" path="name" type="text" class="form:input-large"/> <form:errors path="name" cssClass="text-danger"/> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="unitPrice"><spring:message code="addProduct.form.unitPrice.label"/></label> <div class="col-lg-10"> <div class="form:input-prepend"> <form:input id="unitPrice" path="unitPrice" type="text" class="form:input-large"/> <form:errors path="unitPrice" cssClass="text-danger"/> </div> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="description"><spring:message code="addProduct.form.description.label"/></label> <div class="col-lg-10"> <form:textarea id="description" path="description" rows = "2"/> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="manufacturer"><spring:message code="addProduct.form.manufacturer.label"/></label> <div class="col-lg-10"> <form:input id="manufacturer" path="manufacturer" type="text" class="form:input-large"/> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="category"><spring:message code="addProduct.form.category.label"/></label> <div class="col-lg-10"> <form:input id="category" path="category" type="text" class="form:input-large"/> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="unitsInStock"><spring:message code="addProduct.form.unitsInStock.label"/></label> <div class="col-lg-10"> <form:input id="unitsInStock" path="unitsInStock" type="text" class="form:input-large"/> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="condition"><spring:message code="addProduct.form.condition.label"/></label> <div class="col-lg-10"> <form:radiobutton path="condition" value="New" />New <form:radiobutton path="condition" value="Old" />Old <form:radiobutton path="condition" value="Refurbished" />Refurbished </div> </div> <div class="form-group"> <div class="col-lg-offset-2 col-lg-10"> <input type="submit" id="btnAdd" class="btn btn-primary" value ="Add"/> </div> </div> </fieldset> </form:form> </section> </body> </html>
这部分的内容比较长,重点需要关注的就是如下几个字段: <form:errors path="*" cssClass="alert alert-danger" element="div"/>
<form:errors path="productId" cssClass="text-danger"/> 。 在spring mvc里,如果任何一个字段在验证的时候失败了,它对应的错误信息将会被显示到<form:errors>的内容里。比如前面如果是productId出错了,那么<form:errors path="productId" cssClass="text-danger"/> 这个部分将会显示这个字段的出错信息。所以在页面上将该部分放在对应的显示部位就可以。而<form:errors path="*" cssClass="alert alert-danger" element="div"/>则是显示所有的错误信息,它相当于一个错误信息的汇总。在前面的页面里,无非就是在表单的头部显示所有错误信息,而每个对应的字段显示具体字段的错误信息。
当然,如果需要让页面能够正确的显示这些信息,还需要在配置文件里做一些对应的修改。我们需要在dispatcher-servlet.xml文件里添加如下部分的内容:
<mvc:annotation-driven validator="validator"/> <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basename" value="messages"/> </bean> <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"> <property name="validationMessageSource" ref="messageSource"/> </bean>
其中id为validator的bean为LocalValidatorFactoryBean,它在启动的时候会初始化hibernate validator,同时它引用的消息显示内容是前面定义的messages.properties的消息内容文件。
这个时候,如果我们启动服务器,进入到如下页面:http://localhost:8080/BlogExample/products/add
假设我们不输入任何表单数据而点击提交按钮,这个时候页面将显示如下的内容:
自定义validator
除了上面我们使用的JSR303规范所实现的验证,还有一种验证手法是自定义的validator。它相对来说在某些情况下更加灵活。比如说在前面的Product对象模型中,我们希望在输入的时候判断是否已经存在现有的productId。这样以保证输入不存在重复的productId。
首先创建一个ProductId的annotation interface:
package com.yunzero.validator; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; @Target( { METHOD, FIELD, ANNOTATION_TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = ProductIdValidator.class) @Documented public @interface ProductId { String message() default "{com.yunzero.validator.ProductId.message}"; Class<?>[] groups() default {}; public abstract Class<? extends Payload>[] payload() default {}; }
在上述的声明里定义了validatedBy = ProductIdValidator.class,表示具体的验证逻辑用ProductIdValidator来实现。ProductIdValidator的实现如下:
package com.yunzero.validator; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import org.springframework.beans.factory.annotation.Autowired; import com.yunzero.domain.Product; import com.yunzero.exception.ProductNotFoundException; import com.yunzero.service.ProductService; public class ProductIdValidator implements ConstraintValidator<ProductId, String> { @Autowired private ProductService productService; @Override public void initialize(ProductId constraintAnnotation) { } @Override public boolean isValid(String value, ConstraintValidatorContext context) { Product product; try { product = productService.getProductById(value); } catch (ProductNotFoundException e) { return true; } if(product != null) { return false; } return true; } }
ProductIdValidator实现ConstraintValidator,具体验证该方法是否合法的实现在isValid方法里。我们这里引用了ProductService这个服务,这部分是后面的一个对象访问的具体实现,这里可以先忽略。
在完成上面的步骤后,在messages.properties中加入如下内容:
com.yunzero.validator.ProductId.message = A product already exists with this product id.
前面,我们已经定义好了ProductId这个annotation,然后我们将它应用到domain model里的ProductId属性上:
@Pattern(regexp="P[0-9]+", message="{Pattern.Product.productId.validation}") @ProductId private String productId;
这个时候,如果我们启动应用服务器,输入必须的信息,但是刻意输入一个已经存在的productId信息。页面将显示如下内容:
spring validation
除了上述的validator,还有一种在spring中比较传统的validation机制。虽然和JSR 303比起来,它要更加复杂一些,但是它更加灵活和具有可扩展性一些。比如在某些情况下我们需要检查若干个字段组合起来的合法性。它的作用就会更加明显了。我们来看一个示例。
假设我们想要限制价格大于100的Product,它的数量不能超过99。我们可以定义一个如下的实现:
package com.yunzero.validator; import java.math.BigDecimal; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import org.springframework.validation.Validator; import com.yunzero.domain.Product; @Component public class UnitsInStockValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return Product.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { Product product = (Product) target; if(product.getUnitPrice() != null && new BigDecimal(10000).compareTo(product.getUnitPrice()) <= 0 && product.getUnitsInStock() > 99) { errors.rejectValue("unitsInStock", "com.yunzero.validator.UnitsInStockValidator.message"); } } }
在所有基于spring的validator里都必须实现接口org.springframework.validation.Validator。support方法用来定义该validator适用于哪个类。而validate方法则用于具体逻辑的检查。在这里,通过具体检查unitsInStock和unitPrice来看是否有存在错误的情况。如果有,则通过errors.rejectValue方法,设置对应的错误信息和字段。
为了能够显示这个错误信息,我们需要将这个错误信息显示内容添加到messages.properties文件里:
com.yunzero.validator.UnitsInStockValidator.message = You cannot add more than 99 units if the unit price is greater than 10000.
同时,需要修改ProductController里面的代码,在里面添加对UnitsInStockValidator的引用:
@Autowired private UnitsInStockValidator unitsInStockValidator;
同时将initialiseBinder方法修改成如下:
@InitBinder public void initialiseBinder(WebDataBinder binder) { binder.setAllowedFields("productId","name","unitPrice","description", "manufacturer","category","unitsInStock", "condition"); binder.setValidator(unitsInStockValidator); }
如果这个时候,我们启动程序尝试输入非法的unitsInStock和unitPrice组合,将看到如下的错误:
但是,如果我们这个时候去尝试输入其他的错误形式,我们会发现页面出错了。这是为什么呢?因为在WebDataBinder里绑定了unitsInStockValidator,spring mvc将会自动忽略前面JSR 303的annotation。所以,这个时候,这将成为一个问题。在下一节将讨论怎么解决这个问题。
对于spring validation来说,它的用法其实就是定义一个Validator的实现,然后在initialiseBinder里将该validator给设置上。
组合spring validation和bean validation
对于上面的问题有没有办法解决呢?当然是有的。既然spring mvc的validation很灵活,我们可以利用一些办法来解决。首先,我们定义一个spring validator:
package com.yunzero.validator; import java.util.HashSet; import java.util.Set; import javax.validation.ConstraintViolation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.Errors; import org.springframework.validation.Validator; import com.yunzero.domain.Product; public class ProductValidator implements Validator { @Autowired private javax.validation.Validator beanValidator; private Set<Validator> springValidators; public ProductValidator() { springValidators = new HashSet<Validator>(); } public void setSpringValidators(Set<Validator> springValidators) { this.springValidators = springValidators; } @Override public boolean supports(Class<?> clazz) { return Product.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { Set<ConstraintViolation<Object>> constraintViolations = beanValidator.validate(target); for(ConstraintViolation<Object> constraintViolation : constraintViolations) { String propertyPath = constraintViolation.getPropertyPath().toString(); String message = constraintViolation.getMessage(); errors.rejectValue(propertyPath, "", message); } for(Validator validator : springValidators) { validator.validate(target, errors); } } }
在这里,我们重点是定义了一个bean validator。而这里的beanValidator是基于JSR303规范的。在前面的validate方法里面首先通过beanValidator.validate方法将所有bean validation的验证结果放到一个set里。然后在一个循环里将所有的错误信息都处理了。在后面的一个循环里,将所有spring validator都统一进行验证处理。
这样,修改之后我们就可以统一使用一个这样的ProductValidator。为了能够使用这个ProductValidator需要修改dispatcher-servlet.xml,在里面添加如下的部分:
<bean id="productValidator" class="com.yunzero.validator.ProductValidator"> <property name="springValidators"> <set> <ref bean="unitsInStockValidator"/> </set> </property> </bean> <bean id="unitsInStockValidator" class="com.yunzero.validator.UnitsInStockValidator"/>
同时,原来ProductController里面对unitsInstockValidator的引用替换成ProductValidator。这样,我们就相当于用一个spring validator包括了bean validator和spring validator。
保存表单数据
从使用表单来保存数据的角度来说,如果前面的参数绑定和验证都完成后,剩下的就差不多是要保存数据了。保存表单数据的过程其实和我们传统的操作数据访问层差不多。在这个示例里,我们采用JPA的方式,具体运用hibernate作为JPA的实现来做ORM。 关于spring, hibernate, jpa相关的内容可以参考前面的一篇文章。这里也列举出一些相关的配置文件信息。
首先前面的文章里也提到,尽量将web, servlet相关的内容放到dispatcher-servlet.xml中,而对于一些在整个应用的appliationContext中通用的东西最好放到applicationContext.xml文件中来定义。于是,从最初的定义来说,我们会有如下几个文件:
web.xml:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:/spring/dispatcher-servlet.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:/spring/applicationContext.xml</param-value> </context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> </web-app>
这部分的内容很简单,就是定义了web context和application context。
dispatcher-servlet.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd"> <mvc:annotation-driven validator="validator"/> <context:component-scan base-package="com.yunzero.controller"/> <context:component-scan base-package="com.yunzero.validator"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean> <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basename" value="messages"/> </bean> <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"> <property name="validationMessageSource" ref="messageSource"/> </bean> <bean id="productValidator" class="com.yunzero.validator.ProductValidator"> <property name="springValidators"> <set> <ref bean="unitsInStockValidator"/> </set> </property> </bean> <bean id="unitsInStockValidator" class="com.yunzero.validator.UnitsInStockValidator"/> </beans>
这里的配置主要就是显示页面的viewResolver,还有就是externalize显示内容的配置以及我们后面配置的validator。
至于applicationContext的内容,由于在引用中考虑到具体的应用需要,我们可以将该文件作为一个总的配置文件的引用,比如应用中要配置数据库、消息队列等东西,分别配置到不同的文件中,然后将它们给引用到applicationContext.xml中。
所以这里applicationContext.xml的内容如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"> <import resource="classpath:/spring/jdbc.xml"/> <context:component-scan base-package="com.yunzero.service" /> </beans>
而这里具体引用的jdbc.xml文件则是对数据库引用和配置的详细设置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:c="http://www.springframework.org/schema/c" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd"> <context:property-placeholder location="classpath:/spring/datasource.properties" /> <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close" p:driverClassName="${dataSource.driverClassName}" p:url="${dataSource.url}" p:username="${dataSource.username}" p:password="${dataSource.password}" /> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" p:dataSource-ref="dataSource" p:packagesToScan="com.yunzero.domain"> <property name="persistenceProvider"> <bean class="org.hibernate.jpa.HibernatePersistenceProvider" /> </property> <property name="jpaProperties"> <props> <prop key="hibernate.hbm2ddl.auto">update</prop> <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop> <prop key="hibernate.show_sql">false</prop> </props> </property> </bean> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" p:entityManagerFactory-ref="entityManagerFactory" /> <tx:annotation-driven /> <context:component-scan base-package="com.yunzero.repository" /> </beans>
在代码里详细的关于service层和repository层的实现可以参考后面的附件。在前面的一些关于spring, hibernate, jpa的实现里也有讨论。
总结
这算是一篇比较长的文章了。在spring mvc里,仅仅讨论一个表单的创建和提交就可以牵扯出这么多的东西来。如果从头到尾的理一遍的话,我们需要从一开始考虑对需要访问的数据建立domain模型,然后创建表单显示页面并绑定模型和页面字段。然后需要考虑对映射字段的过滤以及数据合法性的验证。尤其是数据合法性的验证,它有若干种方式,最常用的是JSR 303的实现,有时候有特殊的需求情况下,我们还需要创建自定义的validator或者运用spring validator。怎么样将这些validator结合起来也是一个很重要的问题。
另外,从用户交互的角度考虑,如果字段验证错误了,需要显示错误信息提示,该怎么显示和处理并在页面中展示也是一个很费劲的工作。在上述工作结束后就要考虑领域对象的持久化,各种 ORM框架的配置和运用又是一个让人操心的地方。总之,form表单并不简单。
参考材料
相关推荐
在Spring MVC中,文件上传是常见的功能之一,用于接收用户通过表单提交的文件数据。本文将深入探讨Spring MVC中的文件上传方法,并基于提供的“spring学习:spring mvc上传文件方法分析”标题进行详细的解析。 首先...
标题中的“spring学习:spring mvc在redirect请求中传递数据”指的是在Spring MVC框架中,如何在重定向(Redirect)请求过程中传递参数。这通常涉及到HTTP响应的状态码、重定向URL以及模型数据的处理。在Spring MVC...
6. **数据绑定与验证**:Spring MVC提供了数据绑定和验证功能,允许将表单数据自动绑定到Java对象,并进行校验,简化了业务逻辑处理。 7. **视图解析**:Spring MVC 4.0支持多种视图技术,如JSP、FreeMarker、...
### Spring MVC 学习指南详解 #### 一、Spring MVC 概述 Spring MVC 是 Spring 框架的一部分,主要用于构建基于 Model-View-Controller (MVC) 设计模式的 Web 应用程序。它提供了丰富的功能来简化 Web 开发过程,...
本篇文章将深入探讨Spring MVC中的表单标签库,以及如何在实际项目中使用它们来创建动态、交互式的表单。 1. **表单标签库介绍** Spring MVC的表单标签库是JSP的一部分,它可以生成HTML表单元素,并且能够自动绑定...
Spring MVC还支持数据绑定,自动将请求参数绑定到Java对象,这极大地简化了表单提交的处理。对于验证,Spring MVC提供了BindingResult和Validator接口,用于校验模型数据的正确性。 另外,Spring MVC与Spring框架的...
3. **第3部分:在Spring 3.0 MVC中进行表单处理** - 探讨如何使用Spring MVC处理表单提交,包括数据绑定和验证。 4. **第4部分:Spring 3 MVC的Tiles支持与Eclipse中的插件教程** - Tiles框架是一种用于管理页面布局...
在本实例中,我们主要探讨了如何利用Layui库进行图片上传,同时结合表单提交和Spring MVC框架实现后端处理。Layui是一个轻量级的前端组件库,提供了丰富的UI组件,包括上传功能。以下是具体的知识点: 1. **Layui...
3. **Spring MVC**:学习Spring MVC的架构,包括DispatcherServlet、Controller、Model、View、ModelAndView等概念,以及请求处理流程。 4. **Struts集成**:如果使用了spring-webmvc-struts.jar,需要了解如何在...
10. **Conversion and Validation**: Spring MVC提供了数据转换和验证功能,如使用`@RequestParam`、`@PathVariable`注解自动转换参数,以及使用`@Valid`和`BindingResult`进行表单验证。 11. **Tiles or Layouts**...
9. **Form Handling and Data Binding**: Spring MVC提供强大的表单处理和数据绑定功能。它可以自动将HTTP请求参数绑定到Java对象的属性上,反之亦然。 10. **Validation**: Spring MVC集成Hibernate Validator,...
### Spring Web MVC 框架核心知识点解析 #### 一、Spring Web MVC 概述 - **Spring Web MVC** 是...通过上述内容的学习,可以全面掌握Spring Web MVC框架的核心开发知识,为构建高质量的Web应用程序打下坚实的基础。
【第四课:轻松实现MVC框架1】 在本课程中,我们将探讨如何构建...通过理解框架的需求、设计和实现,开发者不仅能学会如何创建自己的MVC框架,还能深化对Spring等现有框架的理解,从而在实际项目中更好地应用和优化。
《网上书城 Spring-MVC》项目是基于Spring-MVC框架构建的一个典型电子商务平台,它展示了如何使用Spring-MVC来实现高效、...开发者可以通过学习和实践这个项目,深入理解Spring-MVC的机制和最佳实践,提升Web开发能力。
- EasyUI将表单数据提交到Spring MVC的控制器。 - 控制器通过Spring的依赖注入获取Hibernate的DAO对象,调用DAO方法将数据保存到数据库。 - Hibernate自动处理与数据库的交互,包括SQL语句的生成和执行。 - 保存...