参数校验是我们程序开发中必不可少的过程。用户在前端页面上填写表单时,前端js程序会校验参数的合法性,当数据到了后端,为了防止恶意操作,保持程序的健壮性,后端同样需要对数据进行校验。后端参数校验最简单的做法是直接在业务方法里面进行判断,当判断成功之后再继续往下执行。但这样带给我们的是代码的耦合,冗余。当我们多个地方需要校验时,我们就需要在每一个地方调用校验程序,导致代码很冗余,且不美观。
那么如何优雅的对参数进行校验呢?JSR303就是为了解决这个问题出现的,本篇文章主要是介绍 JSR303,Hibernate Validator 等校验工具的使用,以及自定义校验注解的使用。
校验框架介绍
JSR303 是一套JavaBean参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们JavaBean的属性上面,就可以在需要校验的时候进行校验了。注解如下:
Hibernate validator 在JSR303的基础上对校验注解进行了扩展,扩展注解如下:
Spring validtor 同样扩展了jsr303,并实现了方法参数和返回值的校验
Spring 提供了MethodValidationPostProcessor类,用于对方法的校验
代码实现
添加JAR包依赖
在pom.xml中添加如下依赖:
<!--jsr 303-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
<!-- hibernate validator-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.0.Final</version>
</dependency>
最简单的参数校验
1、Model 中添加校验注解
public class Book {
private long id;
/**
* 书名
*/
@NotEmpty(message = "书名不能为空")
private String bookName;
/**
* ISBN号
*/
@NotNull(message = "ISBN号不能为空")
private String bookIsbn;
/**
* 单价
*/
@DecimalMin(value = "0.1",message = "单价最低为0.1")
private double price; // getter setter ....... }
2、在controller中使用此校验
/**
* 添加Book对象
* @param book
*/
@RequestMapping(value = "/book", method = RequestMethod.POST)
public void addBook(@RequestBody @Valid Book book) {
System.out.println(book.toString());
}
当访问这个post接口时,如果参数不符合Model中定义的话,程序中就回抛出400异常,并提示错误信息。
自定义校验注解
虽然jSR303和Hibernate Validtor 已经提供了很多校验注解,但是当面对复杂参数校验时,还是不能满足我们的要求,这时候我们就需要 自定义校验注解。
下面以“List数组中不能含有null元素”为实例自定义校验注解
1、注解定义如下:
package com.beiyan.validate.annotation;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 自定义参数校验注解
* 校验 List 集合中是否有null 元素
*/
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = ListNotHasNullValidatorImpl.class)此处指定了注解的实现类为ListNotHasNullValidatorImpl
public @interface ListNotHasNull {
/**
* 添加value属性,可以作为校验时的条件,若不需要,可去掉此处定义
*/
int value() default 0;
String message() default "List集合中不能含有null元素";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* 定义List,为了让Bean的一个属性上可以添加多套规则
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@interface List {
ListNotHasNull[] value();
}
}
2、注解实现类:
package com.beiyan.validate.annotation;
import org.springframework.stereotype.Service;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.List;
/**
* 自定义注解ListNotHasNull 的实现类
* 用于判断List集合中是否含有null元素
*/
@Service
public class ListNotHasNullValidatorImpl implements ConstraintValidator<ListNotHasNull, List> {
private int value;
@Override
public void initialize(ListNotHasNull constraintAnnotation) {
//传入value 值,可以在校验中使用
this.value = constraintAnnotation.value();
}
public boolean isValid(List list, ConstraintValidatorContext constraintValidatorContext) {
for (Object object : list) {
if (object == null) {
//如果List集合中含有Null元素,校验失败
return false;
}
}
return true;
}
}
3、model添加注解:
public class User {
//其他参数 .......
/**
* 所拥有的书籍列表
*/
@NotEmpty(message = "所拥有书籍不能为空")
@ListNotHasNull(message = "List 中不能含有null元素")
@Valid
private List<Book> books;
//getter setter 方法.......
}
使用方法同上,在在需要校验的Model上面加上@Valid 即可
分组验证
对同一个Model,我们在增加和修改时对参数的校验也是不一样的,这个时候我们就需要定义分组验证,步骤如下
1、定义两个空接口,分别代表Person对象的增加校验规则和修改校验规则
/**
* 可以在一个Model上面添加多套参数验证规则,此接口定义添加Person模型新增时的参数校验规则
*/
public interface PersonAddView {
}
/**
* 可以在一个Model上面添加多套参数验证规则,此接口定义添加Person模型修改时的参数校验规则
*/
public interface PersonModifyView {
}
2、Model上添加注解时使用指明所述的分组
public class Person {
private long id;
/**
* 添加groups 属性,说明只在特定的验证规则里面起作用,不加则表示在使用Deafault规则时起作用
*/
@NotNull(groups = {PersonAddView.class, PersonModifyView.class}, message = "添加、修改用户时名字不能为空", payload = ValidateErrorLevel.Info.class)
@ListNotHasNull.List({
@ListNotHasNull(groups = {PersonAddView.class}, message = "添加上Name不能为空"),
@ListNotHasNull(groups = {PersonModifyView.class}, message = "修改时Name不能为空")})
private String name;
@NotNull(groups = {PersonAddView.class}, message = "添加用户时地址不能为空")
private String address;
@Min(value = 18, groups = {PersonAddView.class}, message = "姓名不能低于18岁")
@Max(value = 30, groups = {PersonModifyView.class}, message = "姓名不能超过30岁")
private int age;
//getter setter 方法......
}
3、启用校验
此时启用校验和之前的不同,需要指明启用哪一组规则
/**
* 添加一个Person对象
* 此处启用PersonAddView 这个验证规则
* 备注:此处@Validated(PersonAddView.class) 表示使用PersonAndView这套校验规则,若使用@Valid 则表示使用默认校验规则,
* 若两个规则同时加上去,则只有第一套起作用
*/
@RequestMapping(value = "/person", method = RequestMethod.POST)
public void addPerson(@RequestBody @Validated({PersonAddView.class, Default.class}) Person person) {
System.out.println(person.toString());
}
/**
* 修改Person对象
* 此处启用PersonModifyView 这个验证规则
*/
@RequestMapping(value = "/person", method = RequestMethod.PUT)
public void modifyPerson(@RequestBody @Validated(value = {PersonModifyView.class}) Person person) {
System.out.println(person.toString());
}
Spring validator 方法级别的校验
JSR和Hibernate validator的校验只能对Object的属性进行校验,不能对单个的参数进行校验,spring 在此基础上进行了扩展,添加了MethodValidationPostProcessor拦截器,可以实现对方法参数的校验,实现如下:
1、实例化MethodValidationPostProcessor
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
2、在所要实现方法参数校验的类上面添加@Validated,如下
@RestController
@Validated
public class ValidateController {
}
3、在方法上面添加校验规则:
@RequestMapping(value = "/test", method = RequestMethod.GET)
public String paramCheck(@Length(min = 10) @RequestParam String name) {
System.out.println(name);
return null;
}
当方法上面的参数校验失败,spring 框架就回抛出异常
{
"timestamp": 1476108200558,
"status": 500,
"error": "Internal Server Error",
"exception": "javax.validation.ConstraintViolationException",
"message": "No message available",
"path": "/test"
}
从此可以优雅的对参数进行校验了
写在后面的话:
本篇文章只列举了常用的几种校验方法,其实关于校验的内容还有很多:
校验信息的国际化显示,
组合参数校验,
message中使用EL表达式,
将校验信息绑定到ModelAndView等,这里就不一一列出了,下面这几篇文章写的也不错,读者可以参考:
将校验信息绑定到ModelAndView http://www.voidcn.com/blog/983836259/article/p-5794496.html
集成Bean Validation 1.1(JSR-349)到SpringMVC https://my.oschina.net/qjx1208/blog/200946
一 简介
SpringMVC支持与JSR 349 Bean Validation API的集成。借助于Bean验证,可以非常容易地将验证元数据应用到实体类,并且通过合适的视图向用户展示可能的错误结果。在模型类中可以通过注解对属性验证进行定义,常见的注解有:@Size ,@Email ,@Pattern,@Max等,分别验证长度,邮箱格式,自定义正则表达式,最大值(PS:更多相关注解可以百度或者查API)
二 测试实例
(1)新建一个动态Java web项目,然后下载“Hibernate Validator”的jar包,下载地址:http://hibernate.org/validator/ ,最后是导入必要的几个jar包和springmvc所需要的jar包,最后的项目结构如下:
(2)配置文件web.xml:
<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>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
在web.xml中定义了springmvc处理请求的后缀是.html,同时设置了编码为UTF-8
(3)配置文件springmvc-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-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<context:component-scan base-package="cn.zifangsky.* *.controller" />
<context:annotation-config /> <!-- 激活Bean中定义的注解 -->
<mvc:annotation-driven validator="validator"/>
<!-- 视图相关配置 -->
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/" /> <!-- 视图前缀 -->
<property name="suffix" value=".jsp" /> <!-- 视图后缀 -->
</bean>
<bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basename" value="classpath:errors" />
</bean>
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="validationMessageSource" ref="messageSource"/>
</bean>
</beans>
在这里,主要是配置了视图相关配置,国际化(PS:在这里就是根据中文和英文网页显示不同的错误提示信息),以及定义了验证相关的配置。需要注意的是,还需要在mvc命名空间的annotation-driven标签中定义验证程序
(4)两个国际化文件errors_zh_CN.properties和errors_en_US.properties:
errors_zh_CN.properties文件:
error.password = \u5BC6\u7801\u662F\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5199\u5B57\u6BCD\u3001\u5C0F\u5199\u5B57\u6BCD\u548C\u6570\u5B57\u76846-20\u4F4D\u5B57\u7B26
error.username = \u7528\u6237\u540d\u9700\u8981\u0033\u002d\u0032\u0030\u4f4d
error.age = \u5e74\u9f84\u6700\u5927\u662f\u0031\u0032\u0030\u5c81
这里以键值对的形式定义了三个中文错误提示信息,需要注意的是使用了“Unicode编码”,如果开发工具不能自动编码的话,可以在这个网页进行手动转换编码:http://tool.chinaz.com/tools/unicode.aspx
errors_en_US.properties文件:
error.password = Passwords are 6-20 bit characters that must contain both upper and lower case letters and digits at the same time.
error.username = username needs 3-20 bit characters
error.age = Max age is 120 years old
(5)实体类User.java:
package cn.zifangsky.model;
import javax.validation.constraints.Max;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.Email;
public class User {
@Size(min = 3, max = 20, message = "{error.username}")
private String username;
@Email
private String email;
@Pattern(regexp = "^((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])).{6,20}$", message = "{error.password}")
private String password;
@Max(value = 120, message = "{error.age}")
private int age;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
这里验证了:用户名必须是3-20位,如果验证不通过则通过配置的“message”参数根据语言环境在前台页面显示错误提示信息;邮箱使用默认配置验证了邮箱格式是否正确;密码这一项通过一个正则表达式验证了必须是包含大写字母、小写字母和数字的6-20位的字符;年龄验证了最大是120岁
(6)UserController.java这个controller类:
package cn.zifangsky.controller;
import javax.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import cn.zifangsky.model.User;
@Controller
public class UserController {
@RequestMapping(value = "/form")
public ModelAndView user() {
ModelAndView modelAndView = new ModelAndView("userForm");
modelAndView.addObject("user", new User());
return modelAndView;
}
@RequestMapping(value = "/result", method = RequestMethod.POST)
public ModelAndView processUser(@Valid User user, BindingResult result) {
ModelAndView modelAndView = new ModelAndView("userResult");
modelAndView.addObject("u", user);
// 如果出现验证错误,则转到"userForm"视图
if (result.hasErrors()) {
modelAndView.setViewName("userForm");
} else {
modelAndView.setViewName("userResult");
}
return modelAndView;
}
}
从代码可以看出,定义了两个方法,user方法负责处理”/form”请求,然后转到“userForm.jsp”这个视图页面;在processUser方法中,对用户输入的验证由@Valid注解触发,该注解被递归地应用到实体类User的各个属性中。同时在这个方法中接收了一个额外的参数result,该参数是一个BindingResult实例,通过使用该参数来检查在将请求参数映射到实体类属性的过程中是否发生了任何验证错误。最后是根据条件转到相关视图页面
(7)三个前台jsp页面:
i)根目录下的index.jsp:
<% response.sendRedirect("form.html"); %>
这个文件就简单的一句话,在项目启动时直接请求“form.html”
ii)userForm.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib uri="http://www.springframework.org/tags/form" prefix="mvc"%>
<html>
<head>
<title>SpringMVC Form表单验证</title>
<style type="text/css">
.formFieldError {
background-color: #FAFFBD;
}
</style>
</head>
<body>
<h2>用户注册:</h2>
<mvc:form modelAttribute="user" action="result.html">
<table>
<tr>
<td><mvc:label path="username">用户名:</mvc:label></td>
<td><mvc:input path="username" cssErrorClass="formFieldError" /></td>
<td><mvc:errors path="username" /></td>
</tr>
<tr>
<td><mvc:label path="email">邮箱:</mvc:label></td>
<td><mvc:input path="email" cssErrorClass="formFieldError" /></td>
<td><mvc:errors path="email" /></td>
</tr>
<tr>
<td><mvc:label path="password">密码:</mvc:label></td>
<td><mvc:password path="password"
cssErrorClass="formFieldError" /></td>
<td><mvc:errors path="password" /></td>
</tr>
<tr>
<td><mvc:label path="age">年龄:</mvc:label></td>
<td><mvc:input path="age" cssErrorClass="formFieldError" /></td>
<td><mvc:errors path="age" /></td>
</tr>
<tr>
<td colspan="3"><input type="submit" value="Submit" /></td>
</tr>
</table>
</mvc:form>
</body>
</html>
这个文件中定义的errors标签就是分别显示各个参数验证之后的错误提示信息
iii)userResult.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib uri="http://www.springframework.org/tags/form" prefix="mvc"%>
<html>
<head>
<title>SpringMVC Form表单验证结果</title>
</head>
<body>
<h2>用户注册结果</h2>
<table>
<tr>
<td>用户名:</td>
<td>${u.username}</td>
</tr>
<tr>
<td>邮箱:</td>
<td>${u.email}</td>
</tr>
<tr>
<td>密码:</td>
<td>${u.password}</td>
</tr>
<tr>
<td>年龄:</td>
<td>${u.age}</td>
</tr>
</table>
</body>
</html>
这个文件只是简单的信息展示,不多做解释
三 测试
(1)中文环境下的错误提示:
项目启动后,使用中文类型的浏览器填写表单,可以发现,出现错误后显示的提示信息如下:
可以发现,出现了预期的提示效果
(2)英文环境下的错误提示:
由于我装的浏览器是中文的,而且也不想更改浏览器的语言,因此我通过在提交表单时抓包手动更改Header中的“Accept-Language”这一项属性,改成“en-US,en;q=0.5”,相当于给服务端说我是英语环境(PS:q参数表示用户的喜爱度,不用管,随便设一个就行)
最后的显示效果如下:
注:实际上,应该把页面中其他的一些中文也设置成国际化形式,那样就可以在英文环境中显示成英文了。不过为了减小理解难度,因此在这里就没有添加上这个步骤,当然我在后面可能会单独写一写SpringMVC的国际化
bean validation 分组验证及分组顺序
分组验证及分组顺序
如果我们想在新增的情况验证id和name,而修改的情况验证name和password,怎么办? 那么就需要分组了。
首先定义分组接口:
- public interface First {
- }
- public interface Second {
- }
分组接口就是两个普通的接口,用于标识,类似于java.io.Serializable。
接着我们使用分组接口标识实体:
- public class User implements Serializable {
- @NotNull(message = "{user.id.null}", groups = {First.class})
- private Long id;
- @Length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {Second.class})
- @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.illegal}", groups = {Second.class})
- private String name;
- @NotNull(message = "{user.password.null}", groups = {First.class, Second.class})
- private String password;
- }
验证时使用如:
- @RequestMapping("/save")
- public String save(@Validated({Second.class}) User user, BindingResult result) {
- if(result.hasErrors()) {
- return "error";
- }
- return "success";
- }
即通过@Validate注解标识要验证的分组;如果要验证两个的话,可以这样@Validated({First.class, Second.class})。
接下来我们来看看通过分组来指定顺序;还记得之前的错误消息吗? user.name会显示两个错误消息,而且顺序不确定;如果我们先验证一个消息;如果不通过再验证另一个怎么办?可以通过@GroupSequence指定分组验证顺序:
- @GroupSequence({First.class, Second.class, User.class})
- public class User implements Serializable {
- private Long id;
- @Length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {First.class})
- @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.illegal}", groups = {Second.class})
- private String name;
- private String password;
- }
通过@GroupSequence指定验证顺序:先验证First分组,如果有错误立即返回而不会验证Second分组,接着如果First分组验证通过了,那么才去验证Second分组,最后指定User.class表示那些没有分组的在最后。这样我们就可以实现按顺序验证分组了。
@GroupSequence只能运用在Type(也就是类)下。其中,Default.class不能出现在GroupSequence列表中,且对应类的Object.class是GroupSequence列表中的一部分,一般放在最后。
Restful风格接口能够返回JSON类型的数据形式:
{
"code": "400",
"data": null,
"messages": [
{
"invalidValue": "Name : Donald, Street: Stree no 27, Phone : (123) 123-1234, city: Sydney",
"message": "Invalid address: Check your phone number or zip code"
}
]
-
组合约束
在一些复杂的场景,验证值需要多个约束同时使用,会让人感动过于复杂且啰嗦,如果相同的字段被其他类引用,则相应的约束需完全被拷贝至其他类,这违背了DRY原则(In software engineering, don’t repeat yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds.)。
为了解决这个问题,我们可以通过多个基本的约束组合创建更高级别的约束。示例代码如下:
@NotNull
@Size(min = 2, max = 14)
@CheckCase(CaseMode.UPPER)
@Target({ METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = { })
@Documented
@ReportAsSingleViolation
public @interface ValidLicensePlate {
String message() default "{org.hibernate.validator.referenceguide.chapter06." +
"constraintcomposition.ValidLicensePlate.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
创建组合约束,仅需要组合其他约束即可,如果组合约束需要验证器,通过@Constraint
指定,反之,无需指定,如上代码所示:@Constraint(validatedBy = { })
。
注解@ReportAsSingleViolation
使任何组合验证违反时,仅作为单一错误信息。使用和之前的使用方式一致。