التحقق من الصحة والاستثناءات التعامل مع الربيع

في كل مرة أبدأ في تنفيذ واجهة برمجة تطبيقات REST جديدة باستخدام Spring ، أجد صعوبة في تحديد كيفية التحقق من صحة الطلبات والتعامل مع استثناءات الأعمال. على عكس مشكلات واجهة برمجة التطبيقات الشائعة الأخرى ، لا يبدو أن Spring ومجتمعها يتفقان على أفضل الممارسات لحل هذه المشكلات ، ومن الصعب العثور على مقالات مفيدة حول هذا الموضوع.

في هذه المقالة ، ألخص تجربتي وأقدم بعض النصائح حول التحقق من الواجهة.

العمارة والمصطلحات

أقوم بإنشاء تطبيقاتي الخاصة التي توفر Web-API ، باتباع نمط  هندسة البصل  ( Onion Architecture ) . لا تتناول هذه المقالة هندسة Onion ، لكني أود أن أذكر بعض نقاطها الرئيسية المهمة في فهم أفكاري:

  • تعتبر وحدات تحكم REST  وأي مكونات وتكوينات ويب جزءًا من طبقة "البنية التحتية" الخارجية  .

  •  تحتوي طبقة "الخدمة" الوسطى  على خدمات تدمج وظائف الأعمال وتحل المشكلات الشائعة مثل الأمان أو المعاملات.

  •  تحتوي طبقة "المجال" الداخلية  على منطق الأعمال بدون أي مهام متعلقة بالبنية التحتية مثل الوصول إلى قاعدة البيانات ونقاط نهاية الويب وما إلى ذلك.

رسم تخطيطي لطبقات هندسة البصل ووضع فصول الربيع النموذجية.
Spring.

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