[SpringBoot] バリデータ フレームワークを使用して SpringBoot のパラメーターをエレガントに検証する

1. パラメータを検証する必要があるのはなぜですか?

日常の開発では、不正なパラメータがビジネスに影響を与えることを防ぐために、インターフェイスのパラメータが正しく格納されるように検証する必要があります。
たとえば、ログインするときは、ユーザー名、パスワード、その他の情報が空かどうかを確認する必要があります。フロントエンドにも検証がありますが、インターフェイスのセキュリティのため、バックエンド インターフェイスでパラメータ検証を実行する必要があります。

同時に、パラメータをよりエレガントに検証するために、その方法をここで紹介しますSpring Validation

Java API 仕様 (JSR303: Java EE 6 のサブ仕様、Bean Validation と呼ばれる) は、Bean 検証のための標準の validation-api を定義していますが、実装は提供していません。Hibernate 検証はこの仕様の実装であり、検証の注釈が追加されます。例: @Email、@Length。

JSR公式サイト

Hibernate Validator 公式ウェブサイト

Spring Validation は Hibernate Validation の二次的なカプセル化であり、Spring MVC パラメータの自動検証をサポートするために使用されます。

2. 依存関係を導入する

spring-boot のバージョンが 2.3.x より前の場合、spring-boot-starter-web は自動的に hibernate-validator 依存関係を渡します。Spring-Boot バージョンが 2.3.x 以降の場合は、依存関係を手動で導入する必要があります。

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

Webサービスでは、不正なパラメータによるビジネスへの影響を防ぐために、パラメータの検証をController層で行う必要があります。ほとんどの場合、リクエスト パラメーターは次の 2 つの形式に分けられます。

  • POST、PUTリクエスト、@requestBodyパラメータの受信に使用
  • GET リクエスト、@requestParam、@PathVariableパラメータの受信に使用します

次に、簡単に紹介します~~~


3. @requestBodyパラメータの検証

POST リクエストと PUT リクエストの場合、バックエンドは通常、@requestBody + 对象受信パラメータを使用します。この時点で必要なのは、@Validated 或 @Validオブジェクトにアノテーションを追加するだけで、自動パラメータ検証を簡単に実装できます。検証が失敗した場合は、MethodArgumentNotValidException例外がスローされます。

UserVo: 検証アノテーションを追加

@Data
public class UserVo {
    
    

	private Long id;

    @NotNull
    @Length(min = 2, max = 10)
    private String userName;

    @NotNull
    @Length(min = 6, max = 20)
    private String account;

    @NotNull
    @Length(min = 6, max = 20)
    private String password;
}

ユーザーコントローラー :

@RestController
@RequestMapping("/user")
public class UserController {
    
    

    @PostMapping("/addUser")
    public String addUser(@RequestBody @Valid UserVo userVo) {
    
    
        return "addUser";
    }
}

または、@Validated注釈を使用します。

@PostMapping("/addUser")
public String addUser(@RequestBody @Validated UserVo userVo) {
    
    
    return "addUser";
}

4. @requestParam、@PathVariable パラメータの検証

GET リクエストは通常​​、@requestParam、@PathVariableアノテーションを使用してパラメータを受け取ります。パラメータが多い場合(たとえば、5 つ以上)、オブジェクト受信を使用することをお勧めします。それ以外の場合は、パラメータをメソッド入力パラメータに 1 つずつフラット化することをお勧めします。

この場合、Controller クラスに注釈を付け、入力パラメーターで@Validated制約注釈 ( など) を宣言する必要があります。@Min検証が失敗するとConstraintViolationException例外がスローされます

@RestController
@RequestMapping("/user")
@Validated
public class UserController {
    
    

    @GetMapping("/getUser")
    public String getUser(@Min(1L) Long id) {
    
    
        return "getUser";
    }
}

5. 統一された例外処理

検証が失敗した場合は、例外がMethodArgumentNotValidExceptionスローされますConstraintViolationException実際のプロジェクト開発では、よりわかりやすいプロンプトを返すために、通常、統一された例外処理が使用されます。

@RestControllerAdvice
public class ExceptionControllerAdvice {
    
    

    @ExceptionHandler({
    
    MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.OK)
    public String handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
    
    
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
    
    
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
        return "参数校验失败" + msg;
    }

    @ExceptionHandler({
    
    ConstraintViolationException.class})
    public String handleConstraintViolationException(ConstraintViolationException ex) {
    
    
        return "参数校验失败" + ex;
    }

}

6. グループ認証

実際のプロジェクトでは、複数のメソッドがパラメータを受け取るために同じクラス オブジェクトを使用する必要がある場合があり、メソッドごとに検証ルールが異なる可能性があります。現時点では、クラスのフィールドに制約アノテーションを追加するだけでは問題を解決できません。したがって、spring-validationこの種の問題を解決するために特に使用されるグループ検証機能がサポートされています。

たとえば、User を保存する場合、userId は null 可能ですが、User を更新する場合、userId の値は 1L 以上である必要があり、他のフィールドの検証ルールはどちらの場合も同じです。この時点でグループ検証を使用するコード例は次のとおりです。

制約アノテーションで適用可能なグループ化情報を宣言するgroups

1: グループ化インターフェースを定義する

public interface ValidGroup extends Default {
    
    
	// 添加操作
    interface Save extends ValidGroup {
    
    }
    // 更新操作
    interface Update extends ValidGroup {
    
    }
	// ...
}

デフォルトを継承する理由 以下にあります。

2: 検証が必要なフィールドにグループを割り当てる

@Data
public class UserVo {
    
    
	
    @Null(groups = ValidGroup.Save.class, message = "id要为空")
    @NotNull(groups = ValidGroup.Update.class, message = "id不能为空")
    private Long id;

    @NotBlank(groups = ValidGroup.Save.class, message = "用户名不能为空")
    @Length(min = 2, max = 10)
    private String userName;

    @Email
    @NotNull
    private String email;
}

チェックフィールドによると:

  • id: 割り当てグループ: 保存、更新。追加する場合は null にする必要がありますが、更新する場合は null であってはなりません。
  • ユーザー名: グループの割り当て: 保存。追加するときは空であってはなりません
  • 電子メール: 割り当てグループ: なし。つまり、デフォルトのグループ化を使用します。

3: 検証が必要なパラメータのグループを指定します

@RestController
@RequestMapping("/user")
public class UserController {
    
    

    @PostMapping("/addUser")
    public String addUser(@RequestBody @Validated(ValidGroup.Save.class) UserVo userVo) {
    
    
        return "addUser";
    }

    @PostMapping("/updateUser")
    public String updateUser(@RequestBody @Validated(ValidGroup.Update.class) UserVo userVo) {
    
    
        return "updateUser";
    }
}

テスト検証。

4: デフォルトのグループ化

ValidGroupインターフェイスがDefaultインターフェイスを継承しない場合、emailフィールドは検証できません (グループ化は割り当てられません)。継承後、フィールドはタイプ、つまりデフォルト グループValidGroupに属するため、検証できます。Defaultemail

7. ネストされた検証

@Valid注釈は必須です

@Data
public class UserVo {
    
    

	@NotNull(groups = {
    
    ValidGroup.Save.class, ValidGroup.Update.class})
    @Valid
    private Address address;
}

8. カスタマイズされた検証

ケース 1. 暗号化 ID のカスタマイズされた検証

暗号化された ID (数字または文字 a ~ から、長さ 32 ~ 256 で構成される) 検証をカスタマイズするとします。これは主に 2 つのステップに分かれています。

1. カスタム制約の注釈

@Target({
    
    METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {
    
    EncryptIdValidator.class}) // 自定义验证器
public @interface EncryptId {
    
    

    // 默认错误消息
    String message() default "加密id格式错误";
    // 分组
    Class<?>[] groups() default {
    
    };
    // 负载
    Class<? extends Payload>[] payload() default {
    
    };
}

2. 制約チェッカーを作成する

public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {
    
    

    private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
    
    
        if (value != null) {
    
    
            Matcher matcher = PATTERN.matcher(value);
            return matcher.find();
        }
        return true;
    }
}

3.用途

@Data
public class UserVo {
    
    

    @EncryptId
    private String id;
}

ケース 2: カスタムの性別検証では 2 つの値のみが許可されます

UserVo クラスの sex 属性では、フロントエンドが 2 つの列挙値 M と F を渡すことのみが許可されます。実装方法は?

1. カスタム制約の注釈

@Target({
    
    METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {
    
    SexValidator.class})
public @interface SexValid {
    
    


    // 默认错误消息
    String message() default "value not in enum values";

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

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

    String[] value();
}

2. 制約チェッカーを作成する

public class SexValidator implements ConstraintValidator<SexValid, String> {
    
    

    private List<String> sexs;

    @Override
    public void initialize(SexValid constraintAnnotation) {
    
    
        sexs = Arrays.asList(constraintAnnotation.value());
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
    
    
        if (StringUtils.isEmpty(value)) {
    
    
            return true;
        }
        return sexs.contains(value);
    }

}

3.用途

@Data
public class UserVo {
    
    

    @SexValid(value = {
    
    "F", "M"}, message = "性别只允许为F或M")
    private String sex;

}

4. テスト

@GetMapping("/get")
private String get(@RequestBody @Validated UserVo userVo) {
    
    
    return "get";
}

9. 検証ビジネスルールの実装

ビジネス ルールの検証とは、インターフェイスが特定の特定のビジネス ルールを満たす必要があることを意味します。例: ビジネス システムのユーザーは、一意性を確保する必要があります。ユーザー属性は他のユーザーと競合することはできず、データベース内の既存のユーザーのユーザー名、携帯電話番号、電子メール アドレスと重複することは許可されません。そのため、ユーザー作成時にユーザー名、携帯電話番号、メールアドレスが登録されているかを確認する必要があり、ユーザー編集時に既存ユーザーの属性に情報を変更することはできません。

最も洗練された実装方法は、Bean Validationの標準方法を参照し、カスタム検証アノテーションを使用してビジネス ルールの検証を完了することです。

1. カスタム制約の注釈

まず、ビジネス ルール検証用に 2 つのカスタム アノテーションを作成する必要があります。

  • UniqueUser: ユーザー名、携帯電話番号、電子メール アドレスが含まれるユーザーが一意であることを示します。
  • NotConflictUser: ユーザーの情報に競合がないことを示します。競合がないとは、ユーザーの機密情報が他のユーザーと重複しないことを意味します。
@Documented
@Retention(RUNTIME)
@Target({
    
    FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = UserValidator.UniqueUserValidator.class)
public @interface UniqueUser {
    
    

    String message() default "用户名、手机号码、邮箱不允许与现存用户重复";
    Class<?>[] groups() default {
    
    };
    Class<? extends Payload>[] payload() default {
    
    };

}
@Documented
@Retention(RUNTIME)
@Target({
    
    FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = UserValidator.NotConflictUserValidator.class)
public @interface NotConflictUser {
    
    

    String message() default "用户名称、邮箱、手机号码与现存用户产生重复";
    Class<?>[] groups() default {
    
    };
    Class<? extends Payload>[] payload() default {
    
    };

}

2. 制約チェッカーを作成する

ConstraintValidatorカスタム検証アノテーションを有効にするには、インターフェイスを実装する必要があります。インターフェイスの最初のパラメータはカスタム アノテーション タイプで、2 番目のパラメータはアノテーション付きフィールドのクラスです。複数のパラメータを検証する必要があるため、ユーザー オブジェクトを直接渡します。注意すべき点は、ConstraintValidatorインターフェイスの実装クラスは追加する必要がなく、@Componentコンテナの起動時にすでにロードされているということです。

public class UserValidator<T extends Annotation> implements ConstraintValidator<T, UserVo> {
    
    

    protected Predicate<UserVo> predicate = c -> true;

    @Override
    public boolean isValid(UserVo userVo, ConstraintValidatorContext constraintValidatorContext) {
    
    
        return predicate.test(userVo);
    }

    public static class UniqueUserValidator extends UserValidator<UniqueUser>{
    
    
        @Override
        public void initialize(UniqueUser uniqueUser) {
    
    
            UserDao userDao = ApplicationContextHolder.getBean(UserDao.class);
            predicate = c -> !userDao.existsByUserNameOrEmailOrTelphone(c.getUserName(),c.getEmail(),c.getTelphone());
        }
    }

    public static class NotConflictUserValidator extends UserValidator<NotConflictUser>{
    
    
        @Override
        public void initialize(NotConflictUser notConflictUser) {
    
    
            predicate = c -> {
    
    
                UserDao userDao = ApplicationContextHolder.getBean(UserDao.class);
                Collection<UserVo> collection = userDao.findByUserNameOrEmailOrTelphone(c.getUserName(), c.getEmail(), c.getTelphone());
                // 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突
                return collection.isEmpty() || (collection.size() == 1 && collection.iterator().next().getId().equals(c.getId()));
            };
        }
    }

}
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
    
    

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    
    
        context = applicationContext;
    }

    public static ApplicationContext getContext() {
    
    
        return context;
    }

    public static Object getBean(String name) {
    
    
        return context != null ? context.getBean(name) : null;
    }

    public static <T> T getBean(Class<T> clz) {
    
    
        return context != null ? context.getBean(clz) : null;
    }

    public static <T> T getBean(String name, Class<T> clz) {
    
    
        return context != null ? context.getBean(name, clz) : null;
    }

    public static void addApplicationListenerBean(String listenerBeanName) {
    
    
        if (context != null) {
    
    
            ApplicationEventMulticaster applicationEventMulticaster = (ApplicationEventMulticaster)context.getBean(ApplicationEventMulticaster.class);
            applicationEventMulticaster.addApplicationListenerBean(listenerBeanName);
        }
    }

}

3. テスト

@RestController
@RequestMapping("/user")
public class UserController {
    
    

    @PostMapping("/addUser")
    public String addUser(@RequestBody @UniqueUser UserVo userVo) {
    
    
        return "addUser";
    }

    @PostMapping("/updateUser")
    public String updateUser(@RequestBody @NotConflictUser UserVo userVo) {
    
    
        return "updateUser";
    }
}

10. @Valid と @Validated の違い

違いは次のとおりです。

ここに画像の説明を挿入します

11. よく使用される注釈

Bean Validation基本的な実際の開発には十分な組み込みのアノテーションが多数あります。

注釈 詳細
@ヌル いかなるタイプ。注釈が付けられた要素は null である必要があります
@NotNull いかなるタイプ。注釈が付けられた要素は null ではありません
@Min(値) 数値型 (double および float は精度を失います)。その値は、指定された最小値以上である必要があります
@Max(値) 数値型 (double および float は精度を失います)。その値は、指定された最大値以下でなければなりません
@DecimalMin(値) 数値型 (double および float は精度を失います)。その値は、指定された最小値以上である必要があります
@DecimalMax(値) 数値型 (double および float は精度を失います)。その値は、指定された最大値以下でなければなりません
@サイズ(最大、最小) 文字列、コレクション、マップ、配列型。注釈付き要素のサイズ (長さ) は指定された範囲内である必要があります
@Digits (整数、分数) 数値型、数値文字列型。その値は許容範囲内である必要があります。整数: 整数の精度; 分数: 小数精度
@過去 日付型。注釈が付けられた要素は過去の日付である必要があります
@未来 日付型。注釈付きの要素は将来の日付である必要があります
@パターン(値) 文字列型。注釈が付けられた要素は、指定された正規表現と一致する必要があります

Hibernate Validator次のように、いくつかの注釈もオリジナルに基づいて埋め込まれています。

注釈 詳細
@Eメール 文字列型。注釈付きの要素は電子メール アドレスである必要があります
@長さ 文字列型。注釈付き文字列の長さは指定された範囲内である必要があります
@空ではない 文字列、コレクション、マップ、配列型。注釈が付けられた要素の長さは null 以外である必要があります
@範囲 数値型、文字列型。注釈が付けられた要素は適切なスコープ内に存在する必要があります

おすすめ

転載: blog.csdn.net/sco5282/article/details/130325918