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.
, . REST :
, :
. , 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: neverapplication.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, , , . .