Validierung und Ausnahmebehandlung mit Spring

Jedes Mal, wenn ich mit Spring eine neue REST-API implementiere, fällt es mir schwer zu entscheiden, wie Anforderungen validiert und Geschäftsausnahmen behandelt werden sollen. Im Gegensatz zu anderen gängigen API-Problemen scheinen sich Spring und seine Community nicht auf die Best Practices zur Lösung dieser Probleme zu einigen, und es ist schwierig, hilfreiche Artikel zu diesem Thema zu finden.

In diesem Artikel fasse ich meine Erfahrungen zusammen und gebe einige Ratschläge zur Schnittstellenvalidierung.

Architektur und Terminologie

Ich schaffe meine eigene Anwendungen , die Web-API zur Verfügung stellen, nach dem Muster  der Zwiebel Architektur  ( Zwiebel Architektur ) . In diesem Artikel geht es nicht um die Zwiebelarchitektur, aber ich möchte einige ihrer wichtigsten Punkte erwähnen, die für das Verständnis meiner Gedanken wichtig sind:

  • REST-Controller  sowie alle Webkomponenten und -konfigurationen sind Teil der externen  "Infrastruktur" -Schicht  .

  • Die mittlere  "Service" -Ebene  enthält Services, die Geschäftsfunktionen integrieren und allgemeine Probleme wie Sicherheit oder Transaktionen beheben.

  • Die innere "Domänen" -Schicht   enthält Geschäftslogik ohne infrastrukturbezogene Aufgaben wie Datenbankzugriff, Webendpunkte usw.

Eine Skizze der Zwiebelarchitekturschichten und die Platzierung typischer Frühlingsklassen.
Spring.

, .   REST  :

  •    «».

  • - - .

  • ,    ,     ( ).

  •    , , , .

  • , .

  •   -.  .

  • , , .

Validierung auf Anforderungs-, Service- und Domänenebene.
, .

, :

  • .  ,   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