NHN Cloud Meetup 編集部
Spring BindingResultをJSONで取得
2018.04.12
7,926
概要
SpringはControllerでValidationをした後、有効でない値が存在するとき、Error(BindingResult)にその内容を盛り込んで、JSP、FreeMarkerなどのView Template Engineでエラー内容をMessageSourceにグローバル化して表示できるように対応しています。しかし、そのようなグローバルメッセージをJSONで応答して表示させようとする場合、便利な方法がなかなか見当たりません。そこで、JSONでグローバル化されたエラー内容を取得できるように、Viewの内容をカスタマイズする方法について紹介したいと思います。
デフォルト動作のソースコード
依存性
Spring-Boot:2.0.0.RELEASE
lombok:1.16.18AdderController.java
以下はPOST /add?a=1&b=2を要請したとき、有効性チェック後に{“result”:3}を返却するソースです。
@RestController
@RequestMapping("/add")
public class AdderController {
private final Validator adderRequestValidator;
@Autowired
public AdderController(@Qualifier("adderRequestValidator") Validator adderRequestValidator) {
this.adderRequestValidator = adderRequestValidator;
}
@PostMapping
public AdderResult add(AdderRequest request, BindingResult bindingResult) throws BindException {
adderRequestValidator.validate(request, bindingResult);
if (bindingResult.hasErrors()) {
throw new BindException(bindingResult);
}
return new AdderResult(request.getA() + request.getB());
}
}
AdderRequestValidator.java
検証のために実装してみました。aやbが空の場合、field.requiredコード値でerrorsのfiledErrorsにエラー内容が追加されます。
@Component
public class AdderRequestValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return AdderRequest.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object o, Errors errors) {
AdderRequest request = AdderRequest.class.cast(o);
if (request.getA() == null) {
errors.rejectValue("a", "field.required");
}
if (request.getB() == null) {
errors.rejectValue("b", "field.required");
}
}
}
error.xml
field.requiredの内容を解析して、グローバル化するXMLプロパティを定義しました。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<entry key="field.required.adderRequest.a">aを入力してください</entry>
<entry key="field.required.adderRequest.b">bを入力してください</entry>
</properties>
無効な要請があったとき
何のカスタマイズもせずにPOST /add?a=1にリクエストを送ると、BindExceptionの内容をViewで応答します。内容は以下の通りです。
{
"timestamp": 1519659376474,
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.validation.BindException",
"errors":[{
"codes":[
"field.required.adderRequest.b",
"field.required.b",
"field.required.java.lang.Integer",
"field.required"
],
"arguments": null,
"defaultMessage": null,
"objectName": "adderRequest",
"field": "b",
"rejectedValue": null,
"bindingFailure": false,
"code": "field.required"
}],
"message": "Validation failed for object='adderRequest'. Error count: 1",
"path": "/add"
}
応答内容にerror.xmlで定義したメッセージの内容が降りてきません。(defaultMessageを定義すると、その値は満たされますが、グローバル化が適用されません。)
クライアントで言語関連リソースを持ち、コードを適切に対照して読み込めていればラッキーです。しかし、クライアントが1つでない場合、グローバル処理するロジックに重複が発生するので、サーバーを終了させるのが効果的でしょう。
サーバーからグローバルメッセージに変更
応答モデルの定義
モデルは任意のデータ構造で構いません。それぞれのクライアントの特徴に合わせて開発しよう。ここで以下のようなJSONを出力するように定義します。
{
"errors":[{
"objectName": "adderRequest",
"field": "b",
"code": "field.required",
"message": "bを入力してください"
}]
}
ValidationResult
エラーのリスト(errors)を持ち、必要に応じて共通の属性を追加定義できます。
@Value
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ValidationResult {
private List<FieldErrorDetail> errors;
public static ValidationResult create(Errors errors, MessageSource messageSource, Locale locale) {
List<FieldErrorDetail> details =
errors.getFieldErrors()
.stream()
.map(error -> FieldErrorDetail.create(error, messageSource, locale))
.collect(Collectors.toList());
return new ValidationResult(details);
}
}
FieldErrorDetail
FieldErrorの詳細を記述するクラスです。
@Value
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class FieldErrorDetail {
private String objectName;
private String field;
private String code;
private String message;
public static FieldErrorDetail create(FieldError fieldError, MessageSource messageSource, Locale locale) {
return new FieldErrorDetail(
fieldError.getObjectName(),
fieldError.getField(),
fieldError.getCode(),
messageSource.getMessage(fieldError, locale)); // この部分がポイント
}
}
messageSource.getMessage(MessageSourceResolvable, Locale)を使ってXMLに定義したグローバルメッセージを読み込むことができます。これが可能な理由は、FieldErrorがMessageSourceResolvableを実装しているからです。
ExceptionHandler定義
APIサーバーであれば@RestControllerAdviceなどを使って、コントローラのアドバイスに登録させるのも良い方法です。
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ValidationResult handleBindException(BindException bindException, Locale locale) {
return ValidationResult.create(bindException, messageSource, locale);
}
応答
{
"errors":[{
"objectName": "adderRequest",
"field": "b",
"code": "field.required",
"message": "bを入力してください"
}]
}
結論
基本的に、SpringはBindExceptionに対してExceptionの内容をJSONで表示するだけです。コード値は取得できますが、結局コードに対応するグローバルメッセージをクライアントから解析しなければなりません。しかし複数のクライアントに対応するためには、サーバーからのグローバルコードを解釈して与える方がよいでしょう。
グローバルメッセージを取得するためには、BindExceptionでFieldErrorを読み込んでmessageSourceを使う必要があります。上記のカスタマイズを経て、はじめて希望するグローバルメッセージを読み込むことができるという点が少し残念です。