Validation et gestion des exceptions avec Spring

Chaque fois que je commence à implémenter une nouvelle API REST à l'aide de Spring, je trouve difficile de décider comment valider les demandes et gérer les exceptions commerciales. Contrairement à d'autres problèmes d'API courants, Spring et sa communauté ne semblent pas s'entendre sur les meilleures pratiques pour résoudre ces problèmes, et il est difficile de trouver des articles utiles sur le sujet.

Dans cet article, je résume mon expérience et donne quelques conseils sur la validation d'interface.

Architecture et terminologie

Je crée mes propres applications qui fournissent des API Web, en suivant le modèle  de l'architecture onion  ( Onion Architecture ) . Cet article ne porte pas sur l'architecture Onion, mais je voudrais mentionner certains de ses points clés qui sont importants pour comprendre mes pensées:

  • Les contrôleurs REST  et tous les composants et configurations Web font partie de la  couche «infrastructure» externe  .

  • Le niveau «service» intermédiaire   contient des services qui intègrent des fonctions commerciales et traitent des problèmes courants tels que la sécurité ou les transactions.

  • La couche "domaine" interne   contient une logique métier sans aucune tâche liée à l'infrastructure, telle que l'accès à la base de données, les points de terminaison Web, etc.

Un croquis des couches d'architecture d'oignon et le placement des classes typiques de Spring.
Spring.

, .   REST  :

  •    «».

  • - - .

  • ,    ,     ( ).

  •    , , , .

  • , .

  •   -.  .

  • , , .

Validation au niveau de la demande, du service et du domaine.
, .

, :

  • .  ,   API .  , Jackson,  ,  @NotNull.     .

  • , .  .

  • , . .

   , . Spring Boot Jackson . ,      BGG:

@GetMapping("/newest")
Flux<ThreadsPerBoardGame> getThreads(@RequestParam String user, @RequestParam(defaultValue = "PT1H") Duration since) {
    return threadService.findNewestThreads(user, since);
}

          :

curl -i localhost:8080/threads/newest
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: 189

{"timestamp":"2020-04-15T03:40:00.460+0000","path":"/threads/newest","status":400,"error":"Bad Request","message":"Required String parameter 'user' is not present","requestId":"98427b15-7"}

curl -i "localhost:8080/threads/newest?user=chrigu&since=a"
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: 156

{"timestamp":"2020-04-15T03:40:06.952+0000","path":"/threads/newest","status":400,"error":"Bad Request","message":"Type mismatch.","requestId":"7600c788-8"}

Spring Boot    .  ,

server:
  error:
    include-stacktrace: never

 application.yml .    BasicErrorController   Web MVC  DefaultErrorWebExceptionHandler  WebFlux, ErrorAttributes.

  @RequestParam  .   @ModelAttribute , @RequestBody  ,

@GetMapping("/newest/obj")
Flux<ThreadsPerBoardGame> getThreads(@Valid ThreadRequest params) {
    return threadService.findNewestThreads(params.user, params.since);
}

static class ThreadRequest {
    @NotNull
    private final String user;
    @NotNull
    private final Duration since;

    public ThreadRequest(String user, Duration since) {
        this.user = user;
        this.since = since == null ? Duration.ofHours(1) : since;
    }
}

@RequestParam ,       ,     bean-,  @NotNull Java / Kotlin.  bean-,  @Valid.

bean- ,  BindException  WebExchangeBindException .  BindingResult, .  ,

curl "localhost:8080/java/threads/newest/obj" -i
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: 1138

{"timestamp":"2020-04-17T13:52:39.500+0000","path":"/java/threads/newest/obj","status":400,"error":"Bad Request","message":"Validation failed for argument at index 0 in method: reactor.core.publisher.Flux<ch.chrigu.bgg.service.ThreadsPerBoardGame> ch.chrigu.bgg.infrastructure.web.JavaThreadController.getThreads(ch.chrigu.bgg.infrastructure.web.JavaThreadController$ThreadRequest), with 1 error(s): [Field error in object 'threadRequest' on field 'user': rejected value [null]; codes [NotNull.threadRequest.user,NotNull.user,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [threadRequest.user,user]; arguments []; default message [user]]; default message [darf nicht null sein]] ","requestId":"c87c7cbb-17","errors":[{"codes":["NotNull.threadRequest.user","NotNull.user","NotNull.java.lang.String","NotNull"],"arguments":[{"codes":["threadRequest.user","user"],"arguments":null,"defaultMessage":"user","code":"user"}],"defaultMessage":"darf nicht null sein","objectName":"threadRequest","field":"user","rejectedValue":null,"bindingFailure":false,"code":"NotNull"}]}

, , API.  Spring Boot:

curl "localhost:8080/java/threads/newest/obj?user=chrigu&since=a" -i
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
Content-Length: 513

{"timestamp":"2020-04-17T13:56:42.922+0000","path":"/java/threads/newest/obj","status":500,"error":"Internal Server Error","message":"Failed to convert value of type 'java.lang.String' to required type 'java.time.Duration'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.time.Duration] for value 'a'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [a]","requestId":"4c0dc6bd-21"}

, , since.  , MVC .  .  , bean- ErrorAttributes ,    .  status.

DefaultErrorAttributes,   @ResponseStatus, ResponseStatusException .  .  , , , , .  - @ExceptionHandler . , , . , , (rethrow):

@ControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(TypeMismatchException::class)
    fun handleTypeMismatchException(e: TypeMismatchException): HttpStatus {
        throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid value '${e.value}'", e)
    }

    @ExceptionHandler(WebExchangeBindException::class)
    fun handleWebExchangeBindException(e: WebExchangeBindException): HttpStatus {
        throw object : WebExchangeBindException(e.methodParameter!!, e.bindingResult) {
            override val message = "${fieldError?.field} has invalid value '${fieldError?.rejectedValue}'"
        }
    }
}

Spring Boot , , , Spring.  , , , :

  •  try/catch (MVC)  onErrorResume() (Webflux).  , , , , .

  •   @ExceptionHandler .  @ExceptionHandler (Throwable.class) .

  •    , @ResponseStatus ResponseStatusException, .

Spring Boot , .  , , .

, .  , ,   , , Java Kotlin,    , ,  .   .




All Articles