Java パラメータの検証と統一された例外処理
[はじめに] パラメータの検証はインタフェース開発に欠かせない. 以前は, 検証パラメータは基本的に多数の制御文でif/else
実現されていた. その後反射+自定义注解
, フォームは検証に使用できるが, 再利用性はあまり良くない. その中で、「アリ開発マニュアル」のパラメータ検証に関するプロトコルの説明:
インターフェイスパラメータの合法性をエレガントに検証する方法は? Spring は、アノテーション検証用の検証済みフレームワークを開発しました。これにより、多くの冗長な検証実装ロジックの詳細を保存し、開発とコード メンテナンスのパフォーマンスを向上させることができます。
PART1. 基本概念
-
JSR-303
JSR は Java Specification Requests (Java 仕様提案) の略です。誰でも JSR を送信して、新しい API とサービスを Java プラットフォームに追加できます。JSR は、Java の世界で重要な標準になっています。JSR-303 はBean Validation
と呼ばれる Java EE 6 のサブ仕様であり、Java によって定義された注釈ベースのデータ検証仕様のセットです。 -
Hibernate バリデーター
Hibernate Validator は、Bean Validation のリファレンス実装です. Hibernate Validator は、いくつかの追加の制約に加えて、JSR 303 仕様のすべての組み込み制約の実装を提供します。
その pom 参照は次のとおりです。
<!--jsr 303-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.1</version>
</dependency>
<!-- hibernate validator-->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.5.Final</version>
</dependency>
両者の関係は、Hibernate Validator が Bean Validation の実装であるということです.JSR 仕様に加えて、独自の制約実装もいくつか追加されているため、pom をクリックして、Hibernate Validator が validation-api に依存していることを確認します。
Java EE は 2018 年に Jakarta EE に名前が変更されたため、jakarta.validation は javax.validation から名前が変更されました。
スプリング ブート アプリケーションの場合は、それが提供するスターターを直接参照できます。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Spring Boot には、Spring Boot の pom を継承する独自のバージョン番号構成があるため、バージョン番号を自分で指定する必要はありません。
このスターターは、内部的に Hibernate Validator にも依存しています。Hibernate Validator は内部的に jakarta.validation-api に依存しています。
PART2.Validator パラメータの検証
実装と使用法: 一般的に、2 つの注釈がより頻繁に使用されます: @Validated
、@Valid
@valid と @validateの違い
違い | @有効 | @検証済み |
---|---|---|
プロバイダー | JSR-303仕様 | 春 |
グループ化をサポートするかどうか | サポートしません | サポート |
ラベル位置 | メソッド、フィールド、コンストラクター、パラメーター、TYPE_USE | タイプ、メソッド、パラメータ |
ネストされた検証 | サポート | サポートしません |
ElementType.TYPE: クラス、インターフェイス、または列挙型を変更できます
ElementType.FIELD: メンバー変数を変更できます
ElementType.METHOD: メソッドを変更できます
ElementType.PARAMETER: パラメーターを変更できます
ElementType.CONSTRUCTOR: コンストラクターを変更できます
ElementType.LOCAL_VARIABLE: ローカル変数を変更できます
ElementType .ANNOTATION_TYPE: アノテーションを変更できます
ElementType.PACKAGE: パッケージを変更できますTYPE_USE は、任意のタイプ (クラスを除く) をマークするために使用できます。
特定の検証関数で一般的に使用される注釈は次のとおりです。
制約 | 例証する | サポートされているデータ型 |
---|---|---|
@AssertFalse | 注釈付き要素は false でなければなりません | ブール値 |
@AssertTrue | 注釈付き要素は true でなければなりません | ブール値 |
@DecimalMax | 注釈付き要素は、値が指定された最大値以下でなければならない数値でなければなりません | BigDecimal、BigInteger、CharSequence、byte、short、int、long |
@DecimalMin | 注釈付き要素は、値が指定された最小値以上でなければならない数値でなければなりません | BigDecimal、BigInteger、CharSequence、byte、short、int、long |
@マックス | 注釈付き要素は、値が指定された最大値以下でなければならない数値でなければなりません | BigDecimal、BigInteger、byte、short、int、long |
@最小 | 注釈付き要素は、値が指定された最小値以上でなければならない数値でなければなりません | BigDecimal、BigInteger、byte、short、int、long |
@Digits(整数=,分数=) | 注釈の値が整数と小数以下の数値かどうかを確認します | BigDecimal、BigInteger、CharSequence、byte、short、int、long |
@Eメール | コメント化された要素は電子メール アドレスである必要があります。オプションのパラメータ regexp および flag を使用すると、一致する必要がある追加の正規表現 (正規表現フラグを含む) を指定できます。 | CharSequence |
@未来 | 注釈付き要素は将来の日付でなければなりません | Date、Calendar、Instant、LocalDateなど |
@FutureOrPresent | 注釈付き要素は将来の日付または現在の日付でなければなりません | Date、Calendar、Instant、LocalDateなど |
@過去 | 注釈付き要素は過去の日付でなければなりません | Date、Calendar、Instant、LocalDateなど |
@過去または現在 | 注釈付き要素は、過去の日付または現在の日付でなければなりません | Date、Calendar、Instant、LocalDateなど |
@ノットブランク | コメント化された要素が null ではなく、両側の空白文字を削除した後の長さが 0 より大きい | CharSequence |
@空ではない | 注釈付き要素は null ではなく、コレクションは空ではありません | CharSequence、コレクション、マップ、配列 |
@NotNull | 注釈付き要素は null ではありません | いかなるタイプ |
@ヌル | 注釈付き要素が null です | いかなるタイプ |
@Pattern(正規表現=, フラグ=) | コメント化された要素は、正規表現 regex と一致する必要があります | CharSequence |
@Size(最小=, 最大=) | 注釈付き要素のサイズは、最小から最大の間でなければなりません (クローズド レンジ) | CharSequence、コレクション、マップ、配列 |
次に、パラメーター検証の実践に関する簡単なデモが実行されます。
- パラメーター エンティティ クラスを定義し、検証が必要なパラメーターに対応する注釈を追加します。
@Data
public class UserInfo {
@NotBlank(message = "姓名不能为空")
public String name;
@Min(value = 10, message = "年龄不得少于10岁")
public int age;
}
2. エントリの @Validated アノテーションでパラメーター検証を実行するコントローラー クラスを定義します。
@RestController
@Slf4j
public class TestController {
@PostMapping("getInfo")
public String test(@RequestBody @Validated UserInfo userInfo) {
log.info("入参:【{}】", userInfo);
return "success";
}
}
リクエストの結果は次のとおりです。
PART3. 団体認証
複数のインターフェースがパラメーター オブジェクトを再利用する場合があり、その中のパラメーターはインターフェース A では必要ですが、インターフェース B では必要ありません。
このようなシナリオではグループ検証方法を使用する必要があり、ほとんどのシナリオではグループ検証の柔軟な配置で十分です。
Validator にはグループの概念があり、このパラメーターを使用してさまざまな検証グループを指定するのに役立ちます。
Validated
groups
独自のデフォルト グループ Default.class があります. デフォルトの検証に基づいて新しいグループ検証を追加する必要がある場合は、デフォルトが であるため、Default を継承することをお勧めしますgroups = {Default.class}
.
Step1. グループ化インターフェースを設定する
構築したいグループは、さまざまなビジネスで使用されるフィールドに分割されたグループです. ビジネスの例はユーザー オブジェクトであり、ユーザーはさまざまな役割を持っています. さまざまなインターフェイスは、このユーザー オブジェクトのさまざまなフィールドを使用します. たとえば、学生 ( Student
)、教師 ( Teacher
):
public interface Student {
}
public interface Teacher {
}
Step2. グループ検証が必要なパラメーターにグループを追加する
@Data
public class UserInfo {
@NotBlank(message = "姓名不能为空")
public String name;
@Min(value = 10, message = "年龄不得少于10岁", groups = {
Default.class})
public int age;
@NotBlank(message = "手机号不能为空", groups = {
Teacher.class})
public String phone;
@NotEmpty(message = "授课科目不能为空", groups = {
Teacher.class})
@Size(min = 2, message = "必须至少两个科目", groups = {
Teacher.class})
private List<String> subjects;
}
Step3. @Validated に対応するグループを追加
@RestController
@Slf4j
public class TestController {
@PostMapping("getInfo")
public String test(@RequestBody @Validated({
Teacher.class}) UserInfo userInfo) {
log.info("入参:【{}】", userInfo);
return "success";
}
}
ps. グループ継承の検証:
カスタム グループは継承を使用して検証できます. たとえば、多くのグループを特定のグループにカプセル化します. これは、自由に組み合わせるのに便利です. 複数のカスタム グループについては、次のケースを参照してください:
public interface GroupsOpration extends GroupUpdate{
}
public interface GroupUpdate extends Default {
}
public interface GroupDel extends Default {
}
public interface GroupAdd extends Default {
}
PART4. オブジェクトの検証
インターフェースの入力パラメーターにコレクション オブジェクト要素が含まれている場合があり、このオブジェクトの各属性も個別に検証する必要があります. このシナリオでは、@Valid アノテーションを組み合わせてネストされた検証を実行できます. 例は次のとおりです:
Step1. @Valid アノテーションでマークされたコレクション属性 userInfoList を含む入力パラメーター UsersVo を再定義し、各要素 UserInfo オブジェクトの検証制約は前の定義を参照します。
@Data
public class UsersVo {
@NotBlank(message = "ID不能为空")
public String id;
@Valid
@NotEmpty
public List<UserInfo> userInfoList;
}
Step2.Controller 層のインターフェイス パラメータは、ネストされた検証の目的を達成するために @Validated アノテーションを追加します
@PostMapping("getInfos")
public String test(@RequestBody @Validated UsersVo usersVo) {
log.info("入参:【{}】", usersVo);
return "success";
}
PART5. カスタムバリデーター
デフォルトのアノテーション ルールがビジネス ニーズを満たすことができない場合、validator
カスタム アノテーションの形式が提供され、開発者がカスタム ルールの検証を実行するのに役立ちます。
Step1. カスタム アノテーションを定義します。
最初のステップは、カスタマイズする必要がある注釈を決定することです. たとえば、名前が Zake で始まるかどうかを確認する注釈を定義しました.
@Target({
ElementType.FIELD}) // 可以注入的类型,字段和参数类型
@Retention(RUNTIME) // 运行时生效
@Constraint(validatedBy = {
CheckNameValidator.class}) // 指定用于验证元素的验证器
public @interface CheckName {
String message() default "姓名不正确"; //提示的信息
Class<?>[] groups() default {
}; //分组验证,例如只在新增时进行校验等
Class<? extends Payload>[] payload() default {
};
}
message
、、の 3 つの属性を定義する必要があります。groups
そうしないと、検証中に次のエラーがスローされます。payload
Step2. 実際のアノテーション処理クラスを定義します。
インターフェースを実装する必要がありますConstraintValidator
. ジェネリック型の最初のパラメーターは注釈クラスであり、2 番目のパラメーターは特定の検証オブジェクトの型です.
public class CheckNameValidator implements ConstraintValidator<CheckName, String> {
// 初始化注解的校验内容
@Override
public void initialize(CheckName constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
// 具体的校验逻辑
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value.startsWith("zake");
}
}
Step3、パラメーター変数にアノテーションを追加する
@Data
public class UserInfo {
@NotBlank(message = "姓名不能为空")
@CheckName
public String name;
@Min(value = 10, message = "年龄不得少于10岁", groups = {
Default.class})
public int age;
@NotBlank(message = "手机号不能为空", groups = {
Teacher.class})
public String phone;
@NotEmpty(message = "授课科目不能为空", groups = {
Teacher.class})
@Size(min = 2, message = "必须至少两个科目", groups = {
Teacher.class})
private List<String> subjects;
}
PART6. 例外処理
上記はパラメーター検証の目的を達成しますが、例外情報のプロンプトは友好的ではなく、例外処理をさらに最適化できます。
検証例外情報を処理する方法は?
- BindingResult 情報処理
- 特定の例外処理のコントローラー
- 統一された例外処理
1.BindingResult情報処理
検証はBindResult
オブジェクトのカプセル化の例外情報を提供します。注釈パラメーター位置の直後@Validated
。追跡するように注意してください。そうしないと、追加した後、オブジェクトの検証に失敗した後、カプセル化された基本的な例外情報BindResult
をオブジェクトは開発者が自由に扱うことができます。
@PostMapping("getInfo")
public String test(@RequestBody @Validated({
Teacher.class}) UserInfo userInfo, BindingResult result) {
log.info("入参:【{}】", userInfo);
if (result.hasErrors()) {
FieldError fieldError = result.getFieldError();
String field = fieldError.getField();
String msg = fieldError.getDefaultMessage();
return field + ":" + msg; // 异常信息的处理返回
}
return "success";
}
2. コントローラーが特定の例外処理を実行する
通常、この方法はあまり使われませんが、グローバル例外処理の場合、コントローラ層で例外処理が行われることはほとんどなく、特殊な場合に使用できます。
このメソッドは、定義メソッドが対応するコントローラー層に変更されていることを除いて、グローバル例外ハンドラーに似ていますController
。
@RestController
@Slf4j
public class TestController {
@PostMapping("getInfo")
public String test(@RequestBody @Validated({
Teacher.class}) UserInfo userInfo) {
log.info("入参:【{}】", userInfo);
return "success";
}
/**
在控制器层处理异常信息,仅仅适用于当前控制器
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Object processException(MethodArgumentNotValidException e){
log.error(e.getMessage());
return e.getAllErrors().get(0).getDefaultMessage();
}
}
3. 統一された例外処理 (推奨)
グローバル統合例外処理は、最も一般的に使用される処理方法です.この方法は、例外情報をカスタム結果にアセンブルし、ログと処理にも使用できます. 次のように処理されます。
Step1. 新しいグローバル統合例外処理クラスを作成し、コントローラー層のアノテーションに対応するまたは を类名
マークし、それぞれ をマークします。@ControllerAdvice
@RestControllerAdvice
@Controller
@RestController
@ControllerAdvice -> @Controller
@RestControllerAdvice -> @RestController
Step2. グローバル統一例外処理クラスの対応するメソッド内で、@ExceptionHandler
メソッドアノテーションを使用. @ExceptionHandler アノテーションにパラメータを追加することができます. パラメータは特定の例外クラスのクラスです, つまり、このメソッドはこのタイプの例外を特別に処理します. . 例外が発生すると、Spring は例外のスローに最も近い処理方法を選択します。
コード例は次のとおりです。
エラー コードの列挙を定義する
@Getter
public enum ResultCodeEnum {
SUCCESS(200, "成功"),
FAILED(1001,"失败"),
ERROR(500, "系统内部错误");
private int code;
private String message;
ResultCodeEnum(int code, String message) {
this.code = code;
this.message = message;
}
}
統一された応答形式を定義します。
@Data
public class ResultInfo<T> {
public int code;
public String message;
public T data;
public ResultInfo(T data){
this.code = ResultCodeEnum.SUCCESS.getCode();
this.message = ResultCodeEnum.SUCCESS.getMessage();
this.data = data;
}
public ResultInfo(int code, String msg) {
this.code = code;
this.message = msg;
}
}
グローバル例外ハンドラーは、例外情報のインターセプトを増やし、例外が発生したときに返されるデータ形式を変更します。
@RestControllerAdvice
public class GlobalExceptionHandlerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultInfo<String> methodArgumentNotValidException(MethodArgumentNotValidException e) {
// 从异常对象中拿到ObjectError对象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 然后提取错误提示信息进行返回
return new ResultInfo<>(ResultCodeEnum.FAILED.getCode(), objectError.getDefaultMessage());
}
@ExceptionHandler(Exception.class)
public ResultInfo<String> ExceptionHandler(Exception e) {
return new ResultInfo<>(ResultCodeEnum.ERROR.getCode(), ResultCodeEnum.ERROR.getMessage());
}
}