本章主要记录,如何在Spring MVC中添加Hibernate-Validator以及自定义校验注解。本章主要涉及的技术点有:
- javax.validation : JSR 303: Bean Validation的一种实现
- hibernate.validator : JSR 303: Bean Validation的一种实现
- groups : 分组校验
- @GroupSequence : 排序分组校验
- message.properties : 校验提示信息的配置文件
- 自定义注解校验
##1.目录结构
src
\---main
\---java
| \---pers
| \---hanchao
| \---hespringmvc
| \---validation
| \---InsertGroup.java
| \---UpdateGroup.java
| \---Student.java
| \---JsonResult.java
| \---StudentManageController.java
\---webapp
\---message.properties
\---webapp
\---validation
| \---student.jsp
\---WEB-INF
| \---spring-mvc-servlet.xml
| \---web.xml
\---index.jsp
##2.pom.xml引入相关jar包
<hibernate-validator.version>5.4.1.Final</hibernate-validator.version>
<!-- 验证的jar -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
##3.spring-mvc-servlet.xml配置校验驱动
<!--注册校验驱动-->
<mvc:annotation-driven validator="validator"/>
<!--校验工厂-->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
<property name="validationMessageSource" ref="messageSource"/>
</bean>
<!--validation message 校验消息-->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="message"/>
<property name="defaultEncoding" value="utf-8"/>
<property name="useCodeAsDefaultMessage" value="false"/>
<property name="cacheSeconds" value="60"/>
</bean>
##4.Student.java设置校验规则
先看两个分组接口
package pers.hanchao.hespringmvc.validation;
public interface InsertGroup {}
package pers.hanchao.hespringmvc.validation;
public interface UpdateGroup {}
再看被校验的实体类:
package pers.hanchao.hespringmvc.validation;
import org.hibernate.validator.constraints.CreditCardNumber;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;
import pers.hanchao.hespringmvc.validation.custom.annotation.CustomLength;
import javax.validation.GroupSequence;
import javax.validation.constraints.*;
import java.util.Date;
/**
* <p>javax.validation校验、hibernate.validator校验、分组校验、@GroupSequence分组顺序校验</p>
* @author hanchao 2018/1/20 14:58
**/
@GroupSequence({InsertGroup.class,UpdateGroup.class,Student.class})
public class Student {
/javax.validation/
//添加学生时,id必为空;修改学生时,id必须有值
@Null(groups = InsertGroup.class,message = "{Null.student.id}")
@NotNull(groups = UpdateGroup.class,message = "{NotNull.student.id}")
private String id;
@Size(min = 2,max = 16,message = "{Size.student.name}",groups = {InsertGroup.class,UpdateGroup.class})
private String name;//名字
@AssertTrue(groups = InsertGroup.class, message = "{AssertTrue.student.newRegister}")
@AssertFalse(groups = UpdateGroup.class, message = "{AssertTrue.student.newRegister}")
private boolean newRegister;//是否新注册
@Max(value = 100, message = "{Max.student.score}",groups = {InsertGroup.class,UpdateGroup.class})
@Min(value = 0, message = "{Min.student.score}",groups = {InsertGroup.class,UpdateGroup.class})
private int score;//分数[0-100]
@DecimalMax(value = "30",inclusive = true, message = "{student.age}",groups = {InsertGroup.class,UpdateGroup.class})
@DecimalMin(value = "19",inclusive = false, message = "{student.age}",groups = {InsertGroup.class,UpdateGroup.class})
private String age;//年龄范围[20-30]
@Digits(integer = 3,fraction = 2, message = "{Digits.student.weight}",groups = {InsertGroup.class,UpdateGroup.class})
private float weight;//体重格式[xxx.yy]
@Past(message = "{Past.student.entrance}",groups = {InsertGroup.class,UpdateGroup.class})
private Date entrance;//入学时间
@Future(message = "{Future.student.graduation}",groups = {InsertGroup.class,UpdateGroup.class})
private Date graduation;//毕业时间
@Pattern(regexp = "^S2018[0-9]{4}$",flags = Pattern.Flag.CASE_INSENSITIVE, message = "{Pattern.student.number}",groups = {InsertGroup.class,UpdateGroup.class})
private String number;//学号形式 S20180000-S20189999 大小写敏感
/hibernate.validator/
@URL(message = "{URL.student.blog}",groups = {InsertGroup.class,UpdateGroup.class})
private String blog;//个人学生主页
@Length(min = 1000,max = 5000 ,message = "{Length.student.bonus}",groups = {InsertGroup.class,UpdateGroup.class})
private String tuition;//学费
@Range(min = 2000L,max = 4000L,message = "{Rang.student.bonus}",groups = {InsertGroup.class,UpdateGroup.class})
private String bonus;//奖金
@CreditCardNumber(message = "{CreditCardNumber}",groups = {InsertGroup.class,UpdateGroup.class})
private String creditCard;//银行账号
//toString()
//setter and getter
}
校验规则说明:
-
@URL
、@Length
、@Range、@CreditCardNumber
属于hibernate.validator
,其他的都属于javax.validation
。 -
groups = InsertGroup.class
表名此字段只在InsertGroup
分组被校验。 -
groups = {InsertGroup.class,UpdateGroup.class}
表面此字段在InsertGroup
和UpdateGroup
分组都被校验。 -
一个字段可以被多种校验规则注解,如
@Max(value = 100, message = "{Max.student.score}",groups = {InsertGroup.class,UpdateGroup.class}) @Min(value = 0, message = "{Min.student.score}",groups = {InsertGroup.class,UpdateGroup.class}) private int score;//分数[0-100]
-
@GroupSequence({InsertGroup.class,UpdateGroup.class,Student.class})
表明了InsertGroup
和UpdateGroup
分组的校验顺序,即:如果InsertGroup
分组的校验有错误,则不再进行UpdateGroup
分组的校验,直接返回。 -
@GroupSequence({InsertGroup.class,UpdateGroup.class,Student.class})
一定要注意不要忘了把当前实体类Student.class
写入其中。 -
message = "{Length.student.bonus}""
是通过message.properties
读取校验提示信息的方式。 -
多种校验规则可以共用同样的校验提示信息,如:
@DecimalMax(value = "30",inclusive = true, message = "{student.age}",groups = {InsertGroup.class,UpdateGroup.class}) @DecimalMin(value = "19",inclusive = false, message = "{student.age}",groups = {InsertGroup.class,UpdateGroup.class}) private String age;//年龄范围[20-30]
##5.message.properties设置校验提示信息
Null.student.id = 学生id必须为空!
NotNull.student.id = 学生id必须不为空!
Size.student.name = 姓名应该在2至16个汉字之间!
CustomLength.student.name = 姓名应该在2至16个字符之间!
AssertTrue.student.newRegister = 必须是新注册的学生!
AssertFalse.student.newRegister = 必须是已经注册的学生!
Max.student.score = 分数必须小于等于100!
Min.student.score = 分数必须大于等于0!
student.age = 年龄必须处于20~30之间!
Digits.student.weight = 体重必须是xxx.yy的格式,如101.52!
Past.student.entrance = 入学时间必须早于今天!
Future.student.graduation = 毕业时间必须晚于今天!
Pattern.student.number = 学号格式必须位于S20180000-S20189999之间!
URL.student.blog = 学生个人主页必须错误!
Length.student.bonus = 学费必须在1000-5000之间!
Rang.student.bonus = 奖金必须在2000-4000之间!
CreditCardNumber = 银行账号不合法!
校验提示信息说明:
- 这里的
key
,例如Null.student.id
等,应该与Student.java
中的message = "{Null.student.id}"
相匹配。
##6.StudentManageController.java控制类对实体进行校验
package pers.hanchao.hespringmvc.validation;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
/**
* <p>简单的bean validation和hibernate validator的例子</p>
* @author hanchao 2018/1/20 14:47
**/
@Controller
@RequestMapping("validation")
public class StudentManageController {
/**
* <p>注册学生信息</p>
* @author hanchao 2018/1/20 14:47
**/
@PostMapping("/insert")
@ResponseBody
public JsonResult insert(@Validated(InsertGroup.class) @RequestBody Student student, BindingResult bindingResult){
JsonResult jsonResult = new JsonResult();
validate(bindingResult, jsonResult);
System.out.println(jsonResult.toString());
return jsonResult;
}
/**
* <p>修改学生信息</p>
* @author hanchao 2018/1/20 14:47
**/
@PostMapping("/update")
@ResponseBody
public JsonResult update(@Validated(UpdateGroup.class) @RequestBody Student student, BindingResult bindingResult){
JsonResult jsonResult = new JsonResult();
validate(bindingResult, jsonResult);
System.out.println(jsonResult.toString());
return jsonResult;
}
/**
* <p>对bindingResult进行校验</p>
* @author hanchao 2018/1/20 14:46
**/
private void validate(BindingResult bindingResult, JsonResult jsonResult) {
if (bindingResult.hasErrors()){
StringBuffer errors = new StringBuffer();
List<ObjectError> allErrors = bindingResult.getAllErrors();
for (ObjectError objectError : allErrors){
errors.append(objectError.getDefaultMessage() + "<br/>");
}
jsonResult.setCodeAndMessage("0",errors.toString());
}
}
}
说明:
@Validated(InsertGroup.class) @RequestBody Student student
表名只对页面信息进行InsertGroup.class
分组相关的校验。- 如果不指定校验哪个分组,如
@Validated @RequestBody Student student
则会校验所有的字段。 - 如果不指定校验哪个分组,如
@Validated @RequestBody Student student
可以用@Valid @RequestBody Student student
替代。
##7.student.jsp页面
<%--
Created by IntelliJ IDEA.
User: hanchao
Date: 2018/1/17
Time: 21:14
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>学生信息管理页面</title>
<script type="text/javascript" src="../static/jquery-3.2.1.min.js"></script>
<style>
table, td, th{border:1px solid green;background-color: lemonchiffon;text-align:center}
</style>
</head>
<body>
<p id="errorString"/>
<form>
<table>
<tr>
<td><label>姓名</label></td>
<td><input name="name" type="text"></td>
</tr>
<tr>
<td><label>是否新注册用户</label></td>
<td><input name="newRegister" type="text"></td>
</tr>
<tr>
<td><label>学分</label></td>
<td><input name="score" type="text"></td>
</tr>
<tr>
<td><label>年龄</label></td>
<td><input name="age" type="text"></td>
</tr>
<tr>
<td><label>姓名</label></td>
<td><input name="name" type="text"></td>
</tr>
<tr>
<td><label>体重(kg)</label></td>
<td><input name="weight" type="text"></td>
</tr>
<tr>
<td><label>入学时间</label></td>
<td><input name="entrance" type="text"></td>
</tr>
<tr>
<td><label>毕业时间</label></td>
<td><input name="graduation" type="text"></td>
</tr>
<tr>
<td><label>学号</label></td>
<td><input name="number" type="text"></td>
</tr>
<tr>
<td><label>个人学生主页</label></td>
<td><input name="blog" type="text"></td>
</tr>
<tr>
<td><label>学费</label></td>
<td><input name="tuition" type="text"></td>
</tr>
<tr>
<td><label>奖金</label></td>
<td><input name="bonus" type="text"></td>
</tr>
<tr>
<td><label>银行卡号</label></td>
<td><input name="creditCard" type="text"></td>
</tr>
<tr>
<td><input type="button" value="注册" οnclick="insert()"/></td>
<td><input type="button" value="查询" οnclick="update()"/></td>
</tr>
</table>
</form>
</body>
<script type="text/javascript">
//插入一条学生信息
function insert() {
$.ajax({
type:"POST",
url:"/validation/insert",
data:JSON.stringify({
name:$('input[name="name"]').val(),
newRegister:$('input[name="newRegister"]').val(),
score:$('input[name="score"]').val(),
age:$('input[name="age"]').val(),
weight:$('input[name="weight"]').val(),
entrance:$('input[name="entrance"]').val(),
graduation:$('input[name="graduation"]').val(),
number:$('input[name="number"]').val(),
reward:$('input[name="reward"]').val(),
opinion:$('input[name="opinion"]').val(),
email:$('input[name="email"]').val(),
blog:$('input[name="blog"]').val(),
tuition:$('input[name="tuition"]').val(),
bonus:$('input[name="bonus"]').val(),
creditCard:$('input[name="creditCard"]').val()
}),
contentType:'application/json;charset=utf-8',
dataType:'json',
success:function (jsonResult) {
console.log(jsonResult);
$("#errorString").html(jsonResult.message);
}
})
}
//更新学生信息
function update() {
$.ajax({
type:"POST",
url:"/validation/update",
data:JSON.stringify({
name:$('input[name="name"]').val(),
newRegister:$('input[name="newRegister"]').val(),
score:$('input[name="score"]').val(),
age:$('input[name="age"]').val(),
weight:$('input[name="weight"]').val(),
entrance:$('input[name="entrance"]').val(),
graduation:$('input[name="graduation"]').val(),
number:$('input[name="number"]').val(),
reward:$('input[name="reward"]').val(),
opinion:$('input[name="opinion"]').val(),
email:$('input[name="email"]').val(),
blog:$('input[name="blog"]').val(),
tuition:$('input[name="tuition"]').val(),
bonus:$('input[name="bonus"]').val(),
creditCard:$('input[name="creditCard"]').val()
}),
contentType:'application/json;charset=utf-8',
dataType:'json',
success:function (jsonResult) {
console.log(jsonResult);
$("#errorString").html(jsonResult.message);
}
})
}
</script>
</html>
###8.result
##9.自定义注解校验
场景(假设情况…):
@Length
校验的是输入字符的个数,例如你好
的长度是2- 如果
name
字段的校验规则为(min = 2,max = 4)
,在mysql中name
字段的长度设计为varchar(4)
。 - 这种校验应用在
mysql 5.0
以后是没问题的,因为Mysql 5.0
以后varchar
存储的就是字符数,name
字段最多可以存储4个汉字。 - 但是这种校验应用在
Mysql 4.0
数据库中是有问题的,因为Mysql 4.0
存储的是字节数,name
组多存储1个UTF-8
汉字或者2个GBG
汉字。 - 如果前台传过来的
name=张三丰
,虽然校验不报错,但是插入mysql
时,肯定会报错。
这里引入了自定义校验注解的概念,需要设计一个新的注解实现以下功能:
- 能够通过
min
和max
设置最小和最大长度。 - 校验的是输入内容的字节数。
- 能够设置编码格式。
为了完成这个自定义注解,下面分两步进行:
目录结构(续接之前的目录结构):
\---validation
\---custom
\---annotation
\---CustomLength.java
\---validator
\---CustomLengthValidator.java
###9.1.CustomLength.java自定义校验注解
package pers.hanchao.hespringmvc.validation.custom.annotation;
import pers.hanchao.hespringmvc.validation.custom.validator.CustomLengthValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* <p>校验字符串长度,中文按照charset进行计算</p>
* @author hanchao 2018/1/20 15:40
**/
@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = CustomLengthValidator.class)
@Documented
public @interface CustomLength {
long min() default 0;
long max() default Integer.MAX_VALUE;
String charset() default "gbk";
String message() default "length must be between {min} and {max}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
###9.2.CustomLengthValidator自定义校验规则类
package pers.hanchao.hespringmvc.validation.custom.validator;
import java.io.UnsupportedEncodingException;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.hibernate.validator.internal.util.logging.Log;
import org.hibernate.validator.internal.util.logging.LoggerFactory;
import pers.hanchao.hespringmvc.validation.custom.annotation.CustomLength;
/**
* <p>自定义校验规则诶</p>
* @author hanchao 2018/1/20 17:04
**/
public class CustomLengthValidator implements ConstraintValidator<CustomLength, String> {
private static final Log log = LoggerFactory.make();
private long min;
private long max;
private String charset;
@Override
public void initialize(CustomLength parameters) {
//do nothing
this.min = parameters.min();
this.max = parameters.max();
this.charset = parameters.charset();
validateParameters();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if(null == value){
value = "";
}
long length = 0;
try {
length = ((String)value).getBytes(charset).length;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
boolean result = (length >= min) && (length <= max);
log.info("CustomLength.validator:[value:" + (String)value + ",min:" + min + ",max:" + max + ",length:" + length + "],result:" + result);
return (length >= min) && (length <= max);
}
private void validateParameters() {
if (this.min < 0) {
throw log.getMinCannotBeNegativeException();
}
if (this.max < 0) {
throw log.getMaxCannotBeNegativeException();
}
if (this.max < this.min)
throw log.getLengthCannotBeNegativeException();
}
}
以上,自定义校验注解CustomLenght
定义完毕,下面进行实践。
###9.3.修改Student.java
为了方便演示,注释掉了其他校验
// @Size(min = 2,max = 4,message = "{Size.student.name}",groups = {InsertGroup.class,UpdateGroup.class})
@CustomLength(min = 2,max = 4,charset = "utf-8",message = "{CustomLength.student.name}",groups = {InsertGroup.class,UpdateGroup.class})
private String name;//名字
##9.4.修改message.properties
添加提示信息
CustomLength.student.name = 姓名应该在2至4个字节之间!
###9.5.result