JSR 303の詳細解説

  1. JSR 303とは?

JSR 303(Java Specification Request 303)は、Javaオブジェクトの検証ルールを定義する仕様で、Bean検証として知られています。 1.1 主な機能

アノテーション駆動:Javaクラスに直接検証ルールを定義可能。 組み込み制約:@NotNull、@Size、@Min、@Maxなどの標準アノテーションが利用可能。 カスタム制約:独自の検証アノテーションとロジックの実装が可能。 グループ検証:作成・更新などシーンごとの検証グループを設定可能。

1.2 よく使われるアノテーション

@NotNull:注釈付きの要素がnullでないことの検証。 @Size:注釈付き要素のサイズが指定範囲内であるかの検証。 @Min/@Max:注釈付き要素の値が指定範囲内であるかの検証。 @Email:注釈付き要素が有効なメールアドレスであるかの検証。

  1. 使用手順

JSR 303は仕様であり、具体的な実装が必要です。Hibernate Validatorはその代表的な実装です。 2.1 ライブラリの導入

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>

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

2.2 エンティティクラスでの検証ルール設定

以下に例を示します:

@Data
public class ProductEntity implements Serializable {
  
    /**
     * 商品ID。識別用のユニークな値。
     * 
     * 更新処理(EditGroup)ではnullでないこと、
     * 新規作成(CreateGroup)ではnullであることを強制。
     * アノテーションによる検証は柔軟性と保守性を向上させます。
     */
    @NotNull(message = "更新時は商品IDを指定してください",groups = {EditGroup.class})
    @Null(message = "新規作成時はIDを指定しないでください",groups = {CreateGroup.class})
    private Long productId;


    /**
     * 商品名。必須項目。
     * 
     * 新規作成・更新のどちらでも空文字を許容しない。
     * @NotBlankアノテーションで検証し、空文字の場合はエラーを発生。
     */
    @NotBlank(message = "商品名を入力してください",groups = {CreateGroup.class,EditGroup.class})
    private String name;


  
    /**
     * 商品画像URL。有効なURL形式であることを検証。
     * 
     * 新規作成(CreateGroup)では空文字不可、
     * 更新(EditGroup)では空文字可。
     * URL形式の検証には@URLアノテーションを使用。
     */
    @NotBlank(groups = {CreateGroup.class})
    @URL(message = "有効なURLを入力してください",groups={CreateGroup.class,EditGroup.class})
    private String imageUrl;

    
    /**
     * 表示状態。0:非表示、1:表示。
     * 
     * 新規作成(CreateGroup)と状態変更(StatusEditGroup)の両方で
     * null不可かつ0か1の値のみ許容。
     */
    @NotNull(groups = {CreateGroup.class, StatusEditGroup.class})
    @ListValue(vals={0,1},groups = {CreateGroup.class, StatusEditGroup.class})
    private Integer displayStatus;

    /**
     * 商品の初期文字。新規作成時のみ必須。
     * 
     * 新規作成(CreateGroup)ではnull不可、
     * 更新(EditGroup)ではアルファベット文字のみ許容。
     */
    @NotEmpty(groups={CreateGroup.class})
    @Pattern(regexp="^[a-zA-Z]$",message = "初期文字はアルファベットで入力してください",groups={CreateGroup.class,EditGroup.class})
    private String initialChar;

 
    /**
     * 表示順。非負数であることを検証。
     * 
     * 新規作成(CreateGroup)ではnull不可、
     * 更新(EditGroup)では0以上であることを検証。
     */
    @NotNull(groups={CreateGroup.class})
    @Min(value = 0,message = "表示順は0以上で入力してください",groups={CreateGroup.class,EditGroup.class})
    private Integer order;
    
}

2.3 カスタム検証ルールの実装

以下のインターフェースを定義して検証グループを管理します。

CreateGroup

public interface CreateGroup {
}

EditGroup

public interface EditGroup {
}

StatusEditGroup

public interface StatusEditGroup {
}

カスタム検証アノテーションの例:

CustomEnumValidator

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

    String message() default "{com.example.validator.CustomEnumValidator.message}";

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

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

    int[] allowedValues() default { };
}
  1. ビジネスロジックでの活用

/**
 * <p>
 * 商品管理コントローラー
 * </p>
 *
 * @author shiqi
 * @version 1.0.0
 * @createTime 2024-06-26
 */
@RestController
@RequestMapping("product")
public class ProductController {
    /**
     * 商品情報の保存処理。
     * <p>
     * @Validatedアノテーションでパラメータ検証を実施。
     * 検証結果はBindingResultで取得可能。
     * 
     * @param productEntity 保存対象の商品情報
     * @param bindingResult 検証結果
     * @return 操作結果を表すレスポンス
     */
    @PostMapping("/save")
    public Response save(@Validated({CreateGroup.class}) @RequestBody ProductEntity productEntity, BindingResult bindingResult) {
        // 実際の保存処理はここに記述
        return Response.success();
    }


}
  1. 異常処理の統一

4.1 ビジネスエラーコードの定義

/**
 * <p>
 * ビジネスエラーコード定義
 * </p>
 *
 * @author shiqi
 * @version 1.0.0
 * @createTime 2024-06-26
 */
public enum BusinessErrorCode {
    UNKNOWN_ERROR(10000,"システムエラー"),
    VALIDATION_ERROR(10001,"入力チェックエラー"),
    RATE_LIMIT_EXCEEDED(10002,"リクエスト頻度超過"),
    SMS_CODE_LIMIT(10003,"SMSコード取得頻度超過"),
    PRODUCT_PUBLISH_ERROR(11000,"商品公開エラー"),
    USER_ALREADY_EXISTS(15001,"同一ユーザー存在"),
    PHONE_ALREADY_EXISTS(15002,"同一電話番号存在"),
    STOCK_INSUFFICIENT(21000,"在庫不足"),
    LOGIN_CREDENTIALS_ERROR(15003,"ログイン情報エラー");


    private int code;

    private String message;


    BusinessErrorCode(int code, String message) {

        this.code = code;

        this.message = message;
    }


    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

4.2 レスポンス形式の統一

/**
 * <p>
 * 統一されたレスポンス形式
 * </p>
 *
 * @author shiqi
 * @version 1.0.0
 * @createTime 2024-06-26
 */
 public class Response extends HashMap<String, Object> {
    private static final long serialVersionUID = 1L;

    public Response setData(Object data) {
        put("data",data);
        return this;
    }

    //Fastjsonを使用したシリアライズ処理
    public <T> T getData(TypeReference<T> typeReference) {
        Object data = get("data");    //デフォルトはMap
        String jsonString = JSON.toJSONString(data);
        T t = JSON.parseObject(jsonString, typeReference);
        return t;
    }

    public <T> T getData(String key,TypeReference<T> typeReference) {
        Object data = get(key);    //デフォルトはMap
        String jsonString = JSON.toJSONString(data);
        T t = JSON.parseObject(jsonString, typeReference);
        return t;
    }

    public Response() {
        put("code", 0);
        put("message", "成功");
    }

    public static Response error() {
        return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知のエラーが発生しました");
    }

    public static Response error(String msg) {
        return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
    }

    public static Response error(int code, String msg) {
        Response response = new Response();
        response.put("code", code);
        response.put("message", msg);
        return response;
    }

    public static Response success(String msg) {
        Response response = new Response();
        response.put("message", msg);
        return response;
    }

    public static Response success(Map<String, Object> map) {
        Response response = new Response();
        response.putAll(map);
        return response;
    }

    public static Response success() {
        return new Response();
    }

    public Response put(String key, Object value) {
        super.put(key, value);
        return this;
    }

    public Integer getCode() {

        return (Integer) this.get("code");
    }

}

カスタム検証エラーハンドラ

/**
 * <p>
 * 全ての例外を集中処理するハンドラ
 * </p>
 *
 * @author shiqi
 * @version 1.0.0
 * @createTime 2024-06-26
 */

@Slf4j
@RestControllerAdvice(basePackages = {"com.shiqi.jsr303demo"})
public class GlobalExceptionHandler {

    /**
     * パラメータ検証失敗時の処理。
     * Spring MVCはMethodArgumentNotValidExceptionを発生させる。
     * このハンドラで統一的なエラーレスポンスを生成。
     *
     * @param e MethodArgumentNotValidExceptionインスタンス
     * @return エラーレスポンス
     */
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Response handleValidationException(MethodArgumentNotValidException e){
        // 検証結果を取得
        BindingResult bindingResult = e.getBindingResult();

        // エラーメッセージを格納するマップ
        HashMap<String,String> errorMap = new HashMap<>();

        // エラーがあれば処理
        if (bindingResult.hasErrors()){
            bindingResult.getFieldErrors().forEach(item -> {
                errorMap.put(item.getField(), item.getDefaultMessage());
            });
        }

        // エラーレスポンスを返す
        return Response.error(400,"入力チェック失敗").put("data",errorMap);
    }

    /**
     * 一般的な例外処理。
     * すべての例外をキャッチし、統一されたエラーレスポンスを返す。
     *
     * @param throwable 例外オブジェクト
     * @return エラーレスポンス
     */
    @ExceptionHandler(value = Throwable.class)
    private Response handleGeneralException(Throwable throwable) {
        // ログ出力
        log.error("例外発生: {} - タイプ: {}", throwable.getMessage(), throwable.getClass());
        // 一般的なエラーレスポンスを返す
        return Response.error();
    }


}
  1. 実際の業務での適用例

フォーム入力検証:ユーザー登録・ログイン・フォーム送信時のデータ整合性確認。 DTO検証:API通信時のデータ形式チェック。 ドメインオブジェクト検証:注文処理・支払い処理などビジネスロジック中の状態整合性確認。

JSR 303の使用により、手動での検証コードを削減し、コード構造の簡潔化と保守性向上が可能になります。実際の開発ではSpringフレームワークとHibernate Validatorの併用が一般的です。

タグ: Java Bean検証 Hibernate Validator Spring Boot カスタム検証

6月4日 19:50 投稿