`
frank-liu
  • 浏览: 1665429 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

spring学习: spring mvc表单提交

 
阅读更多

 简介

 

    在前一篇文章中,我简单讲述了一下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 in practice

spring in action

spring mvc beginner's guide

 

  • 大小: 30.4 KB
  • 大小: 35.1 KB
  • 大小: 36.5 KB
  • 大小: 38.5 KB
  • 大小: 62.6 KB
  • 大小: 45.4 KB
  • 大小: 43.6 KB
分享到:
评论
1 楼 15381049808 2016-11-08  
enctype="multipart/form-data"
这句话在文章中有误,虽然在后面去掉了

相关推荐

    大优惠 Spring MVC学习指南(第2版)2017.pdf

    全书共计12章,分别从Spring框架、模型2和MVC模式、Spring MVC介绍、控制器、数据绑定和表单标签库、传唤器和格式化、验证器、表达式语言、JSTL、国际化、上传文件、下载文件多个角度介绍了Spring MVC。除此之外,...

    [免费]Spring MVC学习指南(高清)

    全书共计12章,分别从Spring框架、模型2和MVC模式、Spring MVC介绍、控制器、数据绑定和表单标签库、传唤器和格式化、验证器、表达式语言、JSTL、国际化、上传文件、下载文件多个角度介绍了Spring MVC。除此之外,...

    使用Spring MVC表单标签_IT168文库.doc

    在低版本的Spring中,你必须通过JSTL或...不过这一情况已经一去不复返了,从Spring 2.0开始,Spring MVC开始全面支持表单标签,通过Spring MVC表单标签,我们可以很容易地将控制器相关的表单对象绑定到HTML表单元素中。

    layui 图片上传+表单提交+ Spring MVC的实例

    今天小编就为大家分享一篇layui 图片上传+表单提交+ Spring MVC的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

    Spring全家桶课程 基础篇:Spring MVC快速开发 的源码

    内容概要:该资源是Spring全家桶视频课程的第二部分Spring MVC的源码,其对于Spring的最常用的基本功能,如:表单、国际化、过滤器、拦截器、log等都使用案例进行了说明,并且在最后通过一个贯穿前、中、后台的案例...

    精通Spring MVC 4

    Spring MVC 属于 Spring Framework 的衍生产品,已经融合在 Spring Web Flow 里面。 Spring 框架提供了构建 Web 应用程序的全功能 MVC 模块。Spring MVC 4 是当前最新的版 本,在众多特性上有了进一步的提升。 本书...

    Spring.MVC-A.Tutorial-Spring.MVC学习指南

    全书共计12章,分别从Spring框架、模型2和MVC模式、Spring MVC介绍、控制器、数据绑定和表单标签库、传唤器和格式化、验证器、表达式语言、JSTL、国际化、上传文件、下载文件多个角度介绍了Spring MVC。除此之外,...

    spring-mvc-learn:spring-mvc学习代码源码-mvc source code

    spring-mvc学习代码源码 app03a 第一个Spring MVC应用 app04a 基于注解的控制器 app05a 数据绑定和表单标签库 在视图中帖子:&lt;%@ taglib prefix =“ form” uri =“ ”%&gt;,使用spring的表单 在控制器的动作添加...

    Spring MVC学习指南

    全书共计12章,分别从Spring框架、模型2和MVC模式、Spring MVC介绍、控制器、数据绑定和表单标签库、传唤器和格式化、验证器、表达式语言、JSTL、国际化、上传文件、下载文件多个角度介绍了Spring MVC。除此之外,...

    ssm(spring+spring mvc+mybatis)电子商城项目

    视图框架:Spring MVC 4.3.5 任务调度:Spring + Quartz 2.2.3 持久层框架:MyBatis 3.4.2 + Mybatis-plus 2.0.1 日志管理:SLF4J 1.7 + Log4j2 2.7 工具类:Apache Commons、Jackson 2.2、fastjson 1.2.20 前端技术...

    Spring MVC 3.0 单表单 同时提交 不同对象到后台

    自定义 RequestBean 注解 及 WebArgumentResolver 完成如下格式表单提交 最简单的 是 新定义 一个 过渡对象 Objects 其包含 user group.....等 然后用如上 格式提交 最终 到后台 进行各取所需

    Spring-MVC-CRUD表单提交和ajax:Spring MVC CRUD应用程序(Spring MVC 5.x,hibernate 5.x,Bootstrap 3.x,JQuery,MySql)

    Spring MVC CRUD表单提交和ajax 在这里,我提供了一个有关“ Spring MVC CRUD应用程序”的项目。 在这个项目中,我同时显示了CRUD操作的表单提交和Ajax。 让我们看看我们在这里得到了什么: 作者: Md Mamun Abdul...

    ssm(spring+spring mvc+mybatis)高仿小米电子商城项目实例

    ssm(spring+spring mvc+mybatis)高仿小米电子商城项目实例 开发环境:Eclipse ,JDK 1.8 ,Tomcat7 技术选型: 后端技术 核心框架:Spring Framework 4.3.5 视图框架:Spring MVC 4.3.5 任务调度:Spring + ...

    Spring MVC 3.0实战指南.ppt

    《Spring MVC 3.0实战指南》,参考《Spring 3.x企业应用开发实战》。 内容简介: 1、Spring MVC框架简介 2、HTTP请求地址映射 3、HTTP请求数据的绑定 4、数据转换、格式化、校验 5、数据模型控制 6、视图及解析器 7...

    Spring3MVC注解教程.ppt

    《Spring MVC 3.0实战指南》,参考《Spring 3.x企业应用开发实战》。 内容简介: 1、Spring MVC框架简介 2、HTTP请求地址映射 3、HTTP请求数据的绑定 4、数据转换、格式化、校验 5、数据模型控制 6、视图及...

    Spring.MVC-A.Tutorial-Spring.MVC学习指南.rar

    全书共计12章,分别从Spring框架、模型2和MVC模式、Spring MVC介绍、控制器、数据绑定和表单标签库、传唤器和格式化、验证器、表达式语言、JSTL、国际化、上传文件、下载文件多个角度介绍了Spring MVC。除此之外,...

    Spring MVC表单标签库

    使用SprignMVC标签处理表单实例

Global site tag (gtag.js) - Google Analytics