springboot整合JSR303參數(shù)校驗與全局異常處理

一、前言

我們在日常開發(fā)中芭梯,避不開的就是參數(shù)校驗险耀,有人說前端不是會在表單中進(jìn)行校驗的嗎?在后端中玖喘,我們可以直接不管前端怎么樣判斷過濾甩牺,我們后端都需要進(jìn)行再次判斷,為了安全累奈。因為前端很容易拜托贬派,當(dāng)測試使用PostMan來測試急但,如果后端沒有校驗,不就亂了嗎搞乏?肯定會有很多異常的波桩。今天小編和大家一起學(xué)習(xí)一下JSR303專門用于參數(shù)校驗的,算是一個工具吧请敦!

二镐躲、JSR303簡介

JSR-303 是 JAVA EE 6 中的一項子規(guī)范,叫做 Bean Validation侍筛,官方參考實現(xiàn)是Hibernate Validator萤皂。 Hibernate Validator 提供了 JSR 303 規(guī)范中所有內(nèi)置 constraint 的實現(xiàn),除此之外還有一些附加的 constraint匣椰。

Hibernate官網(wǎng)

官網(wǎng)介紹:

驗證數(shù)據(jù)是一項常見任務(wù)裆熙,它發(fā)生在從表示層到持久層的所有應(yīng)用程序?qū)又小MǔT诿恳粚佣紝崿F(xiàn)相同的驗證邏輯禽笑,這既耗時又容易出錯入录。為了避免重復(fù)這些驗證,開發(fā)人員經(jīng)常將驗證邏輯直接捆綁到域模型中蒲每,將域類與驗證代碼混在一起纷跛,而驗證代碼實際上是關(guān)于類本身的元數(shù)據(jù)喻括。

Jakarta Bean Validation 2.0 - 為實體和方法驗證定義了元數(shù)據(jù)模型和 API邀杏。默認(rèn)元數(shù)據(jù)源是注釋,能夠通過使用 XML 覆蓋和擴展元數(shù)據(jù)唬血。API 不依賴于特定的應(yīng)用程序?qū)踊蚓幊棠P屯K貏e不依賴于 Web 或持久層,并且可用于服務(wù)器端應(yīng)用程序編程以及富客戶端 Swing 應(yīng)用程序開發(fā)人員拷恨。


三脖律、導(dǎo)入依賴

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

四、常用注解

約束注解名稱 約束注解說明
@Null 用于驗證對象為null
@NotNull 用于對象不能為null腕侄,無法查檢長度為0的字符串
@NotBlank 只用于String類型上小泉,不能為null且trim()之后的size>0
@NotEmpty 用于集合類、String類不能為null,且size>0冕杠。但是帶有空格的字符串校驗不出來
@Size 用于對象(Array,Collection,Map,String)長度是否在給定的范圍之內(nèi)
@Length 用于String對象的大小必須在指定的范圍內(nèi)
@Pattern 用于String對象是否符合正則表達(dá)式的規(guī)則
@Email 用于String對象是否符合郵箱格式
@Min 用于Number和String對象是否大等于指定的值
@Max 用于Number和String對象是否小等于指定的值
@AssertTrue 用于Boolean對象是否為true
@AssertFalse 用于Boolean對象是否為false

所有的大家參考jar包

五微姊、@Validated、@Valid區(qū)別

@Validated:

  • Spring提供的
  • 支持分組校驗
  • 可以用在類型分预、方法和方法參數(shù)上兢交。但是不能用在成員屬性(字段)上
  • 由于無法加在成員屬性(字段)上,所以無法單獨完成級聯(lián)校驗笼痹,需要配合@Valid

@Valid:

  • JDK提供的(標(biāo)準(zhǔn)JSR-303規(guī)范)
  • 不支持分組校驗
  • 可以用在方法配喳、構(gòu)造函數(shù)酪穿、方法參數(shù)和成員屬性(字段)上
  • 可以加在成員屬性(字段)上,能夠獨自完成級聯(lián)校驗

總結(jié):@Validated用到分組時使用晴裹,一個學(xué)校對象里還有很多個學(xué)生對象需要使用@Validated在Controller方法參數(shù)前加上被济,@Valid加在學(xué)校中的學(xué)生屬性上,不加則無法對學(xué)生對象里的屬性進(jìn)行校驗涧团!

例子:

@Data
public class School{

    @NotBlank
    private String id;
    private String name;
    @Valid                // 需要加上溉潭,否則不會驗證student類中的校驗注解
    @NotNull              // 且需要觸發(fā)該字段的驗證才會進(jìn)行嵌套驗證。
    private List<Student> list;
}

@Data
public class Student {

    @NotBlank
    private String id;
    private String name;
    private int age;

}

@PostMapping("/test")
public Result test(@Validated @RequestBody School school){

}

六少欺、常用使用測試

1. 實體類添加校驗

import lombok.Data;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.io.Serializable;

@Data
public class BrandEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 品牌id
     */
    @NotNull(message = "修改必須有品牌id")
    private Long brandId;
    /**
     * 品牌名F
     */
    @NotBlank(message = "品牌名必須提交")
    private String name;
    /**
     * 品牌logo地址
     */
    @NotBlank(message = "地址必須不為空")
    private String logo;
    /**
     * 介紹
     */
    private String descript;

    /**
     * 檢索首字母
     */
    //正則表達(dá)式
    @Pattern(regexp = "^[a-zA-Z]$",message = "檢索的首字母必須是字母")
    private String firstLetter;
    /**
     * 排序
     */
    @Min(value = 0,message = "排序必須大于等于0")
    private Integer sort;

}

2. 統(tǒng)一返回類型

import com.alibaba.druid.util.StringUtils;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

//統(tǒng)一返回結(jié)果
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel
public class Result<T> {
    @ApiModelProperty("響應(yīng)碼")
    private Integer code;
    @ApiModelProperty("相應(yīng)信息")
    private String msg;
    @ApiModelProperty("返回對象或者集合")
    private T data;

    //成功碼
    public static final Integer SUCCESS_CODE = 200;
    //成功消息
    public static final String SUCCESS_MSG = "SUCCESS";

    //失敗
    public static final Integer ERROR_CODE = 201;
    public static final String ERROR_MSG = "系統(tǒng)異常,請聯(lián)系管理員";
    //沒有權(quán)限的響應(yīng)碼
    public static final Integer NO_AUTH_COOD = 999;

    //執(zhí)行成功
    public static <T> Result<T> success(T data){
        return new Result<>(SUCCESS_CODE,SUCCESS_MSG,data);
    }
    //執(zhí)行失敗
    public static <T> Result failed(String msg){
        msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg;
        return new Result(ERROR_CODE,msg,"");
    }
    //傳入錯誤碼的方法
    public static <T> Result failed(int code,String msg){
        msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg;
        return new Result(code,msg,"");
    }
    //傳入錯誤碼的數(shù)據(jù)
    public static <T> Result failed(int code,String msg,T data){
        msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg;
        return new Result(code,msg,data);
    }
}

3. 測試類

@PostMapping("/add")
public Result add(@Valid @RequestBody BrandEntity brandEntity)  {

    return Result.success("成功");
}

==遇到的坑==:小編在公司的項目中添加沒什么問題喳瓣,但是就是無法觸發(fā)校驗,看到的是Springboot版本太高了赞别,所有要添加下面的依賴才觸發(fā)畏陕。

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.18.Final</version>
</dependency>

4. 普通測試結(jié)果

5. 我們把異常返回給頁面

@PostMapping("/add")
public Result add(@Valid @RequestBody BrandEntity brandEntity, BindingResult bindingResult){

    if (bindingResult.hasErrors()){
        Map<String,String> map = new HashMap<>();
        bindingResult.getFieldErrors().forEach(item ->{
            map.put(item.getField(),item.getDefaultMessage());
        });
        return Result.failed(400,"提交的數(shù)據(jù)不合規(guī)范",map);
    }

    return Result.success("成功");
}

6. 異常處理結(jié)果

{
    "code": 400,
    "data": {
        "name": "品牌名必須提交",
        "logo": "地址必須不為空"
    },
    "msg": "提交的數(shù)據(jù)不合規(guī)范"
}

七、抽離全局異常處理

1. 心得體會

上面我們要在每個校驗的接口上面寫仿滔,所以我們要抽離出來做個全局異常惠毁。并且要改進(jìn)一下,原來的是把錯誤信息放到data里崎页,但是正常情況下的data是返回給前端的數(shù)據(jù)鞠绰。我們這樣把異常數(shù)據(jù)放進(jìn)去,會使data的數(shù)據(jù)有二義性飒焦。這樣對于前端就不知道里面是數(shù)據(jù)還是報錯信息了哈蜈膨,這樣就可以直接前端展示msg里面的提示即可!

2. 書寫ExceptionControllerAdvice

import com.wang.test.demo.response.Result;
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;

@Slf4j
@RestControllerAdvice(basePackages = "com.wang.test.demo.controller")
public class ExceptionControllerAdvice {

    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result handleVaildException(MethodArgumentNotValidException e){

        log.error("數(shù)據(jù)校驗出現(xiàn)問題:{}牺荠,異常類型:{}",e.getMessage(),e.getClass());
        BindingResult bindingResult = e.getBindingResult();
        StringBuffer stringBuffer = new StringBuffer();
        bindingResult.getFieldErrors().forEach(item ->{
            //獲取錯誤信息
            String message = item.getDefaultMessage();
            //獲取錯誤的屬性名字
            String field = item.getField();
            stringBuffer.append(field + ":" + message + " ");
        });
        return Result.failed(400, stringBuffer + "");

    }

    @ExceptionHandler(value = Throwable.class)
    public Result handleException(Throwable throwable){

        log.error("錯誤",throwable);
        return Result.failed(400, "系統(tǒng)異常");
    }
}

3. 測試結(jié)果

{
    "code": 400,
    "data": "",
    "msg": "logo:地址必須不為空 name:品牌名必須提交 "
}

八翁巍、分組校驗

1. 需求

我們在做校驗的時候,通常會遇到一個實體類的添加和修改休雌,他們的校驗規(guī)則是不同的灶壶,所以分組顯得尤為重要。他可以幫助我們少建一個冗余的實體類杈曲,所以我們必須要會的驰凛。

2. 創(chuàng)建分組接口(不需寫任何內(nèi)容)

public interface EditGroup {
}
public interface AddGroup {
}

3. 在需要二義性的字段上添加分組

/**
 * 品牌id
 */
@NotNull(message = "修改必須有品牌id",groups = {EditGroup.class})
@Null(message = "新增不能指定id",groups = {AddGroup.class})
private Long brandId;
// 其余屬性我們不變

4. 不同Controller添加校驗規(guī)則

注意:我們要進(jìn)行分組,所以@Valid不能使用了担扑,要使用@Validated恰响。相信大家已經(jīng)看到上面的他倆區(qū)別了哈!

@PostMapping("/add")
public Result add(@Validated({AddGroup.class}) @RequestBody BrandEntity brandEntity){

    return Result.success("成功");
}

@PostMapping("/edit")
public Result edit(@Validated({EditGroup.class}) @RequestBody BrandEntity brandEntity){

    return Result.success("成功");
}

5. 測試


九魁亦、自定義校驗

1.定義自定義校驗器

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

//編寫自定義的校驗器
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {

    private Set<Integer> set=new HashSet<Integer>();

    //初始化方法
    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] value = constraintAnnotation.vals();
        for (int i : value) {
            set.add(i);
        }
    }
    /**
     * 判斷是否校驗成功
     * @param value  需要校驗的值
     * @param context
     * @return
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return  set.contains(value);
    }
}

2. 定義一個注解配合校驗器使用

@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
    // 使用該屬性去Validation.properties中取
    String message() default "{com.atguigu.common.valid.ListValue.message}";

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

    Class<? extends Payload>[] payload() default { };

    int[] vals() default {};

}

3. 實體類添加一個新的校驗屬性

==注意==:我們上面做了分組渔隶,如果屬性不指定分組,則不會生效,現(xiàn)在我們的部分屬性校驗已沒有起作用间唉,現(xiàn)在只有brandId和showStatus起作用绞灼。

/**
 * 顯示狀態(tài)[0-不顯示;1-顯示]
 */
@NotNull(groups = {AddGroup.class, EditGroup.class})
@ListValue(vals = {0,1},groups = {AddGroup.class, EditGroup.class},message = "必須為0或者1")
private Integer showStatus;

4. 測試


十呈野、總結(jié)

這樣就差不多對JSR303有了基本了解低矮,滿足基本開發(fā)沒有什么問題哈!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末被冒,一起剝皮案震驚了整個濱河市军掂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌昨悼,老刑警劉巖蝗锥,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異率触,居然都是意外死亡终议,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進(jìn)店門葱蝗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來穴张,“玉大人,你說我怎么就攤上這事两曼≡砀剩” “怎么了?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵悼凑,是天一觀的道長偿枕。 經(jīng)常有香客問我,道長佛析,這世上最難降的妖魔是什么益老? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任彪蓬,我火速辦了婚禮寸莫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘档冬。我一直安慰自己膘茎,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布酷誓。 她就那樣靜靜地躺著披坏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪盐数。 梳的紋絲不亂的頭發(fā)上棒拂,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天,我揣著相機與錄音,去河邊找鬼帚屉。 笑死谜诫,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的攻旦。 我是一名探鬼主播喻旷,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼牢屋!你這毒婦竟也來了且预?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤烙无,失蹤者是張志新(化名)和其女友劉穎锋谐,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體截酷,經(jīng)...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡怀估,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了合搅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片多搀。...
    茶點故事閱讀 40,852評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖灾部,靈堂內(nèi)的尸體忽然破棺而出康铭,到底是詐尸還是另有隱情,我是刑警寧澤赌髓,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布从藤,位于F島的核電站,受9級特大地震影響锁蠕,放射性物質(zhì)發(fā)生泄漏夷野。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一荣倾、第九天 我趴在偏房一處隱蔽的房頂上張望悯搔。 院中可真熱鬧,春花似錦舌仍、人聲如沸妒貌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽灌曙。三九已至,卻和暖如春节芥,著一層夾襖步出監(jiān)牢的瞬間在刺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蚣驼,地道東北人忍燥。 一個月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像隙姿,于是被迫代替她去往敵國和親梅垄。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,851評論 2 361

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