Статьи

Весна из окопов: добавление валидации в REST API

Мне немного стыдно это признавать, но до вчерашнего дня я не представлял, что смогу добавить проверку в REST API, используя аннотации @Valid и @RequestBody . Это не работало в Spring MVC 3.0, и по какой-то причине я не заметил, что поддержка этого была добавлена ​​в Spring MVC 3.1 . Мне никогда не нравился старый подход, потому что я должен был

  1. Вставьте бины Validator и MessageSource в мой контроллер, чтобы я мог проверить запрос и извлечь локализованные сообщения об ошибках, если проверка не удалась.
  2. Вызовите метод проверки в каждом методе контроллера, вход которого должен быть проверен.
  3. Переместите логику проверки в общий базовый класс, который расширяется классами контроллера.

Когда я заметил, что мне больше не нужно этим заниматься, я решил написать этот пост в блоге и поделиться своими результатами со всеми вами.

Примечание. Если мы хотим использовать проверку с помощью JSR-303 в Spring Framework, мы должны добавить провайдера JSR-303 в наш путь к классам. Примеры приложений этого блога используют Hibernate Validator 4.2.0, который является эталонной реализацией Bean Validation API (JSR-303).

Давайте начнем с рассмотрения класса DTO, используемого в этом посте. Исходный код класса CommentDTO выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
 
public class CommentDTO {
 
    @NotEmpty
    @Length(max = 140)
    private String text;
 
    //Methods are omitted.
}

Давайте продолжим и выясним, как мы можем добавить проверку в REST API с помощью Spring MVC 3.1.

Spring MVC 3.1 – хорошее начало

Мы можем добавить проверку к нашему REST API, выполнив следующие действия:

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

Оба шага описаны в следующих подразделах.

Реализация контроллера

Мы можем реализовать наш контроллер, выполнив следующие действия:

  1. Создайте класс с именем CommentController и аннотируйте этот класс аннотацией @Controller .
  2. Добавьте метод add () в класс CommentController, который принимает добавленный комментарий в качестве параметра метода.
  3. Аннотируйте метод с помощью аннотаций @RequestMapping и @ResponseBody.
  4. Примените аннотации @Valid и @RequestBody к параметру метода.
  5. Вернуть добавленный комментарий.

Исходный код класса CommentController выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
 
import javax.validation.Valid;
 
@Controller
public class CommentController {
 
    @RequestMapping(value = "/api/comment", method = RequestMethod.POST)
    @ResponseBody
    public CommentDTO add(@Valid @RequestBody CommentDTO comment) {
        return comment;
    }
}

Теперь мы добавили новый метод в наш контроллер и добавили в него проверку. Когда проверка не проходит, выдается исключение MethodArgumentNotValidException . Давайте выясним, как мы можем вернуть значимый ответ пользователю нашего API, если проверка не пройдена.

Обработка ошибок валидации

Мы можем реализовать логику, которая обрабатывает ошибки проверки, выполнив следующие шаги:

  1. Реализуйте объекты передачи данных, которые содержат информацию, возвращаемую пользователю нашего REST API.
  2. Реализуйте метод обработчика исключений.

Эти шаги описаны более подробно ниже.

Создание объектов передачи данных

Во-первых, мы должны создать объекты передачи данных, которые содержат информацию, возвращаемую пользователю нашего REST API. Мы можем сделать это, выполнив следующие действия:

  1. Создайте DTO, который содержит информацию об одной ошибке проверки.
  2. Создайте DTO, который объединяет эти ошибки проверки.

Давайте начнем.

Исходный код первого DTO выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
public class FieldErrorDTO {
 
    private String field;
 
    private String message;
 
    public FieldErrorDTO(String field, String message) {
        this.field = field;
        this.message = message;
    }
 
    //Getters are omitted.
}

Реализация второго DTO довольно проста. Он содержит список объектов FieldErrorDTO и метод, который используется для добавления новых ошибок поля в список. Исходный код ValidationErrorDTO выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
import java.util.ArrayList;
import java.util.List;
 
public class ValidationErrorDTO {
 
    private List<FieldErrorDTO> fieldErrors = new ArrayList<>();
 
    public ValidationErrorDTO() {
 
    }
 
    public void addFieldError(String path, String message) {
        FieldErrorDTO error = new FieldErrorDTO(path, message);
        fieldErrors.add(error);
    }
 
    //Getter is omitted.
}

В следующем листинге приведен пример документа Json, который отправляется обратно пользователю нашего API при сбое проверки:

1
2
3
4
5
6
7
8
{
    "fieldErrors":[
        {
            "field":"text",
            "message":"error message"
        }
    ]
}

Давайте посмотрим, как мы можем реализовать метод обработчика исключений, который создает новый объект ValidationErrorDTO и возвращает созданный объект.

Реализация метода обработчика исключений

Мы можем добавить метод обработчика исключений в наш контроллер, выполнив следующие действия:

  1. Добавьте поле MessageSource в класс CommentController . Источник сообщения используется для извлечения локализованного сообщения об ошибках для ошибок проверки.
  2. Внедрите компонент MessageSource с помощью инжектора конструктора.
  3. Добавьте метод processValidationError () в класс CommentController . Этот метод возвращает объект ValidationErrorDTO и принимает объект MethodArgumentNotValidException в качестве параметра метода.
  4. Аннотируйте метод с помощью аннотации @ExceptionHandler и убедитесь, что метод вызывается при возникновении исключения MethodArgumentNotValidException .
  5. Аннотируйте метод с помощью аннотации @ResponseStatus и убедитесь, что возвращается код состояния HTTP 400 ( неверный запрос).
  6. Аннотируйте метод с помощью аннотации @ResponseBody .
  7. Реализуйте метод.

Давайте подробнее рассмотрим реализацию метода processValidationError () . Мы можем реализовать этот метод, выполнив следующие действия:

  1. Получить список объектов FieldError и обработать их.
  2. Обработайте ошибки поля по одной ошибке поля за раз.
  3. Попробуйте разрешить локализованное сообщение об ошибке, используя объект MessageSource , текущую локаль и код ошибки обработанного поля error.
  4. Вернуть исправленное сообщение об ошибке. Если сообщение об ошибке не найдено в файле свойств, верните наиболее точный код ошибки поля.
  5. Добавьте новую ошибку поля, вызвав метод addFieldError () класса ValidationErrorDTO . Передайте имя поля и сообщение об устраненной ошибке как параметры метода.
  6. Вернуть созданный объект ValidationErrorDTO после обработки каждой ошибки поля.

Исходный код класса CommentController выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
 
import javax.validation.Valid;
import java.util.List;
import java.util.Locale;
 
@Controller
public class CommentController {
 
    private MessageSource messageSource;
 
    @Autowired
    public CommentController(MessageSource messageSource) {
        this.messageSource = messageSource;
    }
 
    //The add() method is omitted.
 
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public ValidationErrorDTO processValidationError(MethodArgumentNotValidException ex) {
        BindingResult result = ex.getBindingResult();
        List<FieldError> fieldErrors = result.getFieldErrors();
 
        return processFieldErrors(fieldErrors);
    }
 
    private ValidationErrorDTO processFieldErrors(List<FieldError> fieldErrors) {
        ValidationErrorDTO dto = new ValidationErrorDTO();
 
        for (FieldError fieldError: fieldErrors) {
            String localizedErrorMessage = resolveLocalizedErrorMessage(fieldError);
            dto.addFieldError(fieldError.getField(), localizedErrorMessage);
        }
 
        return dto;
    }
 
    private String resolveLocalizedErrorMessage(FieldError fieldError) {
        Locale currentLocale =  LocaleContextHolder.getLocale();
        String localizedErrorMessage = messageSource.getMessage(fieldError, currentLocale);
 
        //If the message was not found, return the most accurate field error code instead.
        //You can remove this check if you prefer to get the default error message.
        if (localizedErrorMessage.equals(fieldError.getDefaultMessage())) {
            String[] fieldErrorCodes = fieldError.getCodes();
            localizedErrorMessage = fieldErrorCodes[0];
        }
 
        return localizedErrorMessage;
    }
}

Вот и все. Давайте потратим немного времени, чтобы оценить, что мы только что сделали.

Мы почти на месте

Теперь мы добавили проверку в наш REST API с помощью Spring MVC 3.1. Эта реализация имеет одно важное преимущество перед старым подходом:

Мы можем запустить процесс проверки с помощью аннотации @Valid .

Однако методы, аннотированные аннотацией @ExceptionHandler, будут срабатывать только тогда, когда настроенное исключение выдается из класса контроллера, который содержит метод обработчика исключения. Это означает, что если наше приложение имеет более одного контроллера, мы должны создать общий базовый класс для наших контроллеров и переместить логику, которая обрабатывает ошибки валидации в этот класс. Это может показаться не таким уж большим делом, но мы должны предпочесть композицию наследованию .

Spring MVC 3.2 предоставляет инструменты, которые мы можем использовать для устранения необходимости наследования от наших контроллеров. Давайте двигаться дальше и узнаем, как это делается.

Spring MVC 3.2 на помощь

Spring MVC 3.2 представил новую аннотацию @ControllerAdvice, которую мы можем использовать для реализации компонента обработчика исключений, который обрабатывает исключения, создаваемые нашими контроллерами. Мы можем реализовать этот компонент, выполнив следующие действия:

  1. Удалите логику, которая обрабатывает ошибки проверки из класса CommentController .
  2. Создайте новый класс обработчика исключений и переместите логику, которая обрабатывает ошибки проверки, в созданный класс.

Эти шаги объясняются более подробно в следующих подразделах.

Удаление логики обработки исключений из нашего контроллера

Мы можем удалить логику обработки исключений из нашего контроллера, выполнив следующие действия:

  1. Удалите поле MessageSource из класса CommentController .
  2. Удалите конструктор из нашего класса контроллера.
  3. Удалите метод processValidationError () и приватные методы из нашего класса контроллера.

Исходный код класса CommentController выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
 
import javax.validation.Valid;
 
@Controller
public class CommentController {
 
    @RequestMapping(value = "/api/comment", method = RequestMethod.POST)
    @ResponseBody
    public CommentDTO add(@Valid @RequestBody CommentDTO comment) {
        return comment;
    }
}

Наш следующий шаг – создать компонент обработчика исключений. Посмотрим, как это делается.

Создание компонента обработчика исключений

Мы можем создать компонент обработчика исключений, выполнив следующие действия:

  1. Создайте класс с именем RestErrorHandler и аннотируйте его аннотацией @ControllerAdvice .
  2. Добавьте поле MessageSource в класс RestErrorHandler .
  3. Внедрите компонент MessageSource с помощью инжектора конструктора.
  4. Добавьте метод processValidationError () и необходимые частные методы в класс RestErrorHandler .

Исходный код класса RestErrorHandler выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
 
import java.util.List;
import java.util.Locale;
 
@ControllerAdvice
public class RestErrorHandler {
 
    private MessageSource messageSource;
 
    @Autowired
    public RestErrorHandler(MessageSource messageSource) {
        this.messageSource = messageSource;
    }
 
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public ValidationErrorDTO processValidationError(MethodArgumentNotValidException ex) {
        BindingResult result = ex.getBindingResult();
        List<FieldError> fieldErrors = result.getFieldErrors();
 
        return processFieldErrors(fieldErrors);
    }
 
    private ValidationErrorDTO processFieldErrors(List<FieldError> fieldErrors) {
        ValidationErrorDTO dto = new ValidationErrorDTO();
 
        for (FieldError fieldError: fieldErrors) {
            String localizedErrorMessage = resolveLocalizedErrorMessage(fieldError);
            dto.addFieldError(fieldError.getField(), localizedErrorMessage);
        }
 
        return dto;
    }
 
    private String resolveLocalizedErrorMessage(FieldError fieldError) {
        Locale currentLocale =  LocaleContextHolder.getLocale();
        String localizedErrorMessage = messageSource.getMessage(fieldError, currentLocale);
 
        //If the message was not found, return the most accurate field error code instead.
        //You can remove this check if you prefer to get the default error message.
        if (localizedErrorMessage.equals(fieldError.getDefaultMessage())) {
            String[] fieldErrorCodes = fieldError.getCodes();
            localizedErrorMessage = fieldErrorCodes[0];
        }
 
        return localizedErrorMessage;
    }
}

Мы наконец там

Благодаря Spring MVC 3.2 мы теперь реализовали элегантное решение, в котором проверка запускается аннотацией @Valid , а логика обработки исключений перемещается в отдельный класс. Я думаю, что мы можем назвать это днем ​​и наслаждаться результатами нашей работы.

Резюме

Этот пост научил нас тому, что

  • Если мы хотим добавить проверку в REST API, когда мы используем Spring 3.0, мы должны сами реализовать логику проверки.
  • Spring 3.1 позволил добавить проверку к REST API с помощью аннотации @Valid . Однако мы должны создать общий базовый класс, который содержит логику обработки исключений. Каждый контроллер, который требует проверки, должен расширять этот базовый класс.
  • Когда мы используем Spring 3.2, мы можем запустить процесс проверки с помощью аннотации @Valid и извлечь логику обработки исключений в отдельный класс.

Пример приложения этого блога доступен на Github ( Spring 3.1 и Spring 3.2 )