概要
Spring Boot でGETのリクエストパラメータのバリデーションのやり方と、バリデーションに引っかかった際に投げられる例外のハンドリングのやり方、そしてレスポンスにバリデーションのエラーメッセージを含めるやり方のメモ。
前提
-
@RestController
を用いたJSON API - GETでパラメータを渡す。そのパラメータにバリデーションをかける
環境
Java: 1.8
SpringBoot: 1.5.7
やってみる
Spring Initializerで適当にProjectを作っておく。
DependencyにはWebとlombockを指定する。
まずバリデーションなしバージョン
以下のようなコントローラを作る。
/hello
にパラメータにメールアドレスを渡してアクセスしたら挨拶が返ってくるだけ。
package com.example.sample;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping
public String get(@RequestParam("email") String email) {
return "Hello!, " + email;
}
}
起動して叩いてみる
メールアドレスではない文字列を渡しても、当然素通りしてしまう。
$ curl "http://localhost:8080/hello?email=hogehoge"
Hello, hogehoge
ここにバリデーションを足してみる。
バリデーションありバージョン
実装方法
コントローラを修正する
package com.example.sample;
import org.hibernate.validator.constraints.Email;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/hello")
@Validated // 追加
public class HelloController {
@GetMapping
// @Emailと@Validを追加
public String get(@Email @Valid @RequestParam("email") String email) {
return "Hello!, " + email;
}
}
叩いてみる。
妥当なメールアドレスを渡せば正常なレスポンスが帰ってくる。
無効なメールアドレスを渡すとExceptionの内容が返ってくる。
$ curl "http://localhost:8080/hello?email=hogehoge"
{"timestamp":1507806432128,"status":500,"error":"Internal Server Error","exception":"javax.validation.ConstraintViolationException","message":"No message available","path":"/hello"}
これでバリデーションはできた。
解説
-
@Validated
アノテーションをコントローラの定義文のところに追加するとバリデーションが効くようになる。
(POSTの@RequestBodyに対してバリデーションする際はいらなかったので少し混乱した。この記事を参考にGETパラメータに対してバリデーションする際は@Validated
アノテーションが必要だとわかった。 - バリデーション対象のパラメータ(今回は
@RequestParam("email") String email
)の前に@Valid
アノテーションをつけて、重ねて任意のバリデーション用のアノテーションを付与するとバリデーションが動くようになる。今回は@Email
アノテーションを付与した。
例外をハンドリングする
現状ではバリデーションに引っかっかった際のレスポンスの中身がカスタマイズできない。
バリデーションに引っかかった際に投げられたExceptionの中身をSpringがよしなにJSONに変換して返してくれているだけ。
自分で例外をキャッチして任意のフォーマットでレスポンスを返せるようにする。
実装方法
package com.example.sample;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.validation.ConstraintViolationException;
@RestControllerAdvice
public class HelloExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler
public ResponseEntity<Object> handleConstraintViolationException(ConstraintViolationException ex, WebRequest request) {
return super.handleExceptionInternal(ex, "validation error", null, HttpStatus.BAD_REQUEST, request);
}
}
叩いてみる。
バリデーションに引っかかると validation errorと返ってくる
$ curl "http://localhost:8080/hello?email=hogehoge"
validation error
これで任意のフォーマットでレスポンスを返せるようになった。
解説
例外ハンドラ用のクラスを作っている。
ResponseEntityExceptionHandler
クラスを継承して、@RestControllerAdvice
アノテーションを付与する。
中にバリデーションに引っかっかった際にスローされる例外をハンドリングするメソッドを作る。
@ExceptionHandler
アノテーションを付与して、第一引数にキャッチしたい例外の型を指定すると、その例外が投げられたときにそのメソッドが呼ばれる。
レスポンスの中身を親切にする
バリデーションに引っかっかった際にバリデーションエラーの内容をレスポンスに含めてみる。
実装方法
レスポンス用のクラスを作る
package com.example.sample;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Getter
@RequiredArgsConstructor
public class ApiValidationErrorResponse implements Serializable {
private final String message;
@Getter
@RequiredArgsConstructor
private static class ValidationMessage implements Serializable {
private final String message;
}
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<ValidationMessage> validationMessages = new ArrayList<>();
public void addValidationMessage(String message) {
validationMessages.add(new ValidationMessage(message));
}
}
HelloExceptionHandler
クラスを修正する。
package com.example.sample;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.validation.ConstraintViolationException;
@RestControllerAdvice
public class HelloExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler
public ResponseEntity<Object> handleConstraintViolationException(ConstraintViolationException ex, WebRequest request) {
ApiValidationErrorResponse error = new ApiValidationErrorResponse("validation error");
ex.getConstraintViolations().forEach(v -> error.addValidationMessage(v.getMessage()));
return super.handleExceptionInternal(ex, error, null, HttpStatus.BAD_REQUEST, request);
}
}
叩いてみる。
レスポンスにどんなバリデーションに引っかかったのか、詳細な情報が含まれている。
$ curl "http://localhost:8080/hello?email=h"
{"message":"validation error","validationMessages":[{"message":"not a well-formed email address"}]}
解説
- レスポンス用のクラス
ApiValidationErrorResponse
を作っている。Serializable
インタフェースを継承しておけば、よしなにJSONに変換してくれる。 - lombock使ってます。使わない場合は自分でゲッターやコンストラクタ書いてください。
-
@JsonInclude(JsonInclude.Include.NON_EMPTY)
の部分は、バリデーションメッセージが空のときはvalidationMessageフィールドをJSONに含めないようにするためのアノテーションです。 -
handleConstraintViolationException
メソッドの中で上で作ったレスポンス用のクラスを生成している。
参考
- Validating RequestParams and PathVariables in Spring MVC - {code that works} by Sadique Ali
- Spring徹底入門 株式会社NTTデータ 2016