參數校驗Jakarta Bean Validation學習


 

1.背景

我們在平時的學習與工作中,都需要對參數進行校驗,比如在注冊時,用戶名密碼不能為空,用戶名長度必須小於10等等。雖然有些校驗在前端頁面會進行驗證,但是后端為了增加健壯性也需要對這些參數進行判斷(比如繞過前端頁面而直接調用了接口,參數的合法性未知),可能就會在controller或者service中就會有如下代碼的出現

package com.beemo.validation.controller;

import com.beemo.validation.demo1.entity.Student;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.Objects;

@RestController
public class DemoController {

    @RequestMapping("/demo")
    public String saveDemo(@RequestBody Student student) {
        if (StringUtils.isEmpty(student.getName())) {
            return "學生名稱不能為空";
        }
        if (student.getName().length() > 10) {
            return "學生名稱長度不能超過10位";
        }
        if (Objects.isNull(student.getAge())) {
            return "學生年齡不能為空";
        }
        if (student.getAge() <= 0) {
            return "學生年齡不能為負數";
        }
        if (Objects.isNull(student.getNumber())) {
            return "學號不能為空";
        }
        if (student.getNumber().length() != 10) {
            return "學號長度必須為10";
        }
        // 其他判斷
        // 調用service的方法等
        return "ok";
    }

    @Data
    class Student {
        /**
         * 姓名
         */
        private String name;
    
        /**
         * 年齡
         */
        private Integer age;
    
        /**
         * 學號
         */
        private String number;
    }
}

 

從例子中可以看到,這僅僅是一個實體類3個字段的簡單驗證,就已經占據了很多的篇幅,也需要我們進行手動編寫這種判斷代碼,比較費時,代碼讀起來也沒什么營養,大部分都是在判斷合法性,等我們真正讀到想要的業務邏輯代碼可能需要往下翻好久,那么有沒有辦法能夠讓我們更簡潔更優雅的去驗證這些參數呢

2. Jakarta Bean Validation

2.1 Jakarta Bean Validation簡介

首先要知道Jakarta就是Java更名之后的名稱,Jakarta Bean Validation也就是Java Bean Validation,是一套Java的規范,它可以

通過使用注解的方式在對象模型上表達約束
以擴展的方式編寫自定義約束
提供了用於驗證對象和對象圖的API
提供了用於驗證方法和構造方法的參數和返回值的API
報告違反約定的集合
運行在Java SE,並且集成在Jakarta EE8中
例如:

 public class User {

    private String email;

    @NotNull @Email
    public String getEmail() {
      return email;
    }

    public void setEmail(String email) {
      this.email = email;
    }
}

public class UserService {
    public void createUser(@Email String email, @NotNull String name) {
        ...
    }
}

雖然可以手動運行校驗,但更加自然的做法是讓其他規則和框架在適時對數據進行校驗(用戶在表示框架中進行輸入,業務服務通過CDI執行,實體通過JPA插入或者更新)
換句話說,即運行一次,到處約束

2.2 相關網址

在2020年2月份已經發布了3.0.0-M1版本
其中Jakarta Bean Validation只是一套標准,我們需要使用其他組織機構提供的實現來進行驗證,官方支持的為Hibernate Validator

3. 動手實踐

3.1 所需環境

這里JDK使用了JDK1.8,使用maven進行所需jar文件依賴,使用springboot搭建框架腳手架,使用lombok簡化代碼
如果用的不是這幾個可以適當修改,大同小異,而且springboot以及或其他依賴的版本每天都在變化,各個版本之間難免有或多或少的差別,可能細節處與本文章有所不同,需要大家知曉,並且根據自己的版本進行調整(比如spring-boot-starter-parent版本2.2.7與2.3.0在驗證異常時返回json格式與內容就有很大不同)

3.2 搭建空框架

  • 使用spring initializr創建springboot項目,依次選擇添加webvalidation以及lombok模塊,生成的pom.xml依賴如下。我這里spring-boot-starter-parent的版本為2.3.0,再添加其他所需的pom依賴
...
<!-- spring-boot版本 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
...
<!-- web模塊 -->
<dependency>
     <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 驗證模塊,hibernate-validation -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!- lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
</dependency>
<!-- guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>29.0-jre</version>
</dependency>

3.3 編寫代碼

編寫背景:模擬英雄聯盟游戲的技能與英雄的保存

這里的命名遵循外服名稱而不是國服直譯,例如英雄為champion而不是hero,技能為ability而不是skill

3.3.1 實體類

  • 英雄
package com.beemo.validation.demo2.entity;

import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
 * 英雄entity
 */
@Data
public class Champion {

    /**
     * 英雄名稱
     */
    @NotBlank(message = "英雄名稱不能為空")
    private String name;

    /**
     * 英雄頭銜
     */
    @NotBlank(message = "英雄頭銜不能為空")
    private String title;

    /**
     * 英雄描述
     */
    @NotBlank(message = "英雄描述不能為空")
    private String description;

    /**
     * 英雄類型
     * 坦克、刺客、射手、法師、輔助以及戰士
     */
    @NotNull(message = "英雄類型不能為空")
    private Byte type;
}
  • 技能entity
package com.beemo.validation.demo2.entity;

import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
 * 技能
 */
@Data
public class Ability {

    /**
     * 技能名稱
     */
    @NotBlank(message = "技能名稱不能為空")
    private String name;

    /**
     * 技能描述
     */
    @NotBlank(message = "技能描述不能為空")
    private String description;

    /**
     * 技能類型
     * 例如魔法值、怒氣、能量等
     */
    @NotNull(message = "技能類型不能為空")
    private Byte type;
}

3.3.2 控制層

  • 英雄controller
package com.beemo.validation.demo2.controller;

import com.beemo.validation.demo2.entity.Champion;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@RestController
@RequestMapping("/demo2/champion")
@Validated
public class ChampionController {

    /**
     * 保存
     * @param entity 要保存的英雄實體
     * @return 保存結果
     */
    @PostMapping("save")
    public String save(@Valid @RequestBody Champion entity) {
        // 調用service等
        return "ok";
    }
}
  • 技能controller
package com.beemo.validation.demo2.controller;

import com.beemo.validation.demo2.entity.Ability;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;


@RestController
@RequestMapping("/demo2/ability")
@Validated
public class AbilityController {

    /**
     * 保存
     * @param entity 要保存的技能實體
     * @return 保存結果
     */
    @PostMapping("save")
    public String save(@Valid @RequestBody Ability entity) {
        // 調用service等
        return "ok";
    }
}

3.3.3 測試

使用postman或其他工具發送POST請求,進行驗證,我們直接輸入我們參數直接傳一個內容為空的json,查看結果

 

 

 可以看到,這里返回了400異常,意為參數錯誤
我們再把所有參數補全,再試一下

 

 

 

可以看到,如果我們把參數補全之后,返回的是“ok”,即進入controller執行該方法。
那么,例子中添加的幾個注解都是什么意思,有什么作用,而且注解中寫的message信息在驗證后並沒有輸出,那么我們怎么樣輸出這些message呢

4. 注解含義

4.1 開啟驗證

首先我們看controller類最上方,我們標注了@Validataed(這里可以去掉,不用在控制類上添加該注解),該注解的含義是:這個類要啟用參數校驗。在save方法的參數中標注了@Valid,含義為我們要對緊跟的實體進行校驗,而具體校驗的內容,為實體類中的我們的定義的約束
以Ability類舉例,在name字段上方標記了@NotBlank,意為定義了該字段不允許為空的約束,如果name為空,校驗就不通過,就會返回我們之前碰到的400異常。而type字段也標注了@NotNull,也定義了該字段不允許為空的約束,具體的區別以及其他內置的約束如3.5所示

4.2 內置約束

內置約束位於javax.validation.constraints包內,列表如下

4.2.1 @Null

  • 被標注元素必須為null
  • 接收任意類型

比如在創建一個英雄時,ID需要由數據庫自增生成,而不是我們自定義,那么該我們在接收前台傳遞的json時就必須為空

4.2.2 @NotNull

  • 被標注元素必須不為null
  • 接收任意類型

定義一個字段不能為空,例如技能類型或者英雄名稱

4.2.3 @AssertTrue

  • 被標注元素必須true
  • 支持的類型為boolean以及Boolean
  • null被認為是有效的

要么為null,否則必須為true

4.2.4 @AssertFalse

  • 被標注元素必須false
  • 支持的類型為boolean以及Boolean
  • null被認為是有效的

要么為null,否則必須為false

4.2.5 @Min

  • 被標注元素必須為是一個數字,其值必須大於等於指定的最小值
  • 支持的類型為BigDecimal 、BigInteger 、byte 、short 、int 、long 以及各自的包裝類
  • 注意double以及float由於舍入錯誤而不被支持
  • null被認為是有效的

4.2.6 @Max

  • 被標注元素必須為是一個數字,其值必須小於等於指定的最大值
  • 支持的類型為BigDecimal 、BigInteger 、byte 、short 、int 、long 以及各自的包裝類
  • 注意double以及float由於舍入錯誤而不被支持
  • null被認為是有效的

4.2.7 @DecimalMin

  • 被標注元素必須為是一個數字,其值必須大於等於指定的最小值
  • 支持的類型為BigDecimal 、BigInteger 、CharSequencebyte 、short 、int 、long 以及各自的包裝類
  • 注意double以及float由於舍入錯誤而不被支持
  • null被認為是有效的

4.2.8 @DecimalMax

  • 被標注元素必須為是一個數字,其值必須小於等於指定的最大值
  • 支持的類型為BigDecimal 、BigInteger 、CharSequencebyte 、short 、int 、long 以及各自的包裝類
  • 注意double以及float由於舍入錯誤而不被支持
  • null被認為是有效的

4.2.9 @Negative

  • 被標注元素必須為是一個嚴格意義上的負數(即0被認為是無效的)
  • 支持的類型為BigDecimal 、BigIntegerbyte 、short 、int 、long 、floatdouble以及各自的包裝類
  • null被認為是有效的

4.2.10 @NegativeOrZero

  • 被標注元素必須為是負數或者0
  • 支持的類型為BigDecimal 、BigIntegerbyte 、short 、int 、long 、floatdouble以及各自的包裝類
  • null被認為是有效的

4.2.11 @Positive

  • 被標注元素必須為是一個嚴格意義上的正數(即0被認為是無效的)
  • 支持的類型為BigDecimal 、BigIntegerbyte 、short 、int 、long 、floatdouble以及各自的包裝類
  • null被認為是有效的

4.2.12 @PositiveOrZero

  • 被標注元素必須為是正數或者0
  • 支持的類型為BigDecimal 、BigIntegerbyte 、short 、int 、long 、floatdouble以及各自的包裝類
  • null被認為是有效的

4.2.13 @Size

  • 被標注元素的大小必須在指定的邊界區間
  • 支持的類型為CharSequence(計算字符序列的長度) 、Collection(計算集合的大小)、Map(計算map的大小) 、Array(計算數組的長度)
  • null被認為是有效的

4.2.14 @Digits

  • 被標注元素必須是在可接受范圍內的數字
  • 支持的類型為BigDecimal 、BigIntegerCharSequencebyte 、short 、int 、long 以及各自的包裝類
  • null被認為是有效的

4.2.15 @Past

  • 被標注元素必須是過去的某個時刻、日期或者時間
  • “現在”的概念是附加在Validator或者ValidatorFactory中的ClockProvider定義的,默認的ClockProvider根據虛擬機定義了當前時間,如果需要的話,會應用當前默認時區
  • 支持的類型為java.util.Date 、java.util.Calendar、java.time.Instant、java.time.LocalDate 、java.time.LocalDateTime 、java.time.LocalTime} 、java.time.MonthDay 、java.time.OffsetDateTime 、java.time.OffsetTime 、java.time.Year 、java.time.YearMonth 、java.time.ZonedDateTime 、java.time.chrono.HijrahDate 、java.time.chrono.JapaneseDate 、java.time.chrono.MinguoDate、java.time.chrono.ThaiBuddhistDate 以及各自的包裝類

  • null被認為是有效的

4.2.16 @PastOrPresent

  • 被標注元素必須是過去或現在的某個時刻、日期或者時間
  • “現在”的概念是附加在Validator或者ValidatorFactory中的ClockProvider定義的,默認的ClockProvider根據虛擬機定義了當前時間,如果需要的話,會應用當前默認時區
  • “現在”的概念相對的定義在使用的約束上,例如,如果約束在Year上,那么現在表示當前年份
  • 支持的類型為java.util.Date 、java.util.Calendar、java.time.Instant、java.time.LocalDate 、java.time.LocalDateTime 、java.time.LocalTime} 、java.time.MonthDay 、java.time.OffsetDateTime 、java.time.OffsetTime 、java.time.Year 、java.time.YearMonth 、java.time.ZonedDateTime 、java.time.chrono.HijrahDate 、java.time.chrono.JapaneseDate 、java.time.chrono.MinguoDate、java.time.chrono.ThaiBuddhistDate 以及各自的包裝類

  • null被認為是有效的

4.2.17 @Future

  • 被標注元素必須是未來的某個時刻、日期或者時間
  • “現在”的概念是附加在Validator或者ValidatorFactory中的ClockProvider定義的,默認的ClockProvider根據虛擬機定義了當前時間,如果需要的話,會應用當前默認時區
  • 支持的類型為java.util.Date 、java.util.Calendar、java.time.Instant、java.time.LocalDate 、java.time.LocalDateTime 、java.time.LocalTime} 、java.time.MonthDay 、java.time.OffsetDateTime 、java.time.OffsetTime 、java.time.Year 、java.time.YearMonth 、java.time.ZonedDateTime 、java.time.chrono.HijrahDate 、java.time.chrono.JapaneseDate 、java.time.chrono.MinguoDate、java.time.chrono.ThaiBuddhistDate 以及各自的包裝類

  • null被認為是有效的

4.2.18 @FutureOrPresent

  • 被標注元素必須是未來或現在的某個時刻、日期或者時間
  • “現在”的概念是附加在Validator或者ValidatorFactory中的ClockProvider定義的,默認的ClockProvider根據虛擬機定義了當前時間,如果需要的話,會應用當前默認時區
  • “現在”的概念相對的定義在使用的約束上,例如,如果約束在Year上,那么現在表示當前年份
  • 支持的類型為java.util.Date 、java.util.Calendar、java.time.Instant、java.time.LocalDate 、java.time.LocalDateTime 、java.time.LocalTime} 、java.time.MonthDay 、java.time.OffsetDateTime 、java.time.OffsetTime 、java.time.Year 、java.time.YearMonth 、java.time.ZonedDateTime 、java.time.chrono.HijrahDate 、java.time.chrono.JapaneseDate 、java.time.chrono.MinguoDate、java.time.chrono.ThaiBuddhistDate 以及各自的包裝類

  • null被認為是有效的

4.2.19 @Pattern

  • 被標注的CharSequence必須匹配指定的正則表達式,該正則表達式遵循Java的正則表達式規定
  • 支持的類型為CharSequence
  • null被認為是有效的

4.2.20 @NotEmpty

  • 被標注元素必須不為null或者空(以字符串舉例,不為null並且不為“”)
  • 支持的類型為CharSequence(計算字符序列的長度) 、Collection(計算集合的大小)、Map(計算map的大小) 、Array(計算數組的長度)

4.2.21 @NotBlank

  • 被標注元素必須不為null,並且必須包含至少一個非空格的字符
  • 支持的類型為CharSequence

4.2.22 @Email

  • 字符串必須是格式良好的電子郵件地址
  • 支持的類型為CharSequence

5. 異常模塊

還有一個問題,就是我們定義的message沒有生效,比如“技能名稱不能為空”,並沒有出現在返回結果中,取而代之的是400異常,那么怎樣才能返回我們想要的message呢
首先我們在controller當中定一個一個方法,用@ExceptionHandler注解標注一下,用來獲取controller拋出的異常,然后我們跟蹤一下斷點,看一下到底是什么異常

package com.beemo.validation.demo2.controller;

import com.beemo.validation.demo2.entity.Ability;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.ConstraintViolationException;
import javax.validation.Valid;


@RestController
@RequestMapping("/demo2/ability")
@Validated
public class AbilityController {

    /**
     * 保存
     * @param entity 要保存的技能實體
     * @return 保存結果
     */
    @PostMapping("save")
    public String save(@Valid @RequestBody Ability entity) {
        // 調用service等
        return "ok";
    }

    @ExceptionHandler
    public void handleException(Exception e) {
        e.printStackTrace();
    }
}

 

 

 

拋出的是org.springframework.web.bind.MethodArgumentNotValidException
在看一下DEBUG窗口中的每個參數,發現bindingResult->errors->field和defaultMessage,一個違反約束的字段名稱,另一個是違我們自定義的message

 

 

 此時我們就可以進行處理,返回我們想要的結果,而不是拋出400

5.1 優化返回值

在實際開發中,一般不會返回一個“ok”或者“success”這種字符串,通常情況下會返回一個json字符串,其中包含

  • 一個表示結果的狀態值,例如HTML狀態碼或自定義狀態值
  • 一個返回消息,解釋該狀態值或結果
  • 承載數據
package com.beemo.demo2.common;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.Map;

@AllArgsConstructor
@NoArgsConstructor
public class R {

    private int code;
    private String msg;
    private Object data;

    public static R success() {
        return success(null);
    }

    public static R success(Object data) {
        return new R(1, "操作成功", data);
    }

    public static R violateConstraint(List<Map<String, String>> violation) {
        return new R(2, "參數校驗未通過", violation);
    }
}

修改controller

package com.beemo.demo2.controller;

import com.beemo.demo2.common.R;
import com.beemo.demo2.entity.Ability;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;
import java.util.stream.Collectors;


@RestController
@RequestMapping("/demo2/ability")
@Validated
public class AbilityController {

    /**
     * 保存
     * @param entity 要保存的技能實體
     * @return 保存結果
     */
    @PostMapping("save")
    public R save(@Valid @RequestBody Ability entity) {
        // 調用service等
        return R.success();
    }
}

將異常處理方法提出,標注@ControllerAdvice注解,使得每個controller的異常都可以用該方法處理,並修改返回值,並且如果是單獨提出來一個模塊,需要在啟引用該模塊的啟動類上加掃描

package com.beemo.common.config;

import com.beemo.common.common.R;
import org.springframework.stereotype.Component;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.stream.Collectors;

@ControllerAdvice
@ResponseBody
public class MyExceptionHandler {

    @ExceptionHandler
    public R handleException(MethodArgumentNotValidException e) {
        List<String> violations = e.getBindingResult().getFieldErrors().stream().map(FieldError::getDefaultMessage).
                collect(Collectors.toList());
        return R.violateConstraint(violations);
    }
}
package com.beemo.demo2;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = "com.beemo.*")
public class Demo2Application {

    public static void main(String[] args) {
        SpringApplication.run(Demo2Application.class, args);
    }

}

然后我們再測試一下

 

 

 發現得到的結果再也不是400異常,而是我們指定的message集合了

6. 驗證非前台傳遞的參數

除了在controller驗證前台傳遞的參數之外,有時我們還需要驗證諸如自己new的對象,或者從其他方法查詢出來的對象,這時候我們可能需要把這些操作放在service層或其他層

6.1 調用非本類的校驗方法

例如我們自己new了一個對象,然后調用其他類的一個驗證方法
建立一個service接口以及一個實現類
我們在實現類上,模擬controller校驗,加上@Validated以及@Valid注解

package com.beemo.demo3.service;

import com.beemo.demo3.entity.Ability;

/**
 * 技能service接口
 */
public interface IAbilityService {

    /**
     * 保存
     * @param ability
     */
    void saveOne(Ability ability);
}
package com.beemo.demo3.service.impl;

import com.beemo.demo3.entity.Ability;
import com.beemo.demo3.service.IAbilityService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.Arrays;
import java.util.List;

/**
 * 技能service實現類
 */
@Validated
@Service
public class AbilityServiceImpl implements IAbilityService {

    @Override
    public void saveOne(@Valid @NotNull Ability ability) {
        System.out.println("通過校驗");
        // 進行保存操作等...
    }

然后在controller中調用該方法

package com.beemo.demo3.controller;

import com.beemo.demo3.entity.Ability;
import com.beemo.demo3.service.IAbilityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/demo3/ability")
@Validated
public class AbilityController {

    @Autowired
    private IAbilityService abilityService;

    /**
     * 保存
     * @return 保存結果
     */
    @PostMapping("save")
    public String save() {
        // new
        Ability ability = new Ability();
        abilityService.saveOne(ability);
        return "ok";
    }
}

我們進行測試發現,並沒有我們符合想象的返回R,相反在后台控制台報了一個異常

javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method AbilityServiceImpl#saveOne(Ability) redefines the configuration of IAbilityService#saveOne(Ability).
    at org.hibernate.validator.internal.metadata.aggregated.rule.OverridingMethodMustNotAlterParameterConstraints.apply(OverridingMethodMustNotAlterParameterConstraints.java:24) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.assertCorrectnessOfConfiguration(ExecutableMetaData.java:462) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.build(ExecutableMetaData.java:380) ~[
    ......
一個重寫的方法禁止重新定義參數的約束配置,但是方法AbilityServiceImpl#saveOne(Ability) 重新定義了 IAbilityService#saveOne(Ability)的配置

翻譯過來就是
如果你的接口沒有定義約束,那么你的實現類就不能夠定義該約束

按照異常信息,我們試着將驗證放在接口中在嘗試一下

package com.beemo.demo3.service;

import com.beemo.demo3.entity.Ability;
import org.springframework.validation.annotation.Validated;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;

@Validated
/**
 * 技能service接口
 */
public interface IAbilityService {

    /**
     * 保存
     * @param ability
     */
    void saveOne(@Valid @NotNull Ability ability);
}

 

測試之后發現返回結果為500異常,這次控制器打印異常信息明顯跟上次不一樣,貌似確實是通過校驗了,只不過拋出的異常不一樣

javax.validation.ConstraintViolationException: saveOne.ability: 不能為null
    at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:117) ~[spring-context-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    ......

我們發現在service層中如果違法約束拋出的異常為ConstraintViolationException,而並非在controller中的MethodArgumentNotValidException
我們再次改進異常處理方法,然后跟蹤一下異常的信息

 

 

 根據調試的信息,我們就可以處理我們的返回值了

    @ExceptionHandler
    public R handleException2(ConstraintViolationException e) {
        List<String> violations = e.getConstraintViolations().stream()
                .map(ConstraintViolation::getMessageTemplate).collect(Collectors.toList());
        return R.violateConstraint(violations);
    }

再測試一下

 

 

 測試成功

6.2 調用本類的校驗方法

場景:我們需要從EXCEL中讀取數據,然后保存數據庫中,需要判斷每一條記錄,如果正確就進行保存,如果失敗則打印日志,接口和實現類如下

package com.beemo.demo3.service;

import com.beemo.demo3.entity.Ability;
import org.springframework.validation.annotation.Validated;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;

@Validated
/**
 * 技能service接口
 */
public interface IAbilityService {

    /**
     * 保存
     * @param ability
     */
    void saveOne(@Valid @NotNull Ability ability);

    /**
     * 批量保存EXCEL中的數據
     */
    void saveOnesFromExcel();
}
package com.beemo.demo3.service.impl;

import com.beemo.demo3.entity.Ability;
import com.beemo.demo3.service.IAbilityService;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.validation.ConstraintViolationException;
import java.util.List;

/**
 * 技能service實現類
 */
@Service
@Slf4j
public class AbilityServiceImpl implements IAbilityService {

    @Override
    public void saveOne(Ability ability) {
        System.out.println("通過校驗");
        // 進行保存操作等...
    }

    /**
     * 批量保存EXCEL中的數據
     */
    @Override
    public void saveOnesFromExcel() {
        List<Ability> data = readFromExcel();
        for (int i = 0, size = data.size(); i < size; i ++) {
            try {
                saveOne(data.get(i));
                System.out.println("第" + i + "條記錄保存成功");
            } catch (ConstraintViolationException e) {
                log.error("第" + i + "條記錄違法約束:" + e.getMessage());
            } catch (Exception e) {
                log.error("第" + i + "條記錄保存失敗");
            }
        }
    }

    /**
     * 從EXCEL中讀取
     * @return
     */
    private List<Ability> readFromExcel() {
        return Lists.newArrayList(new Ability(null, null, (byte)1),
                new Ability(null, "測試描述", null),
                new Ability("測試名稱", null, null),
                new Ability("約德爾誘捕器", "布置一個陷阱,陷阱可以束縛敵方英雄2秒並將目標暴露在己方視野內3秒。", (byte)1));
    }
}

我們模擬了一個從EXCEL中讀取list的方法,然后調用了save方法,該方法有參數驗證,我們來進行測試

 

 

 

控制台打印成功,證明我們的約束並沒有成功,但是我們的寫法看似沒問題
其實這個原因就是因為第一個方法saveFromExcel並沒有標注驗證,不論該方法怎么調用本類的驗證方法都不會生效,此問題原因同@Transactional以及@Aysnc標注的方法,其本質原因是因為代理的問題,這里不做過多探討,解決該問題的方法有三種

  1. (不推薦)將驗證方法移到其他類中 。這種方法奏效,但是無緣無故需要多建立一個service,有時候可能就是一個空方法,只不過參數有驗證,其他不知道的小伙伴看到可能會比較懵
  2. 注入ApplicationContext獲取bean
    @Autowired
    private ApplicationContext applicationContext;
    
     /**
     * 批量保存EXCEL中的數據
     */
    @Override
    public void saveOnesFromExcel() {
        List<Ability> data = readFromExcel();
        for (int i = 0, size = data.size(); i < size; i ++) {
            try {
                applicationContext.getBean(IAbilityService.class).saveOne(data.get(i));
                System.out.println("第" + i + "條記錄保存成功");
            } catch (ConstraintViolationException e) {
                log.error("第" + i + "條記錄違法約束:" + e.getMessage());
            } catch (Exception e) {
                log.error("第" + i + "條記錄保存失敗");
            }
        }
    }

 

 

3. 通過注入自己來獲取當前類的實例,再調用該實例的方法。需要加@Lazy注解防止自我注入時spring拋出org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.core.env.Environment' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}異常

@Autowired
@Lazy
private IAbilityService abilityService;
/**
  * 批量保存EXCEL中的數據
  */
@Override
public void saveOnesFromExcel() {
    List<Ability> data = readFromExcel();
    for (int i = 0, size = data.size(); i < size; i ++) {
        try {
            abilityService.saveOne(data.get(i));
            System.out.println("第" + i + "條記錄保存成功");
        } catch (ConstraintViolationException e) {
            log.error("第" + i + "條記錄違法約束:" + e.getMessage());
        } catch (Exception e) {
            log.error("第" + i + "條記錄保存失敗");
        }
    }
}

6.3 關於@Validated的位置

我們已經清楚,約束配置的注解,例如@Valid@NotNull等,需要在接口上進行配置,那么@Validated需要標注在哪里呢,答案是接口和實現類都可以,但是標注位置不同,也有一些區別

  1. 標注在接口:意為實現類都回開啟驗證
  2. 標注在實現類:意為標注該注解的實現類才會開啟驗證,如果有一個實現類未標注@Validated,那么即使接口有約束配置,也不會在該實現類上進行校驗

6.4 關於實現類需不需要標注約束配置

個人感覺有優點優缺點
優點:一般看代碼的時候,都不會看接口,而是直接看實現類。如果標注在實現類上,可以更直觀的看到該方法的約束配置
缺點:必須與接口完全對應,如果接口修改約束配置,那么實現類必須相應的進行修改,否則會拋出異常

6.5 想讓枚舉類屬性驗證生效,需要添加@valid注解

6.6 feign調用中校驗也可以生效 

無需在feign接口中添加校驗注解,只需在controller的接口方法參數中添加@valid

 

轉載:https://blog.csdn.net/csdn_mrsongyang/article/details/106115243

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM