用好@Validated 和 @Valid,讓校驗(yàn)飛起來(lái)

@Validated 和 @Valid簡(jiǎn)單介紹

  • JSR(Java Specification Requests)是Java界的重要標(biāo)準(zhǔn)熙卡;JSR又細(xì)分很多標(biāo)準(zhǔn)芒率,其中JSR303就代表Bean Validation

    • @Valid是使用Hibernate validation的時(shí)候使用
    • @Validated是只用Spring Validator校驗(yàn)機(jī)制使用
    • @Valid注解與@Validated注解功能大部分類似
    • 兩者的不同主要在于:@Valid屬于javax下的凌受,而@Validated屬于spring下
    • @Valid支持嵌套校驗(yàn)决摧、而@Validated不支持
    • @Validated支持分組,而@Valid不支持

    說(shuō)明:java的JSR303聲明了@Valid這類接口示辈,在javax包下寥茫,而Hibernate-validator對(duì)其進(jìn)行了實(shí)現(xiàn)

引入相關(guān)依賴:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

注解位置

  • @Validated:用在類、方法和方法參數(shù)上顽耳。但不能用于成員屬性(field)

  • @Valid:可以用在方法坠敷、構(gòu)造函數(shù)、方法參數(shù)和成員屬性(field)上

  • 在Controller層中射富,放在模型參數(shù)對(duì)象前膝迎。
    當(dāng)Controller層中參數(shù)是一個(gè)對(duì)象模型時(shí),只有將@Validated/@Valid直接放在該模型前胰耗,該模型內(nèi)部的字段才會(huì)被校驗(yàn)(如果有對(duì)該模型的字段進(jìn)行約束的話)限次。

    @PostMapping("/queryPage")
        public PageResult<AssistMsgVO> queryPage(@Validated @RequestBody AssistMsgPageRequest assistMsgPageRequest) {
    
            if (assistMsgPageRequest.getMsgType() != null && MsgTypeEnum.fromCode(assistMsgPageRequest.getMsgType()) == null) {
                throw new BusinessException("消息類型不正確");
            }
    
            return assistMsgService.queryPage(assistMsgPageRequest);
        }
    
    
    
    @Data
    @ApiModel(value = "AssistMsg分頁(yè)請(qǐng)求參數(shù)", description = "消息表分頁(yè)請(qǐng)求參數(shù)")
    public class AssistMsgPageRequest implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        /**
         * 頁(yè)碼
         */
        @ApiModelProperty("頁(yè)碼")
        @NotNull(message = "頁(yè)碼不能為空")
        private Long pageNo;
    
        /**
         * 分頁(yè)大小
         */
        @ApiModelProperty("分頁(yè)大小")
        @NotNull(message = "分頁(yè)大小不能為空")
        private Long pageSize;
    
        @ApiModelProperty("消息發(fā)送時(shí)間")
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        private Date msgSendTime;
    
        @ApiModelProperty("消息分類")
        @ConstraintsEnum(message = "消息類型錯(cuò)誤", targetClass = MsgTypeEnum.class,notEmpty = false)
        private Integer msgType;
    
        @ApiModelProperty("狀態(tài)")
        @ConstraintsEnum(message = "閱讀類型錯(cuò)誤", targetClass = MsgReadStatusEnum.class,notEmpty = false)
        private Integer hasReaded;
    }
    
  • 在Controller層中,放在類上柴灯。當(dāng)一些約束是直接出現(xiàn)在Controller層中的參數(shù)前時(shí)卖漫,只有將@Validated放在類上時(shí),參數(shù)前@Max(value=20L)的約束才會(huì)生效

    @RestController
    @Api(tags = "消息表")
    @RequestMapping("/assistMsg")
    @Validated
    public class AssistMsgController {
      
        @GetMapping("/unread/count")
        public Result<Integer> unreadCount(@RequestParam @Max(value=20L) Integer myParam) {
            return Result.success(assistMsgService.unreadMsgCount());
        }
    }
    

分組校驗(yàn)

  • @Validated:提供分組功能赠群,可以在參數(shù)驗(yàn)證時(shí)羊始,根據(jù)不同的分組采用不同的驗(yàn)證機(jī)制
  • @Valid:沒有分組功能

定義分組接口:

public interface IGroupA {
}
 
public interface IGroupB {
}

定義需要檢驗(yàn)的參數(shù)bean:

public class StudentBean implements Serializable{
    @NotBlank(message = "用戶名不能為空")
    private String name;
    //只在分組為IGroupB的情況下進(jìn)行驗(yàn)證
    @Min(value = 18, message = "年齡不能小于18歲", groups = {IGroupB.class})
    private Integer age;
    @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手機(jī)號(hào)格式錯(cuò)誤")
    private String phoneNum;
    @Email(message = "郵箱格式錯(cuò)誤")
    private String email;
    @MyConstraint
    private String className;

測(cè)試代碼:

檢驗(yàn)分組為IGroupA的情況

@RestController
public class CheckController {
    @PostMapping("stu")
    public String addStu(@Validated({IGroupA.class}) @RequestBody StudentBean studentBean){
        return "add student success";
    }
}

結(jié)果:這里對(duì)分組IGroupB的就沒檢驗(yàn)了

如果把測(cè)試代碼改成下面這樣

@RestController
public class CheckController {
    @PostMapping("stu")
    public String addStu(@Validated({IGroupA.class, IGroupB.class}) @RequestBody StudentBean studentBean){
        return "add student success";
    }
}

結(jié)果:對(duì)IGroupB有校驗(yàn)

1、不分 配groups查描,默認(rèn)每次都要進(jìn)行驗(yàn)證

2突委、對(duì)一個(gè)參數(shù)需要多種驗(yàn)證方式時(shí),也可通過(guò)分配不同的組達(dá)到目的冬三。

組序列

默認(rèn)情況下 不同級(jí)別的約束驗(yàn)證是無(wú)序的匀油,但是在一些情況下,順序驗(yàn)證卻是很重要勾笆。

一個(gè)組可以定義為其他組的序列敌蚜,使用它進(jìn)行驗(yàn)證的時(shí)候必須符合該序列規(guī)定的順序。在使用組序列驗(yàn)證的時(shí)候窝爪,如果序列前邊的組驗(yàn)證失敗弛车,則后面的組將不再給予驗(yàn)證。

舉例:

定義組序列:

@GroupSequence({Default.class, IGroupA.class, IGroupB.class})
public interface IGroup {
}

需要校驗(yàn)的Bean蒲每,分別定義IGroupA對(duì)age進(jìn)行校驗(yàn)帅韧,IGroupB對(duì)className進(jìn)行校驗(yàn):

public class StudentBean implements Serializable{
    @NotBlank(message = "用戶名不能為空")
    private String name;
    @Min(value = 18, message = "年齡不能小于18歲", groups = IGroupA.class)
    private Integer age;
    @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手機(jī)號(hào)格式錯(cuò)誤")
    private String phoneNum;
    @Email(message = "郵箱格式錯(cuò)誤")
    private String email;
    @MyConstraint(groups = IGroupB.class)
    private String className;

測(cè)試代碼:

@RestController
public class CheckController {
    @PostMapping("stu")
    public String addStu(@Validated({IGroup.class}) @RequestBody StudentBean studentBean){
        return "add student success";
    }
}

結(jié)果:如果age出錯(cuò),那么對(duì)組序列在IGroupA后的IGroupB不進(jìn)行校驗(yàn)啃勉,即例子中的className不進(jìn)行校驗(yàn)

嵌套校驗(yàn)@Valid忽舟,@Validated不支持嵌套校驗(yàn)

一個(gè)待驗(yàn)證的pojo類,其中還包含了待驗(yàn)證的對(duì)象,需要在待驗(yàn)證對(duì)象上注解@Valid叮阅,才能驗(yàn)證待驗(yàn)證對(duì)象中的成員屬性刁品,這里不能使用@Validated

舉例:

需要約束校驗(yàn)的bean:

public class TeacherBean {
    @NotEmpty(message = "老師姓名不能為空")
    private String teacherName;
    @Min(value = 1, message = "學(xué)科類型從1開始計(jì)算")
    private int type;
public class StudentBean implements Serializable{
    @NotBlank(message = "用戶名不能為空")
    private String name;
    @Min(value = 18, message = "年齡不能小于18歲")
    private Integer age;
    @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手機(jī)號(hào)格式錯(cuò)誤")
    private String phoneNum;
    @Email(message = "郵箱格式錯(cuò)誤")
    private String email;
    @MyConstraint
    private String className;
 
    @NotNull(message = "任課老師不能為空")
    @Size(min = 1, message = "至少有一個(gè)老師")
    private List<TeacherBean> teacherBeans;

注意:

結(jié)果:這里對(duì)teacherBeans只校驗(yàn)了NotNull, 和 Size浩姥,并沒有對(duì)teacher信息里面的字段進(jìn)行校驗(yàn)

這里teacher中的type明顯是不符合約束要求的挑随,但是能檢測(cè)通過(guò),是因?yàn)樵趕tudent中并沒有做 嵌套校驗(yàn)

可以在teacherBeans中加上 @Valid勒叠,具體如下:

@Valid
@NotNull(message = "任課老師不能為空")
@Size(min = 1, message = "至少有一個(gè)老師")
private List<TeacherBean> teacherBeans;

自定義校驗(yàn)

第一步:創(chuàng)建自定義注解

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy ={EnumValidator.class})
public @interface ConstraintsEnum {

    String message() default " enums Validate error!";
    boolean notEmpty() default true;
    Class<?> targetClass() default Class.class;
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

message兜挨、payload、groups是必須要寫的眯分,還需要什么方法可根據(jù)自己的實(shí)際業(yè)務(wù)需求

第二步:編寫(第一步中的校驗(yàn)器實(shí)現(xiàn)類)該注解

package com.mdkw.likang.common.annotation;

import lombok.extern.slf4j.Slf4j;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.reflect.Method;
import java.util.Objects;

@Slf4j
public class EnumValidator implements ConstraintValidator<ConstraintsEnum,Object> {

    private final static String METHOD_NAME = "getCode";
    private ConstraintsEnum annotation;

    @Override
    public void initialize(ConstraintsEnum constraintAnnotation) {
        this.annotation = constraintAnnotation;
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
        Class<?> cls = annotation.targetClass();
        if(cls.isEnum() && !annotation.notEmpty()){
            return true;
        }
        if(cls.isEnum() && null == value){
            return false;
        }
        boolean result = false;
        Object[] object = cls.getEnumConstants();
        try {
            Method method = cls.getMethod(METHOD_NAME);
            for (Object o : object) {
                Object code = method.invoke(o);
                if(Objects.deepEquals(code,value)){
                    result = true;
                    break;
                }
            }
        } catch (Exception e) {
            log.error("error:",e);
        }
        return result;
    }
}

第三步:使用該注解

package com.mdkw.likang.request;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.mdkw.likang.common.annotation.ConstraintsEnum;
import com.mdkw.likang.enums.MsgReadStatusEnum;
import com.mdkw.likang.enums.MsgTypeEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;

@Data
@ApiModel(value = "AssistMsg分頁(yè)請(qǐng)求參數(shù)", description = "消息表分頁(yè)請(qǐng)求參數(shù)")
public class AssistMsgPageRequest implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 頁(yè)碼
     */
    @ApiModelProperty("頁(yè)碼")
    @NotNull(message = "頁(yè)碼不能為空")
    private Long pageNo;

    /**
     * 分頁(yè)大小
     */
    @ApiModelProperty("分頁(yè)大小")
    @NotNull(message = "分頁(yè)大小不能為空")
    private Long pageSize;

    @ApiModelProperty("消息發(fā)送時(shí)間")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date msgSendTime;

    @ApiModelProperty("消息分類")
    @ConstraintsEnum(message = "消息類型錯(cuò)誤", targetClass = MsgTypeEnum.class,notEmpty = false)
    private Integer msgType;

}

校驗(yàn)錯(cuò)誤統(tǒng)一處理

  • 使用BindingResult類來(lái)容納異常信息拌汇,當(dāng)校驗(yàn)不通過(guò)時(shí),不影響正常程序往下走弊决。我們只需要處理BindingResult中的異常信息即可
@ApiOperation(value = "分頁(yè)查詢")
    @PostMapping("/queryPage")
    public PageResult<AssistMsgVO> queryPage(@Validated @RequestBody AssistMsgPageRequest assistMsgPageRequest, BindingResult bindingResult) {

        if(bindingResult.hasErrors()){
            for (ObjectError allError : bindingResult.getAllErrors()) {
                System.out.println(allError.getDefaultMessage());
            }
        }
        if (assistMsgPageRequest.getMsgType() != null && MsgTypeEnum.fromCode(assistMsgPageRequest.getMsgType()) == null) {
            throw new BusinessException("消息類型不正確");
        }

        return assistMsgService.queryPage(assistMsgPageRequest);
    }
  • 如果不采用BindingResult來(lái)容納異常信息時(shí)噪舀,那么異常會(huì)被向外拋出。注解校驗(yàn)不通過(guò)時(shí)飘诗,可能拋出的異常有BindException異常与倡、ValidationException異常(或其子類異常)、MethodArgumentNotValidException異常昆稿。

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
 
    /**
     * 處理Validated校驗(yàn)異常
     * <p>
     * 注: 常見的ConstraintViolationException異常纺座, 也屬于ValidationException異常
     *
     * @param e
     *         捕獲到的異常
     * @return 返回給前端的data
     */
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = {BindException.class, ValidationException.class, MethodArgumentNotValidException.class})
    public Map<String, Object> handleParameterVerificationException(Exception e) {
        log.error(" handleParameterVerificationException has been invoked", e);
        Map<String, Object> resultMap = new HashMap<>(4);
        resultMap.put("code", "100001");
        String msg = null;
        if (e instanceof MethodArgumentNotValidException) {
            BindingResult bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();
            // getFieldError獲取的是第一個(gè)不合法的參數(shù)(P.S.如果有多個(gè)參數(shù)不合法的話)
            FieldError fieldError = bindingResult.getFieldError();
            if (fieldError != null) {
                msg = fieldError.getDefaultMessage();
            }
        } else if (e instanceof BindException) {
            // getFieldError獲取的是第一個(gè)不合法的參數(shù)(P.S.如果有多個(gè)參數(shù)不合法的話)
            FieldError fieldError = ((BindException) e).getFieldError();
            if (fieldError != null) {
                msg = fieldError.getDefaultMessage();
            }
        } else if (e instanceof ConstraintViolationException) {
            /*
             * ConstraintViolationException的e.getMessage()形如
             *     {方法名}.{參數(shù)名}: {message}
             *  這里只需要取后面的message即可
             */
            msg = e.getMessage();
            if (msg != null) {
                int lastIndex = msg.lastIndexOf(':');
                if (lastIndex >= 0) {
                    msg = msg.substring(lastIndex + 1).trim();
                }
            }
            /// ValidationException 的其它子類異常
        } else {
            msg = "處理參數(shù)時(shí)異常";
        }
        resultMap.put("msg", msg);
        return resultMap;
    }
    
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市溉潭,隨后出現(xiàn)的幾起案子净响,更是在濱河造成了極大的恐慌,老刑警劉巖岛抄,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件别惦,死亡現(xiàn)場(chǎng)離奇詭異狈茉,居然都是意外死亡夫椭,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門氯庆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蹭秋,“玉大人,你說(shuō)我怎么就攤上這事堤撵∪侍郑” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵实昨,是天一觀的道長(zhǎng)洞豁。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么丈挟? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任刁卜,我火速辦了婚禮,結(jié)果婚禮上曙咽,老公的妹妹穿的比我還像新娘蛔趴。我一直安慰自己,他們只是感情好例朱,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布孝情。 她就那樣靜靜地躺著,像睡著了一般洒嗤。 火紅的嫁衣襯著肌膚如雪箫荡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天烁竭,我揣著相機(jī)與錄音菲茬,去河邊找鬼。 笑死派撕,一個(gè)胖子當(dāng)著我的面吹牛婉弹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播终吼,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼镀赌,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了际跪?” 一聲冷哼從身側(cè)響起商佛,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎姆打,沒想到半個(gè)月后良姆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡幔戏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年玛追,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片闲延。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡痊剖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出垒玲,到底是詐尸還是另有隱情陆馁,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布合愈,位于F島的核電站叮贩,受9級(jí)特大地震影響击狮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜益老,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一帘不、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧杨箭,春花似錦寞焙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至慈参,卻和暖如春呛牲,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驮配。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工娘扩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人壮锻。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓琐旁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親猜绣。 傳聞我的和親對(duì)象是個(gè)殘疾皇子灰殴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容