1. パラメータを検証する必要があるのはなぜですか?
日常の開発では、不正なパラメータがビジネスに影響を与えることを防ぐために、インターフェイスのパラメータが正しく格納されるように検証する必要があります。
たとえば、ログインするときは、ユーザー名、パスワード、その他の情報が空かどうかを確認する必要があります。フロントエンドにも検証がありますが、インターフェイスのセキュリティのため、バックエンド インターフェイスでパラメータ検証を実行する必要があります。
同時に、パラメータをよりエレガントに検証するために、その方法をここで紹介しますSpring Validation
。
Java API 仕様 (JSR303: Java EE 6 のサブ仕様、Bean Validation と呼ばれる) は、Bean 検証のための標準の validation-api を定義していますが、実装は提供していません。Hibernate 検証はこの仕様の実装であり、検証の注釈が追加されます。例: @Email、@Length。
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
に属するため、検証できます。Default
email
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 以外である必要があります |
@範囲 | 数値型、文字列型。注釈が付けられた要素は適切なスコープ内に存在する必要があります |