NHN Cloud Meetup 編集部
Spring BindingResultをJSONで取得
2018.04.12
7,734
概要
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を使う必要があります。上記のカスタマイズを経て、はじめて希望するグローバルメッセージを読み込むことができるという点が少し残念です。