Spring Boot JSR 303によるパラメータ検証

42473 ワード

Spring Boot JSR 303によるパラメータ検証
概要
JSR-303はJAVA EE 6のサブ仕様で、Bean Validationと呼ばれています.
いつでも、アプリケーションのビジネスロジックを処理するには、データ検証が考慮され、直面しなければならないことです.アプリケーションは、入力されたデータが意味的に正しいことを何らかの手段で確保する必要があります.通常、アプリケーションは階層化され、異なる階層は異なる開発者によって完成されます.同じデータ検証ロジックが異なるレイヤに現れることが多いため、コード冗長性や意味の一貫性など、管理の問題が発生します.このような状況を回避するためには、検証ロジックを対応するドメインモデルにバインドすることが望ましい.
Bean Validationは、JavaBeanの検証に対応するメタデータ・モデルとAPIを定義します.デフォルトのメタデータはJava Annotationsで、XMLを使用することで既存のメタデータ情報を上書きおよび拡張できます.アプリケーションでは、Bean Validationや@NotNull,@Max,@ZipCodeなど、自分で定義したconstraintを使用することで、データモデル(JavaBean)の正確性を確保できます.constraintは、フィールド、getterメソッド、クラス、またはインタフェースに追加できます.いくつかの特定のニーズに対して、ユーザーはカスタマイズされたconstraintを簡単に開発することができます.Bean Validationは実行時のデータ検証フレームワークで、検証後に検証されたエラーメッセージがすぐに返されます.
Bean Validation仕様に組み込まれた制約注記
コンストレイント注記名
コンストレイント注記の説明
@AssertTrue
Booleanオブジェクトがtrueであることを確認
@AssertFalse
Booleanオブジェクトがfalseであることを確認
@Null
オブジェクトがnullであるかどうかを確認
@NotNull
検証オブジェクトはnullではなく、空の文字列をチェックできません.
@NotBlank
前後のスペースを削除した文字列がNullまたは長さ0でないことを確認します.
@NotEmpty
検証オブジェクト(String/Collection/Map/Array)はnullまたは長さ0にできません.
@Size(min=, max=)
オブジェクト(String/Collection/Map/Array)の長さが指定された範囲内であることを確認します.
@Length(min=, max=)
文字列の長さが指定された範囲内であることを確認します.
@Past
DateオブジェクトとCalendarオブジェクトが現在の時刻より前であることを確認します.
@PastOrPresent
DateオブジェクトとCalendarオブジェクトが現在の時刻より前または現在の時刻であることを確認します.
@Future
DateオブジェクトとCalendarオブジェクトが現在の時刻より後であることを確認します.
@FutureOrPresent
DateオブジェクトとCalendarオブジェクトが現在の時間の後または現在の時間にあるかどうかを確認します.
@Pattern
Stringオブジェクトが正規表現のルールに合致しているかどうかを確認します.
@Min
NumberとStringオブジェクトが指定した値以上であることを確認します.
@Max
NumberとStringオブジェクトが指定した値以下であることを確認します.
@DecimalMax
整形とBigDecimalが指定した値以下であることを確認します.
@DecimalMin
整形とBigDecimalが指定した値以上であることを確認します.
@Digits
検証要素は数値でなければなりません
@Digits(integer=,fraction=)
要素が指定したフォーマットの数値であるかどうかを検証し、intergerは整数精度を指定し、fractionは小数精度を指定します.
@Valid
再帰的検証プロパティ、メソッドパラメータ、またはメソッド戻りタイプ
@Email
検証がメールアドレスかどうか、nullの場合は検証しません(検証済み)
基本的な応用
導入依存
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-validationartifactId>
dependency>

検証が必要なbeanのプロパティに注記を付ける
@Data
public class Employee {

    private Long id;

    @NotBlank(message = "       ")
    private String name;

    @Email
    private String email;

    @Max(value = 120)
    private Integer age;

    private String phone;

}

使用テスト1:Controllerインタフェースメソッドの受信パラメータに@Valid修飾Employeeを使用
@PostMapping("add")
@ResponseBody
public Employee addEmployee(@Valid Employee employee) {
    employeeService.saveOrUpdate(employee);
    return employee;
}

異常の統一処理
パラメータチェックが通らない場合、BingBindException異常が放出されます.統一異常処理では、パラメータチェックが必要な場所ごとにBindingResultでチェック結果を取得する必要がなくなります.
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class})
    public ApiResult handleValidException(Exception e) {
        BindingResult bindingResult = null;
        if (e instanceof MethodArgumentNotValidException) {
            bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();
        } else if (e instanceof BindException) {
            bindingResult = ((BindException) e).getBindingResult();
        }
        Map<String, String> errorMap = new HashMap<>(16);
        assert bindingResult != null;
        bindingResult.getFieldErrors().forEach((fieldError) -> {
                    log.error("      :{},field{},errorMessage{}", fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage());
                    errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
                }

        );
        return ApiResult.validateFailed(errorMap);
    }

}

POST http://localhost:81/emp/add?email=100qqcomへのアクセステストの効果は次のとおりです.
{
  "code": 404,
  "msg": "      ",
  "data": {
    "name": "       ",
    "email": "             "
  }
}

テスト2を使用してBindingResult検証を直接使用
@PostMapping("validAdd")
public ApiResult addEmployee(@Valid Employee employee, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        Map<String, String> map = new HashMap<>(4);
        bindingResult.getFieldErrors().forEach((item) -> {
            String message = item.getDefaultMessage();
            String field = item.getField();
            map.put(field, message);
        });
        return ApiResult.failed("    ", map);
    }
    employeeService.saveOrUpdate(employee);
    return ApiResult.success();
}

結果は次のとおりです.
{
  "code": 400,
  "msg": "    ",
  "data": {
    "name": "       ",
    "age": "      120",
    "email": "             "
  }
}

パケット解決チェック
新規および変更エンティティに対する検証ルールは異なります.たとえば、idが自己増加している場合、新規のidは空で、変更は空でない必要があります.新規および変更は、同じエンティティが適切に使用されている場合は、パケット検証に使用する必要があります.
検証注記にはgroupsプロパティがあり、検証注記をグループ化できます.@NotNullのソースコードを見てみましょう.
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(NotNull.List.class)
@Documented
@Constraint(
    validatedBy = {}
)
public @interface NotNull {
    String message() default "{javax.validation.constraints.NotNull.message}";

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

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

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        NotNull[] value();
    }
}

ソースコードからgroupsがClass>タイプの配列であることがわかる、Groupsを作成することができる.
public class Groups {
    public interface Add {
    }

    public interface Update {
    }
}

パラメータオブジェクトの検証注記にグループを追加
@Data
public class Employee {
    
    @Null(message = "       id" , groups = Groups.Add.class)
    @NotNull(message = "      id" , groups = Groups.Update.class)
    private Long id;

    @NotBlank(message = "       ")
    private String name;

    @Email
    private String email;

    @Max(value = 120)
    private Integer age;

    private String phone;

}

Controllerの元の@Validではグループを指定できません.@Validatedに置き換える必要があります.
@PostMapping("validatedAdd")
public ApiResult addEmployee2(@Validated({Groups.Add.class}) Employee employee) {
    return ApiResult.success();
}
POST http://localhost:81/emp/validatedAdd?id=12のテストにアクセスします.結果は次のとおりです.
{
  "code": 404,
  "msg": "      ",
  "data": {
    "id": "       id"
  }
}

カスタム検証注記
JSR 303とspringboot-validatorはすでに多くの検証注釈を提供していますが、複雑なパラメータの検証に直面しても、私たちの要求を満たすことができません.この場合、カスタム検証注釈が必要です.
Employeeクラスの携帯電話番号のフォーマットが正しいことを確認します
カスタムIsMobile注記クラス
@Documented
@Constraint(validatedBy = {IsMobileValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RUNTIME)
public @interface IsMobile {

    //       
    boolean required() default true;

    //              
    String message() default "        ";

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

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

携帯番号フォーマットチェッカ
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {

    /**
     *    _false,            required
     */
    private boolean required = false;

    /**
     *      
     *                 
     *
     * @param constraintAnnotation     
     */
    @Override
    public void initialize(IsMobile constraintAnnotation) {
        //           ,    
        required = constraintAnnotation.required();
    }

    /**
     *         
     *
     * @param value                            
     * @param constraintValidatorContext         
     * @return boolean
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        //             ,     
        if (!StringUtils.isEmpty(value) && required) {
            return isMobile(value);
        }
        return true;
    }

    public static boolean isMobile(String mobile) {
        if (mobile.length() != 11) {
            return false;
        } else {
            //          
            String pat1 = "^((13[4-9])|(147)|(15[0-2,7-9])|(178)|(18[2-4,7-8]))\\d{8}|(1705)\\d{7}$";
            //          
            String pat2 = "^((13[0-2])|(145)|(15[5-6])|(176)|(18[5,6]))\\d{8}|(1709)\\d{7}$";
            //          
            String pat3 = "^((133)|(153)|(177)|(18[0,1,9])|(149)|(199))\\d{8}$";
            //           
            String pat4 = "^((170))\\d{8}|(1718)|(1719)\\d{7}$";

            Pattern pattern1 = Pattern.compile(pat1);
            Matcher match1 = pattern1.matcher(mobile);
            boolean isMatch1 = match1.matches();
            if (isMatch1) {
                return true;
            }
            Pattern pattern2 = Pattern.compile(pat2);
            Matcher match2 = pattern2.matcher(mobile);
            boolean isMatch2 = match2.matches();
            if (isMatch2) {
                return true;
            }
            Pattern pattern3 = Pattern.compile(pat3);
            Matcher match3 = pattern3.matcher(mobile);
            boolean isMatch3 = match3.matches();
            if (isMatch3) {
                return true;
            }
            Pattern pattern4 = Pattern.compile(pat4);
            Matcher match4 = pattern4.matcher(mobile);
            return match4.matches();
        }
    }
}

エンティティクラスのphoneに@IsMobile注記を付ける
テストPOST http://localhost:81/emp/validAdd?name=chen&phone=137777
{
  "code": 400,
  "msg": "    ",
  "data": {
    "phone": "        "
  }
}

まとめ
カスタム注釈は手動で2つのファイルを実装する必要があります:カスタム注釈クラス+注釈検証クラス
カスタム注釈クラス:message()+groups()+payload()必須;
注記検証クラス:ConstraintValidatorクラス+を継承する2つのメソッド(initialize:初期化アクション、isValid:論理処理)