S-JIS[2017-09-08] 変更履歴

Spring Boot エンティティー精査

Spring Bootのエンティティーの精査方法のメモ。


概要

Spring Boot(1.5.6)ではJSONをエンティティーに変換して受け取ることが出来るが、そのエンティティーに対して精査(バリデーション)をすることも出来る。

精査にはHibernateを使っているようだが、spring-boot-starter-data-restが依存関係に入っていれば、自動的に使用できる。

build.gradle:

〜
dependencies {
	compile('org.springframework.boot:spring-boot-starter-data-rest')
	compile('org.springframework.boot:spring-boot-starter-security')
	compile('org.springframework.boot:spring-boot-starter-thymeleaf')
	runtime('org.springframework.boot:spring-boot-devtools')
	testCompile('org.springframework.boot:spring-boot-starter-test')
	testCompile('org.springframework.security:spring-security-test')
}

RestControllerでJSONを受け取る際に精査を行う例。

src/main/java/com/example/demo/entity/ValidateExampleEntity.java:

package com.example.demo.entity;

import javax.validation.constraints.Digits;
import javax.validation.constraints.NotNull;
public class ValidateExampleEntity {

	@NotNull
	public String value1;

	@Digits(integer = 3, fraction = 1)
	public String value2;
}

精査を行いたいエンティティーには、条件を表すアノテーションを付ける。
例えば@NotNullアノテーションは必須項目(null以外)、@Digitsアノテーションは数値(整数部の最大桁数と小数部の最大桁数を指定)。

ちなみに、これらのアノテーションはjavaxパッケージになっているが、JBossのOSSらしい。(Hibernateを入れると、一緒に入ってくる)

src/main/java/com/example/demo/rest/ValidateController.java:

package com.example.demo.rest;

import java.util.Map;

import javax.validation.Valid;

import org.springframework.http.HttpStatus;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.entity.ValidateExampleEntity;
@RestController
public class ValidateController {

	@RequestMapping(path = "/valid/example1")
	public ValidateExampleEntity example1(@RequestBody @Valid ValidateExampleEntity entity) {
		return entity;
	}

RestControllerでエンティティーを受け取るメソッドに@Validアノテーションを付けると、そのエンティティーに対して精査が行われる。
デフォルトでは、精査エラーが起きるとMethodArgumentNotValidExceptionが発生するので、その例外に対する例外ハンドラーを作成する。

	@ExceptionHandler(MethodArgumentNotValidException.class)
	@ResponseStatus(HttpStatus.BAD_REQUEST)
	public Map<String, ?> handleException(MethodArgumentNotValidException exception) {
		MultiValueMap<String, String> map = new LinkedMultiValueMap<>();

		BindingResult result = exception.getBindingResult();
		for (ObjectError error : result.getGlobalErrors()) {
			map.add("global", error.getDefaultMessage());
		}
		for (FieldError error : result.getFieldErrors()) {
			map.add(error.getField(), error.getDefaultMessage());
		}

		return map;
	}
}

MethodArgumentNotValidExceptionから精査エラーの情報(BindingResult)を取得できる。
BindingResultは複数の精査エラーを保持していることもある。
(BindingResultはErrorsインターフェースを実装している)

実行例
  正常系 精査エラー
JSON {
  "value1" : "a",
  "value2" : "123.0"
}
{
  "value2" : "1234.0"
}
 
実行結果 {
  "value1" : "a",
  "value2" : "123.0"
}
{
  "value1" : [ "may not be null" ],
  "value2" : [ "numeric value out of bounds (<3 digits>.<1 digits> expected)" ]
}

Errorsの例

精査エラーを、例外ハンドラーを介さずにメソッドで直接受け取ることも出来る。

src/main/java/com/example/demo/rest/ValidateController.java:

import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
	@RequestMapping(path = "/valid/example2")
	public Object example2(@RequestBody @Valid ValidateExampleEntity entity, Errors errors) {
		if (!errors.hasErrors()) {
			return entity;
		}

		MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
		for (ObjectError error : errors.getGlobalErrors()) {
			map.add("global", error.getDefaultMessage());
		}
		for (FieldError error : errors.getFieldErrors()) {
			map.add(error.getField(), error.getDefaultMessage());
		}
		return map;
	}

メソッドの引数にErrorsを追加する。
精査エラーがあると、Errorsに精査エラーの情報が入ってくる。

実行例
  正常系 精査エラー
JSON {
  "value1" : "a",
  "value2" : "123.0"
}
{
  "value2" : "1234.0"
}
 
実行結果 {
  "value1" : "a",
  "value2" : "123.0"
}
value2=numeric+value+out+of+bounds+%28%3C3+digits%3E.%3C1+digits%3E+expected%29&value1=may+not+be+null

なんか、精査エラー時の結果がJSONになってないけど^^;


@RequestMappingアノテーションのproducesで「application/json」を指定してやると、(精査エラー時でも)JSONで返るようだ。

参考: stackoverflowのSpring MVC - How to return simple String as JSON in Rest Controller

import org.springframework.http.MediaType;
	@RequestMapping(path = "/valid/example2", produces = MediaType.APPLICATION_JSON_VALUE)
	public Object example2(@RequestBody @Valid ValidateExampleEntity arg, Errors errors) {
〜
	}
実行例
  正常系 精査エラー
JSON {
  "value1" : "a",
  "value2" : "123.0"
}
{
  "value2" : "1234.0"
}
 
実行結果 {
  "value1" : "a",
  "value2" : "123.0"
}
{
  "value2" : [ "numeric value out of bounds (<3 digits>.<1 digits> expected)" ],
  "value1" : [ "may not be null" ]
}

なお、Errorsを受け取る方法では精査エラー時も当然HTTPステータスOK(200)で返るので、それを変えたいなら、ResponseEntityを返すようにする。

import org.springframework.http.ResponseEntity;
	@RequestMapping(path = "/valid/example3", produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<?> example3(@RequestBody @Valid ValidateExampleEntity entity, Errors errors) {
		if (!errors.hasErrors()) {
			return new ResponseEntity<>(entity, HttpStatus.OK);
		}

		MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
		for (ObjectError error : errors.getGlobalErrors()) {
			map.add("global", error.getDefaultMessage());
		}
		for (FieldError error : errors.getFieldErrors()) {
			map.add(error.getField(), error.getDefaultMessage());
		}
		return new ResponseEntity<>(map, null, HttpStatus.BAD_REQUEST);
	}

Spring Bootへ戻る / Spring Frameworkへ戻る / 技術メモへ戻る
メールの送信先:ひしだま