JSR303校验的简单使用以及自定义校验规则的代码编写

JSR303校验的简单使用以及自定义校验规则的代码编写,第1张

文章目录
  • 一、JSR303校验
      • 1、简介
      • 2、相关注解
      • 3、JSR303依赖包
  • 二、JSR303自带的校验规则
      • 1、在JavaBean上添加校验规则
      • 2、生效校验规则
          • 2.1 controller返回的通用实体类R
          • 2.2 方法一:在controller的方法中生效校验
          • 2.3 方法二:写一个异常处理类
      • 2、使用group校验
          • 2.1 分组校验
          • 2.1 @Validated与@Valid
          • 2.2 定义接口,充当标识
          • 2.3 测试
      • 3、其他的校验规则
          • 3.1 组序列
          • 3.2 级联验证
  • 三、自定义校验规则
      • 1、编写自定义注解
      • 2、编写配置文件ValidationMessages.properties
      • 3、测试


环境:
IDEA: 2021
JDK: 1.8
Spring-Boot-dependencies: 2.2.5.RELEASE
mysql: 5.7
mybatis-plus-boot-starter: 3.2.0

一、JSR303校验 1、简介

参数校验是程序开发中必不可少的步骤。用户在前端页面上填写表单时,前端js程序会校验参数的合法性,当数据到了后端,为了防止恶意 *** 作,保持程序的健壮性,后端同样需要对数据进行校验。后端参数校验最简单的做法是直接在业务方法里面进行判断,当判断成功之后再继续往下执行。但这样带给我们的是代码的耦合,冗余。当我们多个地方需要校验时,我们就需要在每一个地方调用校验程序,导致代码很冗余,且不美观。
那么如何优雅的对参数进行校验呢?JSR303就是为了解决这个问题出现的。

2、相关注解

JSR303 是一套JavaBean参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们JavaBean的属性上面,就可以在需要校验的时候进行校验了。

注解如下:

注解说明
@NotNull注解元素必须是非空
@NotBlank注解元素不能是空格并且至少包含一个字符
@NotEmpty注解元素不能为null或空
@Email该字符串必须是格式正确的电子邮件地址。
@Null注解元素必须是空
@Digits带注释的元素必须是可接受范围内的数字
@Future带注释的元素必须是未来的瞬间、日期或时间。
@FutureOrPresent注释元素必须是当前或未来的瞬间、日期或时间。
@Past带注释的元素必须是过去的瞬间、日期或时间。
@PastOrPresent带注释的元素必须是过去或现在的瞬间、日期或时间。
@Max带注释的元素必须是一个数字,其值必须小于或等于指定的最大值。
@Min带注释的元素必须是一个数字,其值必须大于或等于指定的最小值。
@Pattern带注释的 {@code CharSequence} 必须匹配指定的正则表达式。正则表达式遵循 Java 正则表达式约定
@Size验证元素大小是否在指定范围内
@DecimalMax带注释的元素必须是一个数字,其值必须小于或等于指定的最大值。
@DecimalMin带注释的元素必须是一个数字,其值必须大于或等于指定的最小值。
@AssertTrue被注释的元素必须为true
@AssertFalse被注释的元素必须为false
@Positive被注解的元素必须是整数
@PositiveOrZero被注解元素必须是正数或0

Hibernate validator 在JSR303的基础上对校验注解进行了扩展,hibernate-validator官方文档

扩展注解如下:

例举几个:

注解说明
@URL被注释的元素必须是合法的URL
@Length被注释的字符串的大小必须在指定的范围内
@Range被注释的元素必须在合适的范围内
3、JSR303依赖包
        
        <dependency>
            <groupId>javax.validationgroupId>
            <artifactId>validation-apiartifactId>
            <version>2.0.1.Finalversion>
        dependency>
        
        
        <dependency>
            <groupId>org.hibernate.validatorgroupId>
            <artifactId>hibernate-validatorartifactId>
            <version>6.0.17.Finalversion>
        dependency>
二、JSR303自带的校验规则 1、在JavaBean上添加校验规则
package com.zhuang.mall.product.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.*;

/**
 * 品牌
 * 
 * @author mrzhuang
 * @email [email protected]
 * @date 2022-04-11 13:56:19
 */
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 品牌id
	 */
	@TableId
	private Long brandId;
	/**
	 * 品牌名
	 */
	@NotBlank(message = "品牌名必须填写!")
	private String name;
	/**
	 * 品牌logo地址
	 */
	@NotEmpty(message = "logo url必须填写!")
	@URL(message = "必须是合法的url!")
	private String logo;
	/**
	 * 介绍
	 */
	private String descript;
	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	private Integer showStatus;
	/**
	 * 检索首字母
	 */
	@NotEmpty(message = "首字母必须填写!")
	// 正则表达式不能写为"/^[a-zA-Z]$/"
	@Pattern(regexp = "^[a-zA-Z]$" ,  message = "检索首字母必须是一个字母!")
	private String firstLetter;
	/**
	 * 排序
	 */
	@NotNull(message = "排序必须填写!")
	@Min(value = 0, message = "排序必须是大于等于0的整数!")
	private Integer sort;

}


}
2、生效校验规则 2.1 controller返回的通用实体类R
/**
 * Copyright (c) 2016-2019 人人开源 All rights reserved.
 *
 * https://www.renren.io
 *
 * 版权所有,侵权必究!
 */

package com.zhuang.common.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.apache.http.HttpStatus;

import java.util.HashMap;
import java.util.Map;

/**
 * @author mrzhuang
 * @date 2022/4/25 8:12 PM
 */
public class R extends HashMap<String, Object> {
	private static final long serialVersionUID = 1L;
	
	public R() {
		put("code", HttpStatus.SC_OK);
		put("msg", "success");
	}

	public R setData(Object data){
		put("data",data);
		return this;
	}

	public <T> T getData(String key,TypeReference<T> typeReference){
		Object data = get(key);
		String s = JSON.toJSONString(data);
		T t = JSON.parseObject(s, typeReference);
		return t;
	}

	public <T> T getData(TypeReference<T> typeReference){
		Object data = get("data");
		String s = JSON.toJSONString(data);
		T t = JSON.parseObject(s, typeReference);
		return t;
	}
	
	public static R error() {
		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
	}
	
	public static R error(String msg) {
		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
	}
	
	public static R error(int code, String msg) {
		R r = new R();
		r.put("code", code);
		r.put("msg", msg);
		return r;
	}

	public static R ok(String msg) {
		R r = new R();
		r.put("msg", msg);
		return r;
	}
	
	public static R ok(Map<String, Object> map) {
		R r = new R();
		r.putAll(map);
		return r;
	}
	
	public static R ok() {
		return new R();
	}

	public R put(String key, Object value) {
		super.put(key, value);
		return this;
	}

	public Integer getCode(){
		return (Integer) this.get("code");
	}
}

2.2 方法一:在controller的方法中生效校验
package com.zhuang.mall.product.controller;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.zhuang.mall.product.entity.BrandEntity;
import com.zhuang.mall.product.service.BrandService;
import com.zhuang.common.utils.PageUtils;
import com.zhuang.common.utils.R;

import javax.naming.Binding;
import javax.validation.Valid;


/**
 * 品牌
 * @author mrzhuang
 * @email [email protected]
 * @date 2022-04-11 13:56:19
 */
@RestController
@RequestMapping("product/brand")
public class BrandController {
    @Autowired
    private BrandService brandService;

    /**
     * 保存
     */
    //@Valid告诉spring mvc上传的数据需要校验,brandEntity中的@NotBlank规则就会生效!
    //BindingResult会获取到错误的信息结果
    @RequestMapping("/save")
    public R save(@RequestBody @Valid BrandEntity brand, BindingResult result){
        //判断result中是否有错误
        if (result.hasErrors()) {
            Map<String, String > map = new HashMap<>();
            //获取到校验的错误结果
            result.getFieldErrors().forEach((item)->{
                //FiledError 获取到错误提示
                String message = item.getDefaultMessage();
                //获取错误的属性的名字
                String field = item.getField();
                map.put(field, message);
            });
            return R.error(400, "提交的数据不合法").put("data",map);
        }else {
            brandService.save(brand);
            return R.ok();
        }
    }

测试:
使用Postman进行接口的测试。
错误的输入测试:
1、传入空的jason字符串:

{}

2、传入不合校验规则的Jason字符串:

{"name":"","logo":"123","sort":-1,"firstLetter":"ab"}

正确的输入测试:
传入的jason字符串为:

{"name":"小米","logo":"http://www.kaotop.com/file/tupian/20220428/fd039245d688d43f394f6821381ed21b0ff43b7b.jpg","sort":0,"firstLetter":"A"}
2.3 方法二:写一个异常处理类

对于第一种方法,代码的冗余度比较高。使用统一的异常处理类降低冗余、方便。

1、创建通用的异常枚举类
mall-common/src/main/java/com/zhuang/common/exception/BizCodeEnum.java

package com.zhuang.common.exception;

/**
 * @author mrzhuang
 * @date 2022/4/25 5:25 PM 
 */
public enum BizCodeEnum {
    UNKNOWN_EXCEPTION(10000,"系统未知错误"),
    VALID_EXCEPTION(10001,"参数校验异常"),

    private Integer code;
    private String msg;
    //枚举类必有的私有构造器
    private BizCodeEnum(Integer code,String msg){
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

2、创建异常类
mall-product/src/main/java/com/zhuang/mall/product/exception/MallExceptionControllerAdvice.java

package com.zhuang.gulimall.product.exception;

import com.zhuang.common.exception.BizCodeEnum;
import com.zhuang.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

/**
 * @author mrzhuang
 * @date 2022/4/24 8:54 PM
 * description:集中处理所有的异常
 */
@Slf4j
@RestControllerAdvice(basePackages = "com.zhuang.gulimall.product.controller")
public class MallExceptionControllerAdvice {

    //指定处理的异常
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R handleValidException(MethodArgumentNotValidException e) {
        log.error("数据校验出现问题{},异常类型: {}",e.getMessage(),e.getClass());
        BindingResult bindingResult = e.getBindingResult();
        Map<String, String> errorMap = new HashMap<>();
        bindingResult.getFieldErrors().forEach((fieldError -> {
            errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
        }));
        return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(),BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data",errorMap);
    }
    //处理任意类型的异常,将异常抛出
    @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable throwable) {
        log.error("错误:", throwable);
        return R.error(BizCodeEnum.UNKNOWN_EXCEPTION.getCode(),BizCodeEnum.UNKNOWN_EXCEPTION.getMsg());
    }
}

3、还原初始controller类中的方法
方法中的@Valid注解需要加上!!!

/**
 * 品牌
 * @author mrzhuang
 * @email [email protected]
 * @date 2022-04-11 13:56:19
 */
@RestController
@RequestMapping("product/brand")
public class BrandController {
    @Autowired
    private BrandService brandService;

    /**
     * 保存
     */
    //@Valid告诉spring mvc上传的数据需要校验,brandEntity中的@NotBlank规则就会生效!
    //BindingResult会获取到错误的信息结果
    @RequestMapping("/save")
    public R save(@RequestBody @Valid BrandEntity brand){
        
            brandService.save(brand);
            return R.ok();
    }

4、测试
输入错误的jason字符串:

{"name":"","logo":"123","sort":-1,"firstLetter":"xx"}

输入正确的jason字符串:

{"name":"小米","logo":"http://www.kaotop.com/file/tupian/20220428/fd039245d688d43f394f6821381ed21b0ff43b7b.jpg","sort":0,"showStatus":"0","firstLetter":"x"}
2、使用group校验 2.1 分组校验

分组校验可以将JavaBean的某个属性值进行分组,比如在进行更新 *** 作或者添加 *** 作的时候需要进行校验;

对于brangId我们需要在添加 *** 作的时候,可以不需要branId,因为采用的是自增长策略,因此不需要有brandId,这时需要校验必须为空值。而对于更新 *** 作的时候,我们需要brandId,这时需要校验是否是空值。

2.1 @Validated与@Valid

@Validated与@Valid的比较

@Validated:
Spring提供的
支持分组校验
可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
由于无法加在成员属性(字段)上,所以无法单独完成级联校验,需要配合@Valid
@Valid:
JDK提供的(标准JSR-303规范)
不支持分组校验
可以用在方法、构造函数、方法参数和成员属性(字段)上
可以加在成员属性(字段)上,能够独自完成级联校验

2.2 定义接口,充当标识

1、添加AddGroupUpdateGroup接口

package com.zhuang.common.valid;
/**
 * @author mrzhuang
 * @date 2022/4/25 9:18 PM 
 */
public interface AddGroup {
}
package com.zhuang.common.valid;
/**
 * @author mrzhuang
 * @date 2022/4/25 9:18 PM 
 */
public interface UpdateGroup {
}

2、给属性上添加分组

	/**
	 * 品牌id
	 */
	@NotNull(message = "更新 *** 作必须填写brandId!", groups = {UpdateGroup.class})
	@Null(message = "添加 *** 作必须为空值!", groups = {AddGroup.class})
	@TableId
	private Long brandId;

3、controller类中的方法
添加 *** 作

    @RequestMapping("/save")
    public R save(@RequestBody @Validated({AddGroup.class}) BrandEntity brand){

            brandService.save(brand);
            return R.ok();
    }

更新 *** 作

   @RequestMapping("/update")
 public R update(@RequestBody @Validated({UpdateGroup.class}) BrandEntity brand){
 	brandService.updateById(brand);

     return R.ok();
 }
2.3 测试

添加 *** 作:
输入的jason字符串:

{"brandId":"2000","name":"","logo":"123","sort":-1,"firstLetter":"xx"}

结果:

{
    "msg": "参数校验异常",
    "code": 10001,
    "data": {
        "brandId": "添加 *** 作必须为空值!"
    }
}

更新 *** 作:
输入的jason字符串:

{"brandId":"","name":"","logo":"123","sort":-1,"firstLetter":"xx"}
结果:
{
    "msg": "参数校验异常",
    "code": 10001,
    "data": {
        "brandId": "更新 *** 作必须填写brandId!"
    }
}

注意:
从上面结果中看出@Validated注解无法校验属性上定义的规则!,而且当方法中同时使用@Validated与@Valid时,只会生效左边的第一个,也就是谁靠左就生效谁!!!
原因是:
没有加上默认分组 Default.class
解决方法:
1、@Validated({UpdateGroup.class)中的分组中加上默认分组
@Validated({AddGroup.class,Default.class})
@Validated({UpdateGroup.class, Default.class})
2、在写的充当标识的接口时继承默认分组
public interface AddGroup extends Default{
}
public interface UpdateGroup extends Default{
}

修改后的方法:
添加 *** 作

    @RequestMapping("/save")
    public R save(@RequestBody @Validated({AddGroup.class,Default.class}) BrandEntity brand){

            brandService.save(brand);
            return R.ok();
    }

更新 *** 作

   @RequestMapping("/update")
 public R update(@RequestBody @Validated({UpdateGroup.class, Default.class}) BrandEntity brand){
 	brandService.updateById(brand);

     return R.ok();
 }
3、其他的校验规则 3.1 组序列

指定组与组之间的检验顺序,如果第一个组校验没过,就不会校验后面的组

@GroupSequence({UpdateGroup.class, AddGroup.class, Default.class})
public interface DefaultGroupSequence {
}

注意: 需要重新自定义一个接口。

3.2 级联验证

一个待验证的JavaBean类,其中又包含了一个待验证的对象。
需要在待验证的对象属性上加@Valid注解

三、自定义校验规则
	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	private Integer showStatus;

我们需要在showStatus属性上添加一个自定义校验规则@ListValue:输入的值只能为0或1

1、编写自定义注解

创建自定义注解ListValue,可以参考JSR303已有的注解的内容。
mall-common/src/main/java/com/zhuang/common/valid/ListValue.java

package com.zhuang.common.valid;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

/**
 * @author mrzhuang
 * @date 2022/4/26 10:23 AM
 */
@Documented
//约束校验方式
@Constraint(
        validatedBy = {ListValueConstraintValidator.class}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListValue {
    String message() default "{com.chenxin.gulimail.common.valid.ListValue.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
	//定义的值数组
    int[] vals() default {};

}

我们还需要指定校验方式ListValueConstraintValidator:校验我们输入的值是否符合要求

@Constraint(
        validatedBy = {ListValueConstraintValidator.class}
)

创建ListValueConstraintValidator类,mall-common/src/main/java/com/zhuang/common/valid/ListValueConstraintValidator.java

查看validatedBy源码,可以看出validatedBy中的元素是ConstraintValidator的子类。

@Documented
@Target({ ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface Constraint {

	/**
	 * {@link ConstraintValidator} classes implementing the constraint. The given classes
	 * must reference distinct target types for a given {@link ValidationTarget}. If two
	 * {@code ConstraintValidator}s refer to the same type, an exception will occur.
	 * 

* At most one {@code ConstraintValidator} targeting the array of parameters of * methods or constructors (aka cross-parameter) is accepted. If two or more * are present, an exception will occur. * * @return array of {@code ConstraintValidator} classes implementing the constraint */ Class<? extends ConstraintValidator<?, ?>>[] validatedBy(); }

因此,我们写ListValueConstraintValidator类时,需要实现ConstraintValidator

查看ConstraintValidator源码:

public interface ConstraintValidator<A extends Annotation, T> {

	/**
	 * Initializes the validator in preparation for
	 * {@link #isValid(Object, ConstraintValidatorContext)} calls.
	 * The constraint annotation for a given constraint declaration
	 * is passed.
	 * 

* This method is guaranteed to be called before any use of this instance for * validation. *

* The default implementation is a no-op. * * @param constraintAnnotation annotation instance for a given constraint declaration */ default void initialize(A constraintAnnotation) { } /** * Implements the validation logic. * The state of {@code value} must not be altered. *

* This method can be accessed concurrently, thread-safety must be ensured * by the implementation. * * @param value object to validate * @param context context in which the constraint is evaluated * * @return {@code false} if {@code value} does not pass the constraint */ boolean isValid(T value, ConstraintValidatorContext context); }

从上可以看出,ConstraintValidator中的泛型A为ListValue注解,T为获得的数据值value。

因此ListValueConstraintValidator类的内容为:

package com.zhuang.common.valid;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;

/**
 * @author mrzhuang
 * @date 2022/4/26 10:23 AM
 */
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
    Set<Integer> set = new HashSet<Integer>();
    /**
     * 初始化
     * @param constraintAnnotation
     */
    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] vals = constraintAnnotation.vals();
        for (int val : vals) {
            set.add(val);
        }
    }

    /**
     * 真正的校验规则
     * 判断是否校验成功
     * @param integer
     * @param constraintValidatorContext
     * @return
     */
    @Override
    public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
        //判断输入的值是否是规定的值
        return set.contains(integer);
    }
}
2、编写配置文件ValidationMessages.properties

编写配置文件,输出message信息
mall-common/src/main/resources/ValidationMessages.properties
内容为:

com.zhuang.common.valid.ListValue.message=必须提交指定的值
3、测试

1、加上自定义的@ListValue注解

	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	@ListValue(vals = {0, 1})
	private Integer showStatus;

2、输入的jason字符串:

	{"brandId":"","name":"小米","logo":"http://www.kaotop.com/file/tupian/20220428/fd039245d688d43f394f6821381ed21b0ff43b7b.jpg","sort":1,"showStatus":"2","firstLetter":"x"}

showStatus为2时,不是指定的值!校验错误!
结果:

{
    "msg": "参数校验异常",
    "code": 10001,
    "data": {
        "showStatus": "必须提交指定的值"
    }
}

当设置showStatus为0或者1时,校验成功!

{
    "msg": "success",
    "code": 200
}

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/758027.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-01
下一篇 2022-05-01

发表评论

登录后才能评论

评论列表(0条)

保存