- 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:注釈付き要素が有効なメールアドレスであるかの検証。
- 使用手順
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 { };
}
- ビジネスロジックでの活用
/**
* <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();
}
}
- 異常処理の統一
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();
}
}
- 実際の業務での適用例
フォーム入力検証:ユーザー登録・ログイン・フォーム送信時のデータ整合性確認。 DTO検証:API通信時のデータ形式チェック。 ドメインオブジェクト検証:注文処理・支払い処理などビジネスロジック中の状態整合性確認。
JSR 303の使用により、手動での検証コードを削減し、コード構造の簡潔化と保守性向上が可能になります。実際の開発ではSpringフレームワークとHibernate Validatorの併用が一般的です。