Рубрики
Без рубрики

Мертвая простая проверка API REST с помощью Spring Boot (серьезно, очень просто)

Этот пост написан в довольно неторопливой манере проверки API REST… Я серьезно думаю, что этот человек… Помеченный java, ломбок, проверка, весна.

Этот пост написан довольно неторопливо

Проверка API REST… Я серьезно думаю, что людям на самом деле не нравится это делать, или они просто делают это небрежно. Другими словами, люди не заботятся о проверке так сильно, как следовало бы. Конечный результат на самом деле не так уж катастрофичен, по крайней мере, в экосистеме Spring Framework. Обычно какой-то метод, находящийся глубже в стеке вызовов, выдает исключение |, а Spring позаботится обо всем остальном. Вероятно, в качестве ответа вы получите код состояния HTTP 4xx или 5xx с некоторым (обычно) неразборчивым стеком вызовов. Кто в любом случае читает стеки вызовов из HTTP-ответов, мы не сумасшедшие. Кто-то может сказать, что это не так уж важно, так как есть большая вероятность, что никакого вреда не было причинено, и конечные пользователи этого не заметят. Ну, за исключением того, если вы покажете им все сообщения об ошибках HTTP, и в этом случае ваша ошибка.

О, черт. Нет.

Серьезно, сделай свою проверку. Делай это правильно. Ты будешь счастливее.

У Spring может быть довольно пугающая и сложная логика проверки, когда вы впервые смотрите на нее. Хотя это и не обязательно должно быть так.

В этом посте я покажу вам один способ. Так, как я люблю это делать. Мы проверим некоторые параметры запроса.

Скорее всего, если вы программируете на Java, вы уже используете Lombok. Очень хорошо. Ломбок безумно потрясающий, и я действительно рад, что у меня есть шанс им воспользоваться.

Если вы не знаете, что Ломбок делает для вас, вот несколько основных вещей, которые он может сделать:

  • создайте все геттеры и сеттеры для полей вашего класса
  • генерировать конструкторы со всеми/без параметров
  • создайте Конструктор для своего класса
  • генерировать Хэш-код() и toString() методы для вашего класса
  • выполняйте все виды сопоставления данных с делегатами

Для целей этого поста мы будем использовать только аннотацию @Data , которая не будет генерировать конструктор аргументов и все геттеры и сеттеры.

Мы не собираемся здесь углубляться в основы Spring, что такое Maven/Gradle и другие ненужные вещи. Единственное, что вам нужно сделать, если вы хотите следовать этому руководству, это:

  1. Перейти к https://start.spring.io/
  2. Добавить Весеннюю паутину и Ломбок как зависимости
  3. Выберите Java в качестве языка (если вы еще не поняли, что этот пост посвящен Java)

Хорошо, допустим, в качестве примера я хочу создать конечную точку, которая принесет нам некоторые сущности. Какой тип сущностей, в данный момент нас совершенно не волнует, единственное, что мы хотим сделать прямо сейчас, – это проверить параметры, поступающие в метод контроллера.

Если вы немного запутались, параметры, о которых я говорю, – это строки запроса или параметры запроса, как бы вы их ни называли. По сути, это способ параметризации URL-адреса.

Допустим, мы хотим, чтобы в наш метод контроллера входили 4 параметра, которые мы хотим проверить, со следующими правилами:

  1. имя – должны быть предоставлены
  2. отметка времени – также должна быть предоставлена и должна быть в правильном формате ( ISO 8601 )
  3. код 1 – должно быть ровно 3 буквенно-цифровых символа
  4. код2 – длина должна составлять от 4 до 5 символов

Да, я знаю. Я очень изобретателен в выборе имен. Потерпи меня.

Дополнительное правило, которое мы хотим иметь, состоит в том, что мы должны предоставить либо код 1 или код 2

Итак, еще одно правило:

  1. код1 или код2, или и то, и другое, должны быть предоставлены

Теперь я просто собираюсь показать вам весь код для этого.

package com.example.demo;

import lombok.Data;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import javax.validation.constraints.*;
import java.time.Instant;

@RestController
public class DemoController {

    @GetMapping("query")
    public void queryParams(@Valid RequestParams params) {
        // your controller code here
    }

    @Data
    public static class RequestParams {

        @NotBlank
        private String name;

        @NotNull
        private Instant timestamp;

        @Pattern(regexp = "^[0-9A-Za-z]{3}$")
        private String code1;

        @Size(min = 4, max = 5)
        private String code2;

        @AssertTrue(message = "You must provide at least code1 or code2")
        private boolean isCode1orCode2Provided() {
            return code1 != null || code2 != null;
        }
    }
}

Позвольте мне объяснить, пошагово:

  1. Создайте новый класс со всеми вашими параметрами. Обратите внимание, что его не нужно называть RequestParams , я просто назвал его так для этого примера. Spring свяжет все параметры запроса с вашим классом – по умолчанию.
  2. Используйте API проверки Java Bean (спецификация JSR-380, это из пакета javax.validation ), чтобы наложить любые ограничения, которые вы хотите, на отдельные параметры запроса.
  3. Используйте @assertTrue для обработки любых требований к проверке, которые обрабатывают несколько параметров запроса. Метод, аннотированный этой аннотацией, должен возвращать значение true, чтобы весь запрос был действительным.
  4. Примите новый класс в своем методе контроллера и добавьте аннотацию @Valid . Аннотация позволяет выполнить проверку для этого объекта, которая выполняется Spring. Spring свяжет все параметры запроса с вашим классом – по умолчанию.
  5. Прибыль!

Я не собираюсь подробно описывать, что делает каждая из аннотаций, вы можете узнать об этом в официальной спецификации ( https://beanvalidation.org/2.0/spec/#builtinconstraints ) или соответствующий JavaDoc ( https://javaee.github.io/javaee-spec/javadocs/javax/validation/constraints/package-summary.html ). Я ненавижу JavaDoc, поэтому я просто делаю это через IDE.

Больше нет реализации пользовательских валидаторов, уродливых подписей методов контроллера, пользовательских аннотаций проверки и всего того, что в итоге просто делает ваш проект более раздутым. ЯГНИК!

Серьезно, в пакете javax.validation так много разных аннотаций на выбор, что есть хорошие изменения, возможно, это все, что вам нужно. Обычно вы можете проверить почти каждое поле только с помощью этих аннотаций, а для всего остального вы можете использовать @assertTrue . Там также есть аннотация @assertFalse , если вам больше подходит написать проверку другим способом. Вы также можете добавить пользовательское сообщение для любой аннотации, если аннотаций по умолчанию недостаточно (хотя обычно так и есть). Проверьте это сами, поэкспериментируйте!

Давайте проверим это сейчас. Мы попытаемся сделать запрос, который нарушает одно из наших правил проверки, скажем, мы не отправляем код 1 и код2 и ожидайте, что мы получим сообщение об ошибке.

Если мы запустим приложение и отправим запрос на получение по этому URL-адресу: http://localhost:8080/query?name=johnny&timestamp=2018-08-29T09:25:01Z

мы получим код состояния HTTP 400 и следующий ответ:

{
    "timestamp": "2020-05-05T16:21:09.450+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "AssertTrue.requestParams.code1orCode2Provided",
                "AssertTrue.code1orCode2Provided",
                "AssertTrue"
            ],
            "arguments": [
                {
                    "codes": [
                        "requestParams.code1orCode2Provided",
                        "code1orCode2Provided"
                    ],
                    "arguments": null,
                    "defaultMessage": "code1orCode2Provided",
                    "code": "code1orCode2Provided"
                }
            ],
            "defaultMessage": "You must provide at least code1 or code2",
            "objectName": "requestParams",
            "field": "code1orCode2Provided",
            "rejectedValue": false,
            "bindingFailure": false,
            "code": "AssertTrue"
        }
    ],
    "message": "Validation failed for object='requestParams'. Error count: 1",
    "path": "/query"
}

Вы можете сказать, что ответ слишком велик для простого сообщения об ошибке проверки, но почему это должно быть проблемой? Чаще всего сообщения об ошибках адресованы разработчикам, а не пользователям. Я бы предпочел иметь большее сообщение об ошибке, чем меньшее. И из этого вы можете МНОГОЕ узнать.

Другой способ выполнить проверку параметров запроса – через онлайн-аннотацию @RequestParam . Как в примере ниже:

package com.example.demo;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.time.Instant;

@RestController
public class DemoController {

    @GetMapping("query")
    public void queryParams(
            @RequestParam @NotBlank String name,
            @RequestParam @NotNull Instant timestamp,
            @RequestParam @Pattern(regexp = "^[0-9A-Za-z]{3}$") String code1,
            @RequestParam @Size(min = 4, max = 5) String code2
            ) {

        if (code1 == null && code2 == null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "You must provide at least code1 or code2");
        }

        // your controller code here
    }
}

Сигнатура нашего метода теперь выглядит довольно… большой. Там, конечно, многое происходит.

Я намеренно вложил некоторую логику проверки в код контроллера. Разве это неправильно? Я не знаю, может быть, и нет, но это определенно утечка логики проверки в то место (я говорю о теле метода), где должны происходить другие вещи. И теперь у вас также есть два вида сообщений, которые следует ожидать от вашего REST API при ошибке проверки. Если вы подумаете об этом, интерпретатор никогда не должен доходить до тела метода, если параметры запроса неверны.

“Правильным” способом было бы реализовать пользовательскую аннотацию проверки и соответствующий ей обработчик (класс, который каким-то образом реализует интерфейс Validator ).

Если вы это сделаете, вы уже добавили два дополнительных файла. Чтобы подтвердить одну простую вещь. Синтаксис для этого тоже довольно, ну, печальный. Или, может быть, вы просто хотите, чтобы ваша кодовая база выглядела более Предприятие . Нет, спасибо, я буду избегать этого с очень большим радиусом.

Обратите внимание, что я поставил правильно в кавычки. Я действительно не думаю, что существует абсолютно правильный путь. Есть лучшие способы и худшие способы, более ремонтопригодные способы и менее ремонтопригодные способы, более красивые способы и более уродливые способы. Все зависит от того, чего вы хотите.

Пример из этого поста посвящен проверке параметров запроса. В принципе, вы можете использовать тот же API и для проверки тела запроса . Единственное, что вам нужно добавить, это аннотацию @RequestBody вместе с @Valid , чтобы Spring знала, что она должна привязать тело запроса к этому объекту, а не параметры запроса.

Все остальное остается прежним.

Таким образом, вы можете легко проверять входящие DTO, и вам не нужно беспокоиться о сложной логике проверки. И так легко добавлять новые DTO.

Если вам нравится делать что-то лаконично и с меньшим количеством кода, этот способ проверки оставит вас удовлетворенными. Spring Framework и API JSR-380 творят чудеса. Таким образом, у вас будет стандартизированная логика проверки во всем приложении, а также стандартизированный набор ответов на ошибки проверки. И вам почти ничего из этого не придется делать самому.

Конечно, у этого подхода есть некоторые недостатки. Например, модульное тестирование логики проверки является одной из проблем. Должны ли вы протестировать функции Spring Framework? Наверное, нет! Но вы можете захотеть протестировать пользовательскую логику, которая у вас есть в методах, аннотированных @assertTrue и @assertFalse . Есть способы обойти это, потому что вы можете экстернализировать эти методы с помощью ссылок на методы, но вся эта история с “меньшим количеством кода” затем падает в воду.

Этот аргумент относится только к модульному тестированию. Однако вы можете протестировать свой контроллер на интеграцию (чтобы запустить весь Spring behemoth) и протестировать его только для проверки. Может ли это считаться модульным тестированием? С таким же успехом можно было бы, если вы спросите меня.

Других недостатков я не знаю. Ты мне скажи.

Оригинал: “https://dev.to/baso53/dead-simple-rest-api-validation-with-spring-boot-seriously-very-simple-3l6”