NO END FOR LEARNING

Writing blog if you feel tired | 学海无涯 苦写博客

Spring Validation 深入浅出

| Comments

在Web开发中,一个重要的特性,那就是对表单提交的内容进行合法性校验。

校验的方式根据方式的不同,分为两类:前端校验和后端校验。

前端校验的优点是,不用与服务器交互,由JavaScript直接对页面元素中的内容进行校验,速度更快。但这种方式的安全性不高,对于了解web开发的人可以通过修改页面内容的方式绕过前端验证。

后端验证需要表单提交,一般情况下会刷新页面,对于用户体验不是太理想。

所以,一般前端验证会和后端验证会配合在一起。

今天我们一起来学习一下,在Spring MVC中,是如何进行后端的校验,它又是如何通过Spring提供的Form标签,将错误警告信息传递到前端来显示。

基本了解

我们先来简单的写一个demo,这种HelloWorld式例子最容易让人有信心去理解和学习,要实现它,你只需要了解下面五样或者四样东西。

一个重要的包:

Spring框架中有一个非常重要的包,org.springframework.validation,它提供了两个重要的特性:数据绑定和校验。

数据绑定允许用户的输入动态的绑定到应用的领域模型,实现字符串类型和其他类型的转换。

在Spring MVC,数据绑定机制允许你使用任何的命令对象或者表单对象-你不需要实现一个框架指定的接口或者类。

一个重要接口:

Errors:用于存储和暴露于某个对象相关的数据绑定和错误校验信息。

一个重要的类:

BeanPropertyBindingResult:Spring中,Errors和BindingResult接口的默认实现类,主要给JavaBean对象的绑定错误进行注册和评估。

一个重要的Form标签:

<form:errors>:这个标签会将对象field的错误渲染成一个HTML的span标签。它能够访问由Controller或者与Controller相关的的Validator创建出来的errors对象。(关于Validator,后面会介绍)

两个重要的函数:

那么,要在Controller中实现一个最简单的校验,并将校验结果显示在前端就非常简单了,你只需要在了解Errors接口的两个函数:

reject():使用给定的错误描述信息,给这个目标对象注册一个全局的错误信息

rejectValue():使用给定的错误描述信息,注册一个field的错误到当前对象的某个指定field上(或者该对象的某个成员变量的field上,比如customer.name.firstName)。这个field名字也可以是null或者空串来说明是指定该对象而不是该对象的一个field(不过这样容易导致一些错误)。

看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?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:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/mvc
                    http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
                    http://www.springframework.org/schema/beans
                    http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
                    http://www.springframework.org/schema/context
                    http://www.springframework.org/schema/context/spring-context-3.1.xsd">

    <context:component-scan base-package="me.zeph.springview.demo"/>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
        <property name="prefix" value="/WEB-INF/view/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <!-- bind your messages.properties -->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="messages" />
    </bean>

    <mvc:annotation-driven/>

</beans>

这里唯一特殊的一点就是配置了一个ResourceBundle,它指向一个messages.properties文件,里面存放这校验错误码对应的信息。

1
2
NotEmpty.customer.name=Name is required
NotEmpty.customer.password=Password is required
1
2
3
4
5
6
7
8
9
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
package me.zeph.springview.demo.controller;

import me.zeph.springview.demo.domain.Customer;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.RequestMapping;

import static org.springframework.util.StringUtils.isEmpty;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;

@Controller
public class ErrorController {
  public static final String ERROR = "error";
  public static final String ERROR_SUCCESS = "error-success";

  @RequestMapping(value = "error", method = GET)
  public String view(Customer customer) {
      return ERROR;
  }

  @RequestMapping(value = "error", method = POST)
  public String commit(Customer customer, Errors errors) {
      if (isEmpty(customer.getName())) {
          errors.rejectValue("name", "NotEmpty.customer.name", "name is empty");
      }
      if (isEmpty(customer.getPassword())) {
          errors.rejectValue("password", "NotEmpty.customer.password", "password is empty");
      }
      //Just for demonstrating reject the object itself
      errors.reject("customer.invalid", "Customer is invalid");
      return errors.hasErrors() ? ERROR : ERROR_SUCCESS;
  }
}

在Controller里面,通过参数让Spring帮我们注入Errors对象(注意Errors作为参数的位置一定要在领域对象(表单对象)的后面,猜测原因是Spring需要知道Errors对象是与哪个对象绑定的)。这里自己写校验逻辑,然后调用errors的rejectValue方法,将错误信息保存起来。如果你在这里打断点,你会看到,Errors真正的类型是BeanPropertyBindingResult。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Spring MVC View Demo</title>
</head>
<body>
<h1>Spring MVC View Demo</h1>
<form:form commandName="customer" method="post" action="error">
  <form:errors path="*"/>
    <div>
        <label for="name">name :</label>
        <form:input path="name" id="name"/>

    </div>
    <div>
        <label for="password">password :</label>
        <form:password path="password" id="password"/>
    </div>
    <input type="submit" value="submit">
</form:form>
</body>
</html>

path=“*” - 显示全部的内容
path=“name” - 显示与name这个field相关的错误信息
如果path不填 - 只显示与表单对象本身相关的错误信息

到这里,你就可以去试一试,看能不能正常显示出你想要的信息。但是是不是到这里就为止了呢?远远没有。这只是Spring Validation最根本和最简单的实现,这种方式并不被Spring推荐,我们继续。

JSR-303

在Spring 3.0之后,Spring Validation的功能增强了,支持结合使用JSR-303标准进行校验。

先讲讲什么是JSR-303,它是Java标准中Bean Validation的1.0版本。

它可以让你通过注解的方式来对对象模型添加限制,它提供的开箱即用的常用限制注解,同时也允许你写自己的限制规则。

不过就像之前学习的,它只是标准,具体实现由第三方提供,默认的参考实现是Hibernate Validator。

结合JSR-303,对表单的对象的校验就是成为了简单的给领域对象添加限定规则的注解。

来看看怎么写

首先给领域对象的field添加你需要的注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package me.zeph.springview.demo.domain;


import org.hibernate.validator.constraints.NotEmpty;

public class User {

  @NotEmpty
  private String name;
  @NotEmpty
  private String password;

  public String getName() {
      return name;
  }

  public void setName(String name) {
      this.name = name;
  }

  public String getPassword() {
      return password;
  }

  public void setPassword(String password) {
      this.password = password;
  }
}

接下来,Controller就简单了,没有写任何校验逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package me.zeph.springview.demo.controller;

import me.zeph.springview.demo.domain.User;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.validation.Valid;

import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;

@Controller
public class ValidController {

  public static final String VALID = "valid";
  public static final String VALID_SUCCESS = "valid-success";

  @RequestMapping(value = "valid", method = GET)
  public String view(User user) {
      return VALID;
  }

  @RequestMapping(value = "valid", method = POST)
  public String commit(@Valid User user, Errors errors) {
      return errors.hasErrors() ? VALID : VALID_SUCCESS;
  }
}

看下面的错误码和消息的写法,你应该知道JSR-303生成错误码的规则。

1
2
NotEmpty.user.name=Name is required
NotEmpty.user.password=Password is required

如果你觉得它提供的注解不够你用,你还可以写自己的注解,具体的内容就不在这里谈了,这是JSR-303的内容。如果你想了解,可以参考Hibernate Validation的指南: http://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#validator-customconstraints

还没讲完呢,继续深入。

深入理解

1
public String commit(@Valid User user, Errors errors)

看到没,在我们的领域对象User上有一个注解@Valid,它是JSR-303标准API提供的。

在Spring3.0之后,Spring MVC提供了一种能力可以让Controller输入参数进行自动的校验,之前后需要我们自己去触发校验逻辑。方法就是在输入参数加一个@Valid注解。

关于Spring Validation与JSR-303的结合使用,暂时到这里,我们再来看点别的。如果你也在学习Spring的Validation,那么你肯定会看到Spring的文档的第六章( http://docs.spring.io/spring/docs/current/spring-framework-reference/html/validation.html )。

它没有像我这样讲解Errors对象,讲解form怎么显示错误信息,而是介绍了一个Validator接口。那它是什么呢?

如果你有看它的API的解释,它会告诉你,它只是一个单纯的接口,期待将校验逻辑与web层,数据访问层解耦,它提供两个方法supports和validate。

根据它在指南中给的信息,我写出来下面的这段代码。

1
2
3
4
5
6
7
8
9
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
package me.zeph.springview.demo.controller;

import me.zeph.springview.demo.domain.Account;
import me.zeph.springview.demo.validator.AccountValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.validation.Valid;

import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;

@Controller
public class ValidatorController {

  @Autowired
  private AccountValidator accountValidator;

  public static final String VALIDATOR = "validator";
  public static final String VALIDATOR_SUCCESS = "validator-success";

  @RequestMapping(value = "validator", method = GET)
  public String view(Account account) {
      return VALIDATOR;
  }

  @RequestMapping(value = "validator", method = POST)
  public String commit(Account account, Errors errors) {
      accountValidator.validate(account, errors);
      return errors.hasErrors() ? VALIDATOR : VALIDATOR_SUCCESS;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package me.zeph.springview.demo.validator;

import me.zeph.springview.demo.domain.Account;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

@Component
public class AccountValidator implements Validator {
  @Override
  public boolean supports(Class<?> clazz) {
      return Account.class.equals(clazz);
  }

  @Override
  public void validate(Object target, Errors errors) {
      ValidationUtils.rejectIfEmpty(errors, "name", "NotEmpty.account.name");
      ValidationUtils.rejectIfEmpty(errors, "password", "NotEmpty.account.password");
  }
}

和我写的第一个例子比较,是不是很简洁,将校验逻辑和Controller分离。

等等,你这只是分了一个Service层来专门做校验,你在Controller层还是手动触发了校验逻辑,哪里像Validator API中说的那么理想,完全解耦了。

那是因为我写错了,正确的写法应该是这样的。

1
2
3
4
5
6
7
8
9
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
package me.zeph.springview.demo.controller;

import me.zeph.springview.demo.domain.Account;
import me.zeph.springview.demo.validator.AccountValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.validation.Valid;

import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;

@Controller
public class ValidatorController {

  @Autowired
  private AccountValidator accountValidator;

  public static final String VALIDATOR = "validator";
  public static final String VALIDATOR_SUCCESS = "validator-success";

  @InitBinder
  protected void initBinder(WebDataBinder binder) {
      binder.setValidator(accountValidator);
  }

  @RequestMapping(value = "validator", method = GET)
  public String view(Account account) {
      return VALIDATOR;
  }

  @RequestMapping(value = "validator", method = POST)
  public String commit(@Valid Account account, Errors errors) {
      return errors.hasErrors() ? VALIDATOR : VALIDATOR_SUCCESS;
  }
}

上面的代码说明了两件事情:

1.我在Account的前面加了@Valid,导致自动触发校验过程
2.校验逻辑是由我写的Validator实现

你先不要去想@InitBinder是干什么,简单的说,它是Controller中的一个回调函数,Controller容器会在Controller的生命周期的某个阶段调用它。

WebDataBinder到是可以看看,它是web请求参数转换为JavaBean对象时,使用的特殊的数据绑定(DataBinder)对象。因为我们这里不涉及到数据绑定的内容,所以不细聊,但是你可以想象,该数据绑定的过程,必然在进入Controller的某个请求方法之前完成的(因为Spring必须在给我们注入领域对象啊)。

回头想一个问题

好了,如果这里你想清楚了,可以再回头想一个问题,前面使用JSR-303方式定义校验,怎么没有定义一个Validator呢?

原因在这个对象:LocalValidatorFactoryBean,它是Spring为支持JSR-303标准专门实现的一个类,为了同时支持Spring的Validation机制和JSR-303,它同时实现了Spring的Validator接口和JSR-303中Validator接口(这两个不一样哦)。

那么,如果我要使用JSR-303标准,需要显示的在容器中声明这个bean吗?需要也不需要。

在你的xxx-servlet.xml中定义的<mvc:annotation-driven/>标签会帮你声明。

所以如果你在initBinder方法那打个断点,你可以看到WebDataBinder中的Validator对象就是LocalValidatorFactoryBean。

现在是不是比较清楚了?

再讲一个重要的东西

还需要再讲一个东西:MessageCodesResolver和DefaultMessageCodesResolver

MessageCodesResolver是从Validation的ErrorCode映射到MessageCode的策略接口,被DataBinder使用,给ObjectErrors和FieldErrors构建MessageCode列表。

DefaultMessageCodesResolver是MessageCodesResolver提供的一个默认实现。

策略是:

针对object的错误,会去找两种错误信息码:

1.code + “.” + object name
2.code

针对field的错误,会去找三种错误信息码:

1.code + “.” + object name + “.” + field
2.code + “.” + field
3.code + “.” + field type
4.code

举例来说:

1
2
3
4
5
6
7
8
9
//field, error code, default message
errors.rejectValue("name", "NotEmpty", "name is empty");
errors.rejectValue("password", "NotEmpty", "password is empty");
errors.reject("Invalid", "Customer is invalid");

//messages.properties
NotEmpty=Name is required
NotEmpty.password=Password is required
Invalid=Customer is invalid

所以,如果你回过头去,看我写的第一个例子,实际上,它是使用的最后一个策略。

好了,关于Spring Validation的基本内容到此为止,应该比较清楚了,如果你要正确使用它,应该没有任何问题。

其实,我很早就想写这篇文章,一直拖到在项目上遇到问题,才促使我完成,实在惭愧。希望这篇文章对大家有所帮助

参考资料:

1.http://docs.spring.io/spring/docs/current/spring-framework-reference/html/validation.html

2.http://docs.spring.io/spring/docs/current/spring-framework-reference/html/view.html

3.http://docs.spring.io/spring/docs/2.5.x/api/org/springframework/validation/package-summary.html

4.http://docs.huihoo.com/spring/3.0.x/en-us/ch05s07.html

再次了解JDBC(下)- 事务

| Comments

事务就是将一条或者多条语句作为一个单元一起执行,要么全部执行,要么全都不执行。

如果你读过数据库的书,肯定知道数据库事务有四个基本特性:原子性,一致性,隔离性,持久性。

在JDBC中,事务的操作建立在Connection对象上,Connection对象提供了与事务相关的操作函数,例如:setAutoCommit(),commit(),rollback()等。

autocommit

JDBC Connection默认的情况是AutoCommit模式,即每一条SQL语句在执行完之后都会提交到数据库中。对于简单的应用是可以的,但是关闭自动提交模式,由自己管理实务是有必要的,提高执行效率,保证业务处理过程完整性,使用分布式事务。

事务可以让你控制对数据库的改变,它将一条或多条SQL语句作为一个逻辑单元,如果任何一条语句失败,则整个事务失败。

关闭自动提交模式的方法很简单,调用setAutoCommit()方法:

1
connection.setAutoCommit(false);

commit & rollback

一旦你完成了对数据库的改变操作,你需要执行commit()方法来提交改变,当执行过程中出现异常,你需要执行rollback()方法来回滚该事物。

1
connection.commit();
1
connection.rollback();

调用rollback方法会终结一个事务,并返回到数据被修改之前的值。如果你尝试在一个事务中去执行一条或多条语句,结果得到一个SQLException,调用rollback去结束一个事务,然后重新开始事务。这是唯一知道什么被提交,什么没有被提交的办法。因为捕获到SQLException会告诉你什么样的错误发生了,但是不会告诉你什么已经提交,什么没有。回滚是唯一的可靠办法。

完整性

除了将语句分组以一个单元统一执行,事务还帮助保证一张表中数据的完整性。事务会提供某种程度的保护,以防止两个用户同时访问数据时,造成的冲突。

DBMS会使用锁机制,来防止其他用户对已经被事务访问的数据进行访问。一旦加锁,它会强制保证不变直到事务被提交。锁机制的目的是防止用户读到脏数据,也就是读到一些还没有被永久保存的数据(访问一个被更新但是还没有被提交的值,被认为是访问到脏数据,因为这个值很有可能会被回滚到以前的结果,那么你读到的值就是无效的)。

锁是如何被设置的是由一个叫做事务隔离级别决定的。举例来说,如果事务隔离级别被设置为TRANSACTION_READ_COMMITTED,那么它就不会允许数据被访问,直到事务提交。换句话说,DBMS不允许读取脏数据的事件发生。

TRANSACTION_NONE JDBC 驱动不支持事务
TRANSACTION_READ_UNCOMMITTED 允许脏读,不可重复读和幻读。
TRANSACTION_READ_COMMITTED 禁止脏读,但允许不可重复读和幻读。
TRANSACTION_REPEATABLE_READ 禁止脏读和不可重复读,单运行幻读。
TRANSACTION_SERIALIZABLE 禁止脏读,不可重复读和幻读。

不可重复读的场景发生在事务A读取一行数据,事务B后续的改变了这一行,当事务A再次去读时,两次读取的事务就不一致了。

幻读的场景是事务A读取到满足一定条件的一部分数据,事务B后续插入或者更新了一行数据,但是同样满足该条件,此时A再去读取发现多了一行数据。

通常,你不需要对事务隔离级别做任何操作,只要使用默认的即可,但默认值取决于DBMS。例如,对于Java数据库,默认值是TRANSACTION_READ_COMMITTED。JDBC允许你获取和改变该级别,方法是getTransactionIsolation()和setTransactionIsolation()。

参考资料:

http://docs.oracle.com/javase/tutorial/jdbc/basics/transactions.html

http://www.tutorialspoint.com/jdbc/jdbc-transactions.htm

http://blog.csdn.net/chenyongsuda/article/details/5641412