JSR303
中校验注解的groups
属性。groups
的了解之路,来源于一个朋友的解决思路。Controller
中使用@ModelAttribute
注解的方法判断用户来自于哪个国家,并为每个国家编写特定的实体类,才能在Controller
的验证方法入参中使用@Valid
或@Validated
注解进行一步到位的验证操作。Bean
对象,在这些Bean
对象中使用验证注解注释字段;Bean
对象中,使用验证类提供的groups
属性进行验证分组。Java Marker Interface
。Java Marker Interface
标记接口呢?类似于Serializable
,它就是JDK
中的一个标记接口。Java
语言所特有的,而是计算机科学中一种通用的设计理念。JSR303
时,细心的人一定会发现一个细节,所有的验证注解中都存在两个属性:groups
和payload
。@NotNull
注解的源码:
markdown
中会出现错误的代码颜色,这里只贴截图不贴源码了。payload
的用法,可以发现@NotNull
中的确包含了groups
属性。groups
属性用作分组校验,在使用Validator
对实体类进行校验时,可以传入的参数不仅仅是实体类对象。Validator
类中提供的validate()
方法的源码:/**
* Validates all constraints on {@code object}.
*
* @param object object to validate
* @param groups the group or list of groups targeted for validation (defaults to
* {@link Default})
* @param <T> the type of the object to validate
* @return constraint violations or an empty set if none
* @throws IllegalArgumentException if object is {@code null}
* or if {@code null} is passed to the varargs groups
* @throws ValidationException if a non recoverable error happens
* during the validation process
*/
<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);
validate
可以接收Class
对象作为额外的参数,而这个Class
对象指的就是groups
。object
进行校验前,会首先根据groups
信息提前筛选出需要进行校验的字段,然后再对object
进行校验操作。groups
所要完成的使命,就是标记该验证类注解所在字段的分组详情。groups
进行初步剖析后,可能仍然不太明白其中的工作原理。Person
,其中记录着学生Student
和老师Teacher
的信息,但对于学生来说,需要提供名字、学号和班级的信息,而对于老师来说只需要提供名字信息即可。不考虑现实合理性问题,此时如何编写Person
类?Person
类中,我们需要添加验证类注解,以确保字段的合法性,同时通过注解的groups
属性,将验证字段分为两组,一组为Student
,另一组为Teacher
;另一方面,在验证环节需要告诉验证器此时验证的对象是Student
还是Teacher
。Student
和Teacher
如下:package cn.dylanphang.mark;
public interface Student {
}
package cn.dylanphang.mark;
public interface Teacher {
}
Person
如下:package cn.dylanphang.pojo;
import cn.dylanphang.mark.Student;
import cn.dylanphang.mark.Teacher;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* @author dylan
*/
@Data
public class Person {
@NotBlank(groups = {Student.class, Teacher.class})
private String name;
@NotBlank(groups = {Student.class})
private String className;
@NotBlank(groups = {Student.class})
private String studentNo;
}
Person
的字段上,使用groups
进行分组,groups
所接收的参数为Class<?>[]
。package cn.dylanphang.controller;
import cn.dylanphang.mark.Student;
import cn.dylanphang.mark.Teacher;
import cn.dylanphang.pojo.Person;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;
/**
* @author dylan
*/
public class PersonValidateTest {
private Person person;
private Validator validator;
@Before
public void init() {
this.person = new Person();
final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
this.validator = validatorFactory.getValidator();
}
@Test
public void testStudent() {
this.person.setName("dylan");
this.person.setClassName("anyClass");
this.person.setStudentNo("11250401128");
final Set<ConstraintViolation<Person>> validate = this.validator.validate(this.person, Student.class);
Assert.assertEquals(0, validate.size());
}
@Test
public void testTeacher() {
this.person.setName("kevin");
final Set<ConstraintViolation<Person>> validate = this.validator.validate(this.person, Teacher.class);
Assert.assertEquals(0, validate.size());
}
@Test
public void testEmpty() {
final Set<ConstraintViolation<Person>> validateA = this.validator.validate(this.person, Student.class);
Assert.assertEquals(3, validateA.size());
final Set<ConstraintViolation<Person>> validateB = this.validator.validate(this.person, Teacher.class);
Assert.assertEquals(1, validateB.size());
}
/**
* 值得注意,此时Person对象不为null,但其中的各项字段均为null。由于没有指定groups,对this.person来说,等于没有任何需要验证的字段。
*/
@Test
public void testNoGroups() {
final Set<ConstraintViolation<Person>> validate = this.validator.validate(this.person);
Assert.assertEquals(0, validate.size());
}
}
validate
方法进行验证时,如果不传入相关的分组信息,则表明当前传入的对象不需要进行验证。null
,此时验证也能通过。那么如何避免这种情况呢?其实在此前的validate()
源码中就已经出现了答案。我们再看一次validate()
的源码:/**
* Validates all constraints on {@code object}.
*
* @param object object to validate
* @param groups the group or list of groups targeted for validation (defaults to
* {@link Default})
* @param <T> the type of the object to validate
* @return constraint violations or an empty set if none
* @throws IllegalArgumentException if object is {@code null}
* or if {@code null} is passed to the varargs groups
* @throws ValidationException if a non recoverable error happens
* during the validation process
*/
<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);
@param groups the group or list of groups targeted for validation (defaults to{@link Default})
groups
没有值的时候,它具有默认值Default.class
。在此前没有加入分组信息,使用validate()
进行校验时,程序都会自动地添加一个默认的分组Default.class
。Person
实体类中的groups
属性:package cn.dylanphang.pojo;
import cn.dylanphang.mark.Student;
import cn.dylanphang.mark.Teacher;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* @author dylan
*/
@Data
public class Person {
@NotBlank(groups = {Student.class, Teacher.class, Default.class})
private String name;
@NotBlank(groups = {Student.class, Default.class})
private String className;
@NotBlank(groups = {Student.class, Default.class})
private String studentNo;
}
@Test
public void testNoGroups() {
final Set<ConstraintViolation<Person>> validate = this.validator.validate(this.person);
Assert.assertEquals(3, validate.size());
}
testNoGroups()
查看结果:groups
中添加Default.class
以确保在validate()
不提供任何标记接口的情况下,也会对当前传入对象的字段进行合法性校验。此时对象字段所属的校验组别为Default.class
。groups
用法都了解后,可以开始针对开篇的案例进行代码的编写。groups
属性用法,因此案例编写将针对第二种解决方案。China
、日本Japan
和美国USA
三个国家的用户的信息需要进行校验,那么首先应该具备三个标记接口,这三个标记接口均继承了名为Country
的标记接口。(提示:接口是无法实现接口的哟。)Country
标记接口:package cn.dylanphang.mark.supers;
public interface Country {
}
China
标记接口:package cn.dylanphang.mark.supers;
public interface China extends Country {
}
Japan
标记接口:package cn.dylanphang.mark.supers;
public interface Japan extends Country {
}
USA
标记接口:package cn.dylanphang.mark.supers;
public interface USA extends Country {
}
PersonInfo
类,同时在验证注解中列举分组信息:package cn.dylanphang.pojo;
import cn.dylanphang.mark.China;
import cn.dylanphang.mark.Japan;
import cn.dylanphang.mark.USA;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.NotBlank;
/**
* 测试标记接口分组。
* 即使某一个国家提供了额外的验证信息,数据库也不会写入,只要必要信息提供即可,也只会写入必要的数据到MySQL中。
*
* @author dylan
*/
@Data
public class PersonInfo {
@NotBlank(message = "名字不能为空。", groups = {China.class, Japan.class, USA.class, Default.class})
@Length(min = 1, max = 16, message = "名字最大长度不可超过16.", groups = {China.class, Japan.class, USA.class, Default.class})
private String name;
@NotBlank(message = "身份编号不能为空。", groups = {China.class, USA.class, Default.class})
@Length(min = 18, max = 18, message = "China身份编号只能是18位的。", groups = {China.class, Default.class})
@Length(min = 12, max = 12, message = "USA身份编号只能是12位的。", groups = {USA.class})
private String id;
@Range(min = 0, max = 99, message = "非法年龄!", groups = {China.class, Japan.class, Default.class})
private Integer age;
@NotBlank(message = "兴趣没有填,快写!", groups = {Japan.class})
private String hobby;
}
China.class
:用户需提供1~16
位的用户名name
、18
位的身份证号id
、0~99
之间的年龄age
,无需提供爱好;Japan.class
:用户需提供1~16
位的用户名name
、0~99
之间的年龄age
、不为空的爱好,无需提供身份证号;USA.class
:用户需提供1~16
位的用户名name
、12
位的身份证号id
、0~99
之间的年龄age
,无需提供爱好;Default.class
与China.class
均被置于同一个groups
中。如果validate()
默认不提供校验分组时,默认需要校验的是分组则为Default.class
的字段,即等同于校验分组为China.class
的字段。Default.class
进行分组标记。validate()
要求groups
信息时,编写代码却不提供groups
的情况。validate()
时必须携带groups
信息。此时可以使用工具类包装validate()
方法,提供一个必须携带groups
信息的验证方法以供开发人员使用。ValidateInfoController
,进一步模拟当需要验证的参数来源于提交的数据时,应该如何编写:package cn.dylanphang.controller;
import cn.dylanphang.mark.supers.Country;
import cn.dylanphang.pojo.PersonInfo;
import cn.dylanphang.util.CountryUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;
/**
* @author dylan
*/
@Controller
@RestController
public class ValidateInfoController {
@RequestMapping("/validate/{country}")
public boolean validate(@PathVariable("country") String country, PersonInfo person) {
final Class<? extends Country> countryClass = CountryUtils.getCountry(country);
if (countryClass == null) {
return false;
}
final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
final Validator validator = validatorFactory.getValidator();
final Set<ConstraintViolation<PersonInfo>> validate = validator.validate(person, countryClass);
return validate.size() == 0;
}
}
CountryUtils
用于根据country
的字符串数据,获取相应的标记接口的类对象:package cn.dylanphang.util;
import cn.dylanphang.mark.China;
import cn.dylanphang.mark.Japan;
import cn.dylanphang.mark.USA;
import cn.dylanphang.mark.supers.Country;
import java.util.HashMap;
/**
* @author dylan
*/
public class CountryUtils {
private static final HashMap<String, Class<? extends Country>> HASH_MAP;
static {
HASH_MAP = new HashMap<>();
HASH_MAP.put("CHINA", China.class);
HASH_MAP.put("JAPAN", Japan.class);
HASH_MAP.put("USA", USA.class);
}
public static Class<? extends Country> getCountry(String country) {
return country == null ? null : HASH_MAP.get(country.toUpperCase());
}
}
package cn.dylanphang.controller;
import cn.dylanphang.pojo.PersonInfo;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class ValidateInfoControllerTest {
private ValidateInfoController validateInfoController;
private PersonInfo person;
@Before
public void init() {
// *.测试为了免去启动SpringBoot,直接使用new关键字创建ValidateInfoController对象,目的是调用其validate方法
this.validateInfoController = new ValidateInfoController();
this.person = new PersonInfo();
}
@Test
public void testChina() {
this.person.setName("dylan");
this.person.setId("442648399474623047");
this.person.setAge(18);
boolean result = this.validateInfoController.validate("china", this.person);
Assert.assertTrue(result);
this.person.setId("648204658392");
result = this.validateInfoController.validate("china", this.person);
Assert.assertFalse(result);
}
@Test
public void testJapan() {
this.person.setName("dylan");
this.person.setAge(18);
this.person.setHobby("coding.");
boolean result = this.validateInfoController.validate("japan", this.person);
Assert.assertTrue(result);
this.person.setHobby(null);
result = this.validateInfoController.validate("japan", this.person);
Assert.assertFalse(result);
}
@Test
public void testUSA() {
this.person.setName("dylan");
this.person.setId("648204658392");
boolean result = this.validateInfoController.validate("usa", this.person);
Assert.assertTrue(result);
this.person.setName(null);
result = this.validateInfoController.validate("usa", this.person);
Assert.assertFalse(result);
}
@Test
public void test() {
boolean result = this.validateInfoController.validate("", this.person);
Assert.assertFalse(result);
result = this.validateInfoController.validate(null, this.person);
Assert.assertFalse(result);
this.person.setName("dylan");
this.person.setId("442648399474623047");
this.person.setAge(18);
result = this.validateInfoController.validate("china", this.person);
Assert.assertTrue(result);
}
}
groups
信息,可以完成分组校验的操作。groups
属性信息,同时在校验对象时,一并携带groups
信息。validate()
中如果未携带任何校验分组groups
信息,程序将自动赋予一个默认的Default.class
分组。联系客服