73
75

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Spring BootでつくったAPIのリクエストのバリデーションで出るExceptionのまとめ

Last updated at Posted at 2017-10-19

このドキュメントについて

  • リクエストパラメータでバリデーションエラーが発生する際に発生するExceptionの種類がなんか色々ありそうだったので調査してみた
  • 完全に網羅できていないとは思うけど自分なりに使いそうだなーっていうケースを調べた
  • 今回調査したのは以下のケース
    • ①パスのパラメータ
    • ②引数で指定するケース
    • ③引数でDTOを指定するケース
    • ④引数で@RequestBodyをつけてDTOを指定するケース(JSON)

先に結論

  • 調査したケースとExcpetionの対応は以下の表の通り
  • 引数でバリデーションするものはConstraintViolationExceptionになってそうでないものは下記に従うっ て言う感じだった
  • ちなみに複数バリデーションが発生するケースも試してみたけどその場合はDTOを使っているパラメータのバリデーションエラーが優先して発生していた
  • 引数で指定している部分を同時にバリデーションエラーが発生させた場合はどっちもConstraintViolationExceptionなのでどっちの内容も拾えていた
ケース Exceptionの内容
①パスのパラメータ ConstraintViolationException
②引数で指定するケース ConstraintViolationException
③引数でDTOを指定するケース BindException
④引数で@RequestBodyをつけてDTOを指定するケース(JSON) MethodArgumentNotValidException

調査した内容

ExceptionをハンドリングするクラスとリクエストをさばくControllerを用意して、実際にどこでキャッチしているかをリクエストして調査した

ソース

validation-verification

調査する際に準備したクラス

Controllerの実装
@Validated
@RestController
@RequestMapping("/v1/verify")
public class ValidationVerificationController {

    @PostMapping("/{path}")
    public String verify(@PathVariable(value = "path") @Valid @Size(max = 3) String path, // ①パスのパラメータ
                       @Valid @Size(max = 3) @NotNull String arg, // ②引数で指定するケース
                       @Valid ValidationVerificationGetRequest validationVerificationGetRequest, // ③引数でDTOを指定するケース
                       @RequestBody @Valid ValidationVerificationBodyRequest validationVerificationBodyRequest // ④引数で@RequestBodyをつけてDTOを指定するケース(JSON)
    ) {
        return "ok";
    }
}
③でつかう引数で渡しているDTOの実装
package com.example.demo;

import lombok.Data;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@Data
public class ValidationVerificationGetRequest {

    @Size(max = 3)
    @NotNull
    private String dto;
}

④で使う引数で渡しているRequestBodyに渡すときのDTOの実装
package com.example.demo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import org.hibernate.validator.constraints.NotBlank;

import javax.validation.constraints.Max;
import javax.validation.constraints.Size;

@Getter
@Builder
public class ValidationVerificationBodyRequest {

    @NotBlank
    @Size(max = 3)
    private String str;

}

Exceptionをハンドリングするクラス
package com.example.demo;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
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 RestControllerExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
        if (body == null) {
            body = ex.getClass().toString();
        }
        return super.handleExceptionInternal(ex, body, headers, status, request);
    }

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        return handleExceptionInternal(ex, "MethodArgumentNotValid",headers, status, request);
    }

    @Override
    protected ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        return handleExceptionInternal(ex, "BindException",headers, status, request);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<Object> handleConstraintViolation(
            ConstraintViolationException ex, WebRequest request) {
        return handleExceptionInternal(ex, "Constraint", null, HttpStatus.BAD_REQUEST, request);
    }

}


それぞれのテストコード

package com.example.demo;

import net.minidev.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import org.springframework.test.context.junit4.SpringRunner;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ValidationVerifiationControllerFunctionalTest {

    @Autowired
    private TestRestTemplate testRestTemplate;

    @Test
    public void 正常系() {
        String body = "{\"str\":\"123\"}";

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        ResponseEntity actual = testRestTemplate.exchange(
                "/v1/verify/all/123?first=123&second=1&arg=123", HttpMethod.POST, new HttpEntity<>(body, headers), String.class);

        assertThat(actual.getBody().toString(), is("ok"));
        assertThat(actual.getStatusCode(), is(HttpStatus.OK));
    }

    @Test
    public void パスでバリデーションエラー() {
        String body = "{\"str\":\"123\"}";

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        ResponseEntity actual = testRestTemplate.exchange(
                "/v1/verify/all/1234?first=123&second=1&arg=123", HttpMethod.POST, new HttpEntity<>(body, headers), String.class);

        assertThat(actual.getBody().toString(), is("ConstraintViolationException"));
        assertThat(actual.getStatusCode(), is(HttpStatus.BAD_REQUEST));
    }

    @Test
    public void 引数でバリデーションエラー() {
        String body = "{\"str\":\"123\"}";

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        ResponseEntity actual = testRestTemplate.exchange(
                "/v1/verify/all/123?first=123&second=1&arg=1234", HttpMethod.POST, new HttpEntity<>(body, headers), String.class);

        assertThat(actual.getBody().toString(), is("ConstraintViolationException"));
        assertThat(actual.getStatusCode(), is(HttpStatus.BAD_REQUEST));
    }

    @Test
    public void DTOでバリデーションエラー() {
        String body = "{\"str\":\"123\"}";

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        ResponseEntity actual = testRestTemplate.exchange(
                "/v1/verify/all/123?first=1234&second=1&arg=123", HttpMethod.POST, new HttpEntity<>(body, headers), String.class);

        assertThat(actual.getBody().toString(), is("BindException"));
        assertThat(actual.getStatusCode(), is(HttpStatus.BAD_REQUEST));
    }

    @Test
    public void Bodyでバリデーションエラー() {
        String body = "{\"str\":\"1234\"}";

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        ResponseEntity actual = testRestTemplate.exchange(
                "/v1/verify/all/123?first=123&second=1&arg=123", HttpMethod.POST, new HttpEntity<>(body, headers), String.class);

        assertThat(actual.getBody().toString(), is("MethodArgumentNotValidException"));
        assertThat(actual.getStatusCode(), is(HttpStatus.BAD_REQUEST));
    }

}

雑感

  • 発生するExceptionが違うと先に発生するものしか拾えないので同じAPIのパラメータは同一のやりかたでやったほうが良さそう
  • そもそもなんでこうなるかがちゃんとわかってないからControllerの実装みて確認したい
  • めっちゃ関係ないけどテストのときにlombokのアノテーション効かないところでハマったから設定ちゃんと全部のプロジェクトで共有するようにせなあかん
  • タイトルなげえ
73
75
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
73
75

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?