B/S 系统中对 http 请求数据的校验多数在客户端进行,这也是出于简单及用户体验性上考虑,但是在一些安全性要求高的系统中服务端校验是不可缺少的,实际上,几乎所有的系统,凡是涉及到数据校验,都需要在服务端进行二次校验。为什么要在服务端进行二次校验呢?这需要理解客户端校验和服务端校验各自的目的。
首先,我们需要加入校验需要的依赖:
org.hibernate
hibernate-validator
6.1.0.Final
接下来,在 SpringMVC 的配置文件中配置校验的 Bean:
配置时,提供一个 LocalValidatorFactoryBean 的实例,然后 Bean 的校验使用 HibernateValidator。
这样,配置就算完成了。
接下来,我们提供一个添加学生的页面:
在这里需要提交的数据中,假设学生编号不能为空,学生姓名长度不能超过 10 且不能为空,邮箱地址要合法,年龄不能超过 150。那么在定义实体类的时候,就可以加入这个判断条件了。
public class Student {
@NotNull
private Integer id;
@NotNull
@Size(min = 2,max = 10)
private String name;
@Email
private String email;
@Max(150)
private Integer age;
public String getEmail() {
return email;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", email='" + email + '\'' +
", age=" + age +
'}';
}
public void setEmail(String email) {
this.email = email;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
在这里:
@Controller
public class StudentController {
@RequestMapping("/addstudent")
@ResponseBody
public void addStudent(@Validated Student student, BindingResult result) {
if (result != null) {
//校验未通过,获取所有的异常信息并展示出来
List allErrors = result.getAllErrors();
for (ObjectError allError : allErrors) {
System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
}
}
}
}
在这里:
student.id.notnull=id 不能为空
student.name.notnull=name 不能为空
student.name.length=name 最小长度为 2 ,最大长度为 10
student.email.error=email 地址非法
student.age.error=年龄不能超过 150
接下来,在 SpringMVC 配置中,加载这个配置文件:
classpath:MyMessage
最后,在实体类上的注解中,加上校验出错时的信息:
public class Student {
@NotNull(message = "{student.id.notnull}")
private Integer id;
@NotNull(message = "{student.name.notnull}")
@Size(min = 2,max = 10,message = "{student.name.length}")
private String name;
@Email(message = "{student.email.error}")
private String email;
@Max(value = 150,message = "{student.age.error}")
private Integer age;
public String getEmail() {
return email;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", email='" + email + '\'' +
", age=" + age +
'}';
}
public void setEmail(String email) {
this.email = email;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
由于校验规则都是定义在实体类上面的,但是,在不同的数据提交环境下,校验规则可能不一样。例如,用户的 id 是自增长的,添加的时候,可以不用传递用户 id,但是修改的时候则必须传递用户 id,这种情况下,就需要使用分组校验。
分组校验,首先需要定义校验组,所谓的校验组,其实就是空接口:
public interface ValidationGroup1 {
}
public interface ValidationGroup2 {
}
然后,在实体类中,指定每一个校验规则所属的组:
public class Student {
@NotNull(message = "{student.id.notnull}",groups = ValidationGroup1.class)
private Integer id;
@NotNull(message = "{student.name.notnull}",groups = {ValidationGroup1.class, ValidationGroup2.class})
@Size(min = 2,max = 10,message = "{student.name.length}",groups = {ValidationGroup1.class, ValidationGroup2.class})
private String name;
@Email(message = "{student.email.error}",groups = {ValidationGroup1.class, ValidationGroup2.class})
private String email;
@Max(value = 150,message = "{student.age.error}",groups = {ValidationGroup2.class})
private Integer age;
public String getEmail() {
return email;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", email='" + email + '\'' +
", age=" + age +
'}';
}
public void setEmail(String email) {
this.email = email;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
在 group 中指定每一个校验规则所属的组,一个规则可以属于一个组,也可以属于多个组。
最后,在接收参数的地方,指定校验组:
@Controller
public class StudentController {
@RequestMapping("/addstudent")
@ResponseBody
public void addStudent(@Validated(ValidationGroup2.class) Student student, BindingResult result) {
if (result != null) {
//校验未通过,获取所有的异常信息并展示出来
List allErrors = result.getAllErrors();
for (ObjectError allError : allErrors) {
System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
}
}
}
}
配置完成后,属于 ValidationGroup2 这个组的校验规则,才会生效。
数据回显就是当用户数据提交失败时,自动填充好已经输入的数据。一般来说,如果使用 Ajax 来做数据提交,基本上是没有数据回显这个需求的,但是如果是通过表单做数据提交,那么数据回显就非常有必要了。
简单数据类型,实际上框架在这里没有提供任何形式的支持,就是我们自己手动配置。我们继续在第 10 小节的例子上演示 Demo。加入提交的 Student 数据不符合要求,那么重新回到添加 Student 页面,并且预设之前已经填好的数据。
首先我们先来改造一下 student.jsp 页面:
在接收数据时,使用简单数据类型去接收:
@RequestMapping("/addstudent")
public String addStudent2(Integer id, String name, String email, Integer age, Model model) {
model.addAttribute("id", id);
model.addAttribute("name", name);
model.addAttribute("email", email);
model.addAttribute("age", age);
return "student";
}
这种方式,相当于框架没有做任何工作,就是我们手动做数据回显的。此时访问页面,服务端会再次定位到该页面,而且数据已经预填好。
上面这种简单数据类型的回显,实际上非常麻烦,因为需要开发者在服务端一个一个手动设置。如果使用对象的话,就没有这么麻烦了,因为 SpringMVC 在页面跳转时,会自动将对象填充进返回的数据中。
此时,首先修改一下 student.jsp 页面:
注意,在预填数据中,多了一个 student. 前缀。这 student 就是服务端接收数据的变量名,服务端的变量名和这里的 student 要保持一直。服务端定义如下:
@RequestMapping("/addstudent")
public String addStudent(@Validated(ValidationGroup2.class) Student student, BindingResult result) {
if (result != null) {
//校验未通过,获取所有的异常信息并展示出来
List allErrors = result.getAllErrors();
for (ObjectError allError : allErrors) {
System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
}
return "student";
}
return "hello";
}
注意,服务端什么都不用做,就说要返回的页面就行了,student 这个变量会被自动填充到返回的 Model 中。变量名就是填充时候的 key。如果想自定义这个 key,可以在参数中写出来 Model,然后手动加入 Student 对象,就像简单数据类型回显那样。
另一种定义回显变量别名的方式,就是使用 @ModelAttribute 注解。
@ModelAttribute 这个注解,主要有两方面的功能:
在数据回显时,给变量定义别名,非常容易,直接加这个注解即可:
@RequestMapping("/addstudent")
public String addStudent(@ModelAttribute("s") @Validated(ValidationGroup2.class) Student student, BindingResult result) {
if (result != null) {
//校验未通过,获取所有的异常信息并展示出来
List allErrors = result.getAllErrors();
for (ObjectError allError : allErrors) {
System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
}
return "student";
}
return "hello";
}
这样定义完成后,在前端再次访问回显的变量时,变量名称就不是 student 了,而是 s:
假设有一个 Controller 中有很多方法,每个方法都会返回数据给前端,但是每个方法返回给前端的数据又不太一样,虽然不太一样,但是没有方法的返回值又有一些公共的部分。可以将这些公共的部分提取出来单独封装成一个方法,用 @ModelAttribute 注解来标记。
例如在一个 Controller 中 ,添加如下代码:
@ModelAttribute("info")
public Map info() {
Map map = new HashMap<>();
map.put("username", "javaboy");
map.put("address", "www.javaboy.org");
return map;
}
当用户访问当前 Controller 中的任意一个方法,在返回数据时,都会将添加了 @ModelAttribute 注解的方法的返回值,一起返回给前端。@ModelAttribute 注解中的 info 表示返回数据的 key。