文章目录
- 一、关于JSR
- 1.1什么是JSR?
- 1.2什么是JSR-303?
- 1.3JSR-303的作用?
- 二、从if判断到jsr303
- 2.1提供一个场景
- 2.1首先使用if判断
- 2.3jsr303使用
- 三、jsr303相关校验注解导图展示
- 四、@Valid和@Validated的区别
- 五、@Valid和@Validated高级使用
- 5.1Valid级联校验
- 5.2@Validated分组校验
- 5.2.1什么是分组校验
- 5.2.2.引出jsr303校验注解的groups方法
- 5.2.3.@Validated注解value方法开启指定分组校验
- 5.2.4.检验结果
- 5.2.5.结论
- 5.3@Validated分组校验顺序
- 5.3.1为什么需要?
- 5.3.2如何实现?
- 5.4@Validated非实体类校验
- 5.4.1.使用@Validated注解
- 5.4.2.GlobalExceptionHandler类添加异常处理方法
- 5.4.3测试结果
- 六、自定义注解校验
- 6.1自定义注解@ListValue
- 6.2使用自定义注解
- 6.3请求方法
- 6.4测试结果
- 七、补充@PathVariable注解校验
- 7.1@PathVariable作用
- 7.2使用正则表达式校验路径变量
- 7.3自定义类实现ErrorController接口
- 7.4测试结果
- 八、导图总结
一、关于JSR
1.1什么是JSR?
JSR是Java Specification Requests的缩写,意思是Java规范提案,是指向JCP(Java Community Rrocess)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。
比如:
Web应用技术
- Java Servlet 3.0 (JSR 315)
- JavaServer Faces 2.0 (JSR 314)
- JavaServer Pages 2.2/Expression Language 2.2 (JSR 245)
- Standard Tag Library for JavaServer Pages (JSTL) 1.2 (JSR 52)
- Debugging Support for Other Languages 1.0 (JSR 45)
1.2什么是JSR-303?
JSR-303是javaee 6中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation的参考实现. Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint,目前Springboot内置了Hibernate Validator。
1.3JSR-303的作用?
用于对Java Bean的字段值进行校验,确保输入进来的数据在语义上是正确的,使验证逻辑从业务代码中脱离出来。
JSR303是运行时数据验证框架,验证之后验证的错误信息会马上返回。
二、从if判断到jsr303
2.1提供一个场景
提供一个实体类,该实体类含有一个属性
@Data
public class ExceptionQuery {
private String userName;
}
要求用户提供的姓名不能为空,下面开始校验该属性
2.1首先使用if判断
@PostMapping("/query")
public Result getResult(@RequestBody ExceptionQuery query){
//校验参数
if (StringUtils.isEmpty(query.getUserName())){
//抛出异常
}
return Result.buildSuccess(query);
}
这个方法本省是没有问题的,但是如果入参有很多属性需要校验,那么程序中就会存在很多if判断,这样显的代码很冗余。有没有方法替代if判断呢?当然是有的,使用jsr303可以解决该问题
2.3jsr303使用
1.为属性加@NotBlank属性
@Data
public class ExceptionQuery {
@NotBlank(message = "用户姓名必须提交")
private String userName;
}
注:像@NotBlank一类的校验注解都可以重写message属性
public @interface NotBlank {
//default表明可以重写message属性
String message() default "{javax.validation.constraints.NotBlank.message}";
//省略...
}
2.在方法入参参数上加@Valid注解
@PostMapping("/query")
public Result getResult(@Valid @RequestBody ExceptionQuery query){
return Result.buildSuccess(query);
}
3.postman测试结果
从输出结果上可以看到重写后的message属性,而对于错误的输出结果Spring也提供了对应类存储错误信息,这个类就是BindingResult,将上面的方法加上BindingResult参数,如下代码
@PostMapping("/query")
public Result getResult(@Valid @RequestBody ExceptionQuery query, BindingResult result){
if(result.hasErrors()){
Map<String,String> map = new HashMap<>();
result.getFieldErrors().forEach(item->{
//获取发生错误时的message
String message = item.getDefaultMessage();
//获取发生错误的字段
String field = item.getField();
map.put(field,message);
System.out.println(field+":"+message);
});
return Result.buildFailure(400,"参数错误",map);
}
return Result.buildSuccess(query);
}
输出结果
然而,咱们不能对每个方法都传入一个BindingResult参数,可以使用统一异常处理输出错误信息
4.配合异常处理
首先说下为什么能通过异常处理输出错误信息?
我们定位到一个类MethodArgumentNotValidException,该类也是Spring提供的,包含BindingResult引用,因此该类可以拿到输出的错误信息
异常处理代码
@Slf4j
@RestControllerAdvice(annotations = {RestController.class, Controller.class})
public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidationException(MethodArgumentNotValidException e) {
log.error(ErrorStatus.ILLEGAL_DATA.getMessage() + ":" + e.getMessage());
Map<String,Object> map=new HashMap<>();
//拿到异常输出信息
BindingResult bindingResult = e.getBindingResult();
bindingResult.getFieldErrors().forEach(fieldError -> {
String message = fieldError.getDefaultMessage();
String field = fieldError.getField();
map.put(field,message);
});
return Result.buildFailure(ErrorStatus.ILLEGAL_DATA.getCode(),String.valueOf(getFirstOrNull(map)),map);
}
//拿到map第一个值
private static Object getFirstOrNull(Map<String, Object> map) {
Object obj = null;
for (Map.Entry<String, Object> entry : map.entrySet()) {
obj = entry.getValue();
if (obj != null) {
break;
}
}
return obj;
}
}
加上异常处理后的方法代码如下所示,关于@ControllerAdvice全局异常处理请看这一篇博客
@PostMapping("/query")
public Result getResult(@Valid @RequestBody ExceptionQuery query){
return Result.buildSuccess(query);
}
是不是很简洁,下面看下输出结果
三、jsr303相关校验注解导图展示
注意:@Validated和@Valid注解都是开启注解校验功能的注解
下面举个例子说明一下
阐述基本用法可能不是那么适合,到实际应用改下就行
输出结果
四、@Valid和@Validated的区别
上述例子咱们用的是@Valid进行校验的,用@Validated注解也可以,那么他两的区别是什么呢?
所属不同:
该注解所属包为:javax.validation.Valid,而 @Validated是Spring基于@Valid进一步封装,并提供了一些高级功能,如分组,分组顺序等使用位置不同:
@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性上
@Valid:可以用在方法、构造函数、方法参数和成员属性上
正是由于@Valid能用于成员属性(字段)上,因此@Valid可以提供级联校验,关于级联校验下面就要阐述
五、@Valid和@Validated高级使用
5.1Valid级联校验
级联校验也叫嵌套检测,嵌套即一个实体类包含另一个实体类
举个例子
下面有个People实体类
该类还有两个属性,一个是名字,一个头发,因为一个人有很多头发(暂不考虑光头哈哈),Hair实体类如下
下面写一个方法检测下用户数据
在方法参数上用@Validated或者@Valid都可以
@PostMapping("/add")
public Result getPeople(@Validated @RequestBody People people){
return Result.buildSuccess(people);
}
测试结果如下
暴露一个问题
没有输出Hair实体类对数据的校验结果,可以证明@Validated和@Valid加在方法参数前,都不会对嵌套的实体类Hair进行检测
实现检测
为了能够完成对嵌套实体类的检测,我们需要在属性上使用@Vaild注解
看下此时的输出结果
5.2@Validated分组校验
5.2.1什么是分组校验
分组校验是由@Validated的value方法提供的,用于开启指定的组校验,分别作用不同的业务场景中,下面举个例子说明下
5.2.2.引出jsr303校验注解的groups方法
假如使用Mybatis做持久化框架(其他也可以),我们知道当向数据库插入一条数据时,这条数据id是自动生成,不用用户传入的,而当我们修改一条数据时,id需要用户传入,因此在修改操作时需要对id进行校验。像以上修改和插入对id不同的操作,这个时候就要使用groups方法对id进行分组,如下图所示
为了涵盖多种情况,加入了两个属性,personName属性在AddGroup.class,UpdateGroup.class时都会校验,personAge属性在不指定分组时校验
5.2.3.@Validated注解value方法开启指定分组校验
5.2.4.检验结果
下面使用一样的数据请求三个方法,得到的结果如下
模拟数据,该数据都不符合要求
{
"personId":"",
"personName":"",
"personAge":0
}
getPerson方法不指定分组
校验了@Range(min=1,max=400,message="年龄提交有误"),该注解不含groups方法
{
"success": false,
"code": 10003,
"msg": "年龄提交有误",
"data": {
"personAge": "年龄提交有误"
}
}
addPerson方法指定AddGroup分组
校验了@NotBlank(message = "名字不能为空",groups = {AddGroup.class,UpdateGroup.class})
{
"success": false,
"code": 10003,
"msg": "名字不能为空",
"data": {
"personName": "名字不能为空"
}
}
updatePerson方法指定UpdateGroup分组
校验了@NotBlank(message = "id不能为空",groups = UpdateGroup.class)和
@NotBlank(message = "名字不能为空",groups = {AddGroup.class,UpdateGroup.class})
{
"success": false,
"code": 10003,
"msg": "名字不能为空",
"data": {
"personName": "名字不能为空",
"personId": "id不能为空"
}
}
5.2.5.结论
如果校验注解添加上groups方法并指定分组,只有@Validated注解value方法指定该分组,才会开启校验注解的校验数据功能。
同样的如果校验注解没有groups指定分组,则@Validated注解value方法为默认分组时才会开启
5.3@Validated分组校验顺序
5.3.1为什么需要?
默认情况下,分组间的约束验证是无序的,然而某些情况下分组间的校验顺序却很重要,比如第二组约束验证依赖于第一组稳定状态来进行,这个时候需要分组按照顺序校验。
5.3.2如何实现?
分组校验顺序由@GroupSequence注解实现
举个例子
1.列出分组顺序
public interface First {}
public interface Second {}
//此时first、second顺序校验
@GroupSequence({First.class,Second.class})
public interface Group {}
2.实体类及请求方法
@Data
public class UserGroupSequence {
@NotEmpty(message = "id不能为空",groups = First.class)
private String userId;
@NotEmpty(message = "姓名不能空",groups = First.class)
@Size(min = 3,max = 8,message = "姓名长度在3到8之间",groups = Second.class)
private String userName;
}
@RestController
@RequestMapping("/group")
public class UserGroupSequenceController {
//此时用的是value方法指定的是Group接口
@PostMapping("/add")
public Result addGroup(@Validated(value = Group.class) @RequestBody UserGroupSequence sequence){
return Result.buildSuccess(sequence);
}
}
3.测试结果
模拟数据
{
"userId":"",
"userName":""
}
结果
{
"success": false,
"code": 10003,
"msg": "姓名不能空",
"data": {
"userName": "姓名不能空",
"userId": "id不能为空"
}
}
4.结论
@GroupSequence注解指定分组顺序时,如果First标识的校验注解没有通过,则Second标识的注解不会生效。
读者可以测试下,如果把userName上两个校验注解groups去掉,输出结果是"姓名不能空"及"姓名长度在3到8之间"交替出现
5.4@Validated非实体类校验
上面校验都是对实体类的校验,下面来介绍下对非实体类的校验
5.4.1.使用@Validated注解
这里特别要注意一点就是@Validated注解对于非实体类的校验,在类上注解才会起效果
@Validated
public class AnnotationController {
@GetMapping("/getage")
public Result getAge(@Range(min = 3,max = 8,message = "年龄在3-8岁") @RequestParam String age){
return Result.buildSuccess(age);
}
}
5.4.2.GlobalExceptionHandler类添加异常处理方法
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public Result resolveConstraintViolationException(ConstraintViolationException ex){
Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
//对异常信息进行处理
if(!CollectionUtils.isEmpty(constraintViolations)){
StringBuilder msgBuilder = new StringBuilder();
for(ConstraintViolation constraintViolation :constraintViolations){
msgBuilder.append(constraintViolation.getMessage()).append(",");
}
String errorMessage = msgBuilder.toString();
if(errorMessage.length()>1){
errorMessage = errorMessage.substring(0,errorMessage.length()-1);
}
return Result.buildFailure(ErrorStatus.ILLEGAL_DATA.getCode(),errorMessage);
}
return Result.buildFailure(ErrorStatus.ILLEGAL_DATA.getCode(),ex.getMessage());
}
5.4.3测试结果
http://localhost:8082/annotation/getage?age=1
{
"success": false,
"code": 10003,
"msg": "年龄在3-8岁",
"data": null
}
六、自定义注解校验
提供一个场景,假如一个字段只能让用户传入特定的值,比如判断是否显示的属性isShow,只能取0和1,下面我们实现这一个功能
6.1自定义注解@ListValue
@Documented
//该注解由哪个类校验
@Constraint(validatedBy = {ListValue.ListValueConstraintValidator.class})
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface ListValue {
String message() default "{com.thinkcoder.annotation.ListValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default { };
int[] value();
class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set=new HashSet<>();
//获取value属性指定的数字,存入set集合
@Override
public void initialize(ListValue constraintAnnotation) {
int[] value = constraintAnnotation.value();
for (int i : value) {
set.add(i);
}
}
//校验用户输入数据是否在set集合中
//isValid第一个参数传入要校验属性的类型
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
}
上面的代码需要说明以下几点
- 校验类要实现ConstraintValidator<ListValue,Integer>接口;第一个泛型参数是自定义注解,第二个泛型参数是要校验的属性类型
- initalize方法:获取到用户使用自定义注解中的数据
- isValid方法:实现校验逻辑,结果是返回boolean类型
- @Constraint注解:将自定义注解和校验类联系起来
6.2使用自定义注解
@Data
public class AnnotationQuery {
@ListValue(value = {0,1},message = "数值只能是0或者1")
private Integer isShow;
}
6.3请求方法
@PostMapping("/add")
public Result addAnnotation(@Validated @RequestBody AnnotationQuery query){
return Result.buildSuccess(query);
}
6.4测试结果
模拟数据
{
"isShow":-1
}
测试结果
{
"success": false,
"code": 10003,
"msg": "数值只能是0或者1",
"data": {
"isShow": "数值只能是0或者1"
}
}
七、补充@PathVariable注解校验
7.1@PathVariable作用
用来对指定请求的URL路径里面的变量,比如@GetMapping("/get/{id}"),其中{id}就是这个请求的变量,可以通过@PathVariable来获取。
和@RequestParam的区别是,@RequestParam用来获得静态的URL请求入参。
7.2使用正则表达式校验路径变量
下面正则表达式表示id值只能是数字,如果不是数字报出404路径找不到的异常
@GetMapping("/get/{id:\\d+}")
public Result getId(@PathVariable(name="id") String userId){
return Result.buildSuccess(userId);
}
7.3自定义类实现ErrorController接口
说下为什么要这么做?
@ControllerAdive注解只能处理进入控制器方法抛出的异常,ErrorController接口可以处理全局异常,而404路径找不到异常不是控制器方法抛出的,此时还没有进入控制器方法。ErrorController处理404异常时会跳转到/error路径,此时会返回错误的html页。为了让返回结果统一,重写下ErrorController接口的getErrorPath方法
@RestController
public class MyErrorController extends BasicErrorController {
@Autowired
public MyErrorController(ErrorAttributes errorAttributes,
ServerProperties serverProperties,
List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, serverProperties.getError(), errorViewResolvers);
}
//处理html请求
@Override
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML));
ModelAndView modelAndView = new ModelAndView("myErrorPage", model, status);
return modelAndView;
}
//处理json请求
@Override
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
Map<String,Object> resultBody=new HashMap<>(16);
resultBody.put("success",false);
resultBody.put("code",body.get("status"));
resultBody.put("msg",body.get("error"));
return new ResponseEntity<>(resultBody, HttpStatus.OK);
}
}
7.4测试结果
实例:
http://localhost:8082/get/aa
结果:
{
"success": false,
"code": 404,
"msg": "请求接口不存在请检查路径",
}
八、导图总结
通过以上叙述基本包含了常用开发用到的校验注解,下面用导图总结下