特定のテーブルのデータ取得・追加・更新・削除を行うRest APIサービスでは、データ追加・更新する際は、@RequestBodyアノテーションでエンティティクラスのオブジェクトを渡すが、そのエンティティクラスのオブジェクトに対し、チェック処理を追加することもできる。
今回は、STS(Spring Tool Suite)を利用したSpring Bootアプリケーション上で、データ追加・更新する際に、@RequestBodyアノテーションで渡すエンティティクラスのオブジェクトにチェック処理を追加してみたので、そのサンプルプログラムを共有する。
前提条件
下記記事の実装が完了していること。
作成したサンプルプログラムの内容
作成したサンプルプログラムの構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。
pom.xml(追加分)の内容は以下の通りで、バリデーションチェックを行うためのライブラリを追加している。
1 2 3 4 5 | <!-- バリデーションチェックを行うためのライブラリを追加 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> |
また、application.propertiesの内容は以下の通りで、エラーメッセージのファイル名(messages.properties)を指定する設定を追加している。
1 2 3 4 5 6 7 8 9 10 11 | # ポート番号:8085で起動するよう設定 server.port = 8085 # Oracle DBの接続先を設定 spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe spring.datasource.username=USER01 spring.datasource.password=USER01 spring.datasource.driverClassName=oracle.jdbc.driver.OracleDriver # エラーメッセージのファイル名(messages.properties)を指定 spring.messages.basename=messages |
さらに、messages.propertiesの内容は以下の通りで、チェック処理でのエラーメッセージを定義している。
1 2 3 4 | #メッセージ validation.name-empty=名前を入力してください。 validation.date-invalidate=生年月日が存在しない日付になっています。 validation.sex-invalidate=性別は"1"(男)または"2"(女)を指定してください。 |
また、ユーザーデータのエンティティクラスの内容は以下の通りで、名前・生年月日・性別のチェックを行うアノテーションを追加している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | package com.example.demo; import lombok.Data; import javax.persistence.Entity; import javax.persistence.Table; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Pattern; import com.example.demo.check.CheckDate; import javax.persistence.Column; import javax.persistence.Id; /** * ユーザーデータテーブル(user_data)アクセス用エンティティ */ //テーブル名を「@Table」アノテーションで指定 //生年月日の日付チェックを「@CheckDate」アノテーションで指定 @Entity @Table(name="user_data") @Data @CheckDate(dtYear="birthY", dtMonth="birthM", dtDay="birthD" , message="{validation.date-invalidate}") public class UserData { /** ID */ //主キー項目に「@Id」を付与 @Id private long id; /** 名前 */ //必須チェックを「@NotEmpty」アノテーションで追加 @NotEmpty(message="{validation.name-empty}") private String name; /** 生年月日_年 */ //カラム名を「@Column」アノテーションで指定 @Column(name="birth_year") private int birthY; /** 生年月日_月 */ @Column(name="birth_month") private int birthM; /** 生年月日_日 */ @Column(name="birth_day") private int birthD; /** 性別 */ //性別の値チェック(1,2のいずれかであること)を「@Pattern」アノテーションで追加 @Pattern(regexp="[1-2]", message="{validation.sex-invalidate}") private String sex; /** メモ */ private String memo; } |
なお、ユーザーデータのエンティティクラスで生年月日の日付チェックを行うアノテーションの内容は、以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | package com.example.demo.check; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Documented; /** * 日付チェックを行うためのアノテーション */ //RetentionPolicyはclassファイルに記録され実行時に参照できるモード(Runtime)とする //JavaDoc指定対象(@Documented)とする //バリデーションの実装クラスはCheckDateValidatorクラスとする @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy={CheckDateValidator.class}) public @interface CheckDate { //表示するエラーメッセージ(アノテーション属性で指定) String message(); //特定のバリデーショングループがカスタマイズできるような設定 Class<?>[] groups() default {}; //チェック対象のオブジェクトになんらかのメタ情報を与えるためだけの宣言 Class<? extends Payload>[] payload() default {}; //チェック対象の日付_年(アノテーション属性で指定) String dtYear(); //チェック対象の日付_月(アノテーション属性で指定) String dtMonth(); //チェック対象の日付_日(アノテーション属性で指定) String dtDay(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | package com.example.demo.check; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; import org.springframework.util.StringUtils; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.ResolverStyle; /** * 日付チェックを行うためのアノテーション内でのチェック処理 */ public class CheckDateValidator implements ConstraintValidator<CheckDate, Object> { /** 日付のフォーマット */ private final static String dateFormat = "uuuuMMdd"; /** アノテーションで指定した年・月・日・メッセージの項目名 */ private String message; @Override public void initialize(CheckDate annotation) { // アノテーションで指定したメッセージの項目名を取得 this.message = annotation.message(); } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { // アノテーションで指定した年・月・日の項目値を取得 BeanWrapper beanWrapper = new BeanWrapperImpl(value); String year = String.valueOf((Integer)beanWrapper.getPropertyValue("birthY")); String month = String.valueOf((Integer)beanWrapper.getPropertyValue("birthM")); String day = String.valueOf((Integer)beanWrapper.getPropertyValue("birthD")); // 年・月・日が存在する日付でなければ、エラーメッセージ・エラー項目を設定し // falseを返す。そうでなければtrueを返す String dateStr = String.valueOf(year) + addZero(month) + addZero(day); if(!isCorrectDate(dateStr, dateFormat)){ context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate(message) .addConstraintViolation(); return false; } return true; } /** * DateTimeFormatterを利用して日付チェックを行う * @param dateStr チェック対象文字列 * @param dateFormat 日付フォーマット * @return 日付チェック結果 */ private static boolean isCorrectDate(String dateStr, String dateFormat){ if(!StringUtils.hasText(dateStr) || !StringUtils.hasText(dateFormat)){ return false; } // 日付と時刻を厳密に解決するスタイルで、DateTimeFormatterオブジェクトを作成 DateTimeFormatter df = DateTimeFormatter.ofPattern(dateFormat) .withResolverStyle(ResolverStyle.STRICT); try{ // チェック対象文字列をLocalDate型の日付に変換できれば、チェックOKとする LocalDate.parse(dateStr, df); return true; }catch(Exception e){ return false; } } /** * 数値文字列が1桁の場合、頭に0を付けて返す * @param intNum 数値文字列 * @return 変換後数値文字列 */ private static String addZero(String intNum){ if(!StringUtils.hasText(intNum)){ return intNum; } if(intNum.length() == 1){ return "0" + intNum; } return intNum; } } |
また、Restコントローラクラスの内容は以下の通りで、saveUserData・updateUserDataメソッドで渡すユーザーデータのエンティティクラスのチェック処理を行えるよう、@Validアノテーションを付与している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | package com.example.demo; import java.util.List; import java.util.Optional; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; /** * Rest API定義クラス */ @RestController public class DemoRestController { /** ユーザーデータテーブル(user_data)アクセス用リポジトリ */ @Autowired private UserDataRepository repository; /** * ユーザーデータリストを取得する. * @return ユーザーデータリスト */ @GetMapping("/users") public List<UserData> getAllUserData() { return repository.findAll(); } /** * 指定したIDをもつユーザーデータを取得する. * @param id ID * @return 指定したIDをもつユーザーデータ */ @GetMapping("/users/{id}") public UserData getOneUserData(@PathVariable long id) { Optional<UserData> userData = repository.findById(id); // 指定したIDをもつユーザーデータがあればそのユーザーデータを返す if(userData.isPresent()) { return userData.get(); } // 指定したIDをもつユーザーデータがなければnullを返す return null; } /** * 指定したユーザーデータを登録する. * @param userData ユーザーデータ * @return 登録したユーザーデータ */ @PostMapping("/users") public UserData saveUserData(@Valid @RequestBody UserData userData) { return repository.save(userData); } /** * 指定したユーザーデータを更新する. * @param id ID * @param userData ユーザーデータ * @return 更新したユーザーデータ */ @PutMapping("/users/{id}") public UserData updateUserData(@PathVariable long id , @Valid @RequestBody UserData userData) { userData.setId(id); return repository.save(userData); } /** * 指定したIDをもつユーザーデータを削除する. * @param id ID */ @DeleteMapping("/users/{id}") public void deleteUserData(@PathVariable long id) { Optional<UserData> userData = repository.findById(id); // 指定したIDをもつユーザーデータがあればそのユーザーデータを削除する if(userData.isPresent()) { repository.deleteById(id); } } } |
さらに、例外処理を行うハンドラークラスの内容は以下の通りで、エンティティクラスのチェック処理やURL指定でエラーになった場合の処理を行っている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | package com.example.demo.exception; import java.util.Date; import java.util.List; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; /** * 例外処理を行うハンドラークラス */ @ControllerAdvice @RestController public class DemoExceptionHandler { /** * APIのURLが不正な場合のエラー処理を行う. * @param ex MethodArgumentTypeMismatch例外 * @return レスポンスエンティティオブジェクト */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ResponseEntity<DemoExceptionResponse> handleMethodArgumentTypeMismatchException( MethodArgumentTypeMismatchException ex){ DemoExceptionResponse exceptionResponse = new DemoExceptionResponse( ex.getMessage(), getStackTraceLog(ex.getStackTrace()), new Date()); return new ResponseEntity<DemoExceptionResponse>( exceptionResponse, HttpStatus.BAD_REQUEST); } /** * UserDataクラスのチェック処理でエラーになった場合の処理を行う. * @param ex MethodArgumentNotValid例外 * @return レスポンスエンティティオブジェクト */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<DemoExceptionResponse> handleMethodArgumentNotValidException( MethodArgumentNotValidException ex){ DemoExceptionResponse exceptionResponse = new DemoExceptionResponse( getMethodArgumentNotValidExceptionMsg(ex), ex.getMessage(), new Date()); return new ResponseEntity<DemoExceptionResponse>( exceptionResponse, HttpStatus.BAD_REQUEST); } /** * その他例外が発生した場合の処理を行う. * @param ex 任意例外 * @return レスポンスエンティティオブジェクト */ @ExceptionHandler(Exception.class) public ResponseEntity<DemoExceptionResponse> handleException(Exception ex){ DemoExceptionResponse exceptionResponse = new DemoExceptionResponse( ex.getMessage(), getStackTraceLog(ex.getStackTrace()), new Date()); return new ResponseEntity<DemoExceptionResponse>( exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR); } /** * スタックトレースログの文字列を取得する. * @param stackArray StackTraceElement配列 * @return スタックトレースログの文字列 */ private String getStackTraceLog(StackTraceElement[] stackArray) { StringBuilder detail = new StringBuilder(); for(StackTraceElement stackTraceElement : stackArray) { detail.append(stackTraceElement); } return detail.toString(); } /** * MethodArgumentNotValid例外からエラーメッセージを生成する * @param ex MethodArgumentNotValid例外 * @return エラーメッセージ */ private String getMethodArgumentNotValidExceptionMsg( MethodArgumentNotValidException ex) { StringBuilder errMsg = new StringBuilder(); if(ex.getFieldErrorCount() > 0) { List<FieldError> fieldErrorList = ex.getFieldErrors(); for(FieldError error : fieldErrorList) { errMsg.append(error.getDefaultMessage()); } } if(ex.getGlobalErrorCount() > 0) { List<ObjectError> objectErrorList = ex.getGlobalErrors(); for(ObjectError error : objectErrorList) { errMsg.append(error.getDefaultMessage()); } } return errMsg.toString(); } } |
なお、上記ハンドラークラスで利用するレスポンスエンティティに指定するクラスの内容は、以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package com.example.demo.exception; import java.util.Date; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * 例外処理を行うハンドラークラス内で返却するレスポンスエンティティに指定するクラス */ //「@Data」アノテーションを付与すると、このクラス内の全フィールドに対する //Getterメソッド・Setterメソッドにアクセスができる //「@NoArgsConstructor」は、引数をもたないコンストラクタを生成するアノテーション //「@AllArgsConstructor」は、全てのメンバを引数にもつコンストラクタを生成するアノテーション @Data @NoArgsConstructor @AllArgsConstructor public class DemoExceptionResponse { /** メッセージ */ private String message; /** メッセージ詳細 */ private String details; /** 発生日時 */ private Date timestamp; } |
その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-rest-api-check/demoRestApi
サンプルプログラムの実行結果
サンプルプログラムの実行結果は、以下の通り。Spring Bootアプリケーションと、Postmanを起動した後で、それぞれの動作確認を行う。
1) 接続先となるOracleデータベース上に、以下のUSER_DATAテーブルとデータを作成しておく。
2) リクエストを「POST」、URLを「http://localhost:8085/users」、リクエストボディに(名前の)チェックエラーとなるデータをJSON形式で設定して「Send」ボタンを押下すると、以下のように、名前が未入力である旨のエラーが返却される。
3) リクエストを「POST」、URLを「http://localhost:8085/users」、リクエストボディに(生年月日の)チェックエラーとなるデータをJSON形式で設定して「Send」ボタンを押下すると、以下のように、生年月日が不正である旨のエラーが返却される。
4) リクエストを「POST」、URLを「http://localhost:8085/users」、リクエストボディに(性別の)チェックエラーとなるデータをJSON形式で設定して「Send」ボタンを押下すると、以下のように、性別が不正である旨のエラーが返却される。
5) リクエストを「POST」、URLを「http://localhost:8085/users」、リクエストボディに(名前・生年月日・性別の)チェックエラーとなるデータをJSON形式で設定して「Send」ボタンを押下すると、以下のように、それぞれのエラーがまとめて返却される。
6) リクエストを「PUT」、URLを「http://localhost:8085/users/3」、リクエストボディに(名前・生年月日・性別の)チェックエラーとなるデータをJSON形式で設定して「Send」ボタンを押下すると、POSTの場合と同様に、それぞれのエラーがまとめて返却される。
7) 1)~6)までの操作を実行後に、接続先となるOracleデータベース上のUSER_DATAテーブルを確認すると、データが更新されていないことが確認できる。
なお、追加・更新が正常終了する場合の結果は、以下の記事の実行結果と同様となる。
要点まとめ
- データ追加・更新する際に、@RequestBodyアノテーションで渡すエンティティクラスのオブジェクトでチェック処理を行えるようにするには、追加・更新するエンティティクラスに、チェックを行うアノテーションを追加すると共に、Restコントローラクラスで渡すエンティティクラスに、@Validアノテーションを付与すればよい。