Валидация generic параметров в Spring контроллерах

image
Все мы часто пишем простые методы в контроллерах работающие через числовые идентификаторы.

    @RequestMapping(value = {"/entityName/{entityId}/get"}, method = RequestMethod.GET)
    @ResponseBody
    public Entity get(@PathVariable(value = "entityId") Integer entityId) {
        //возврат значения сущности по ID
    }


Пришедший ID надо валидировать.Например, не у всех пользователей есть доступ ко всем ID.

Понято что использовать UUID безопаснее и надежнее. Но его надо создавать, хранить, индексировать итд. Для всех сущностей это делать долго и сложно. Во многих случаях проще работать по обычным числовым ID.

Валидацию можно сделать по простому:

    @RequestMapping(value = {"/entityName/{entityId}/get"}, method = RequestMethod.GET)
    @ResponseBody
    public Entity get(@PathVariable(value = "entityId") Integer entityId) {
        if(!@dao.validate(entityId))
             return some_error;

        //возврат значения сущности по ID
    }


Плюс в таком решении только один. Просто и быстро.
Все остальное плохо. Дублирование кода, валидация не совместима с валидацией объектов, нужна отдельная обработка в каждом методе.

Хочется сделать так:

    @RequestMapping(value = {"/entityName/{entityId}/get"}, method = RequestMethod.GET)
    @ResponseBody
    public Entity get(@Validated @PathVariable(value = "entityId") Integer entityId) {
        //возврат значения сущности по ID
    }


Этот простой и логичный вариант не работает. Валидатор просто не вызывается. Валидация PathVariable Спрингом не поддерживается.

Для работоспособности этого варианта надо превратить PathVariable в ModelAttribute:


    @ModelAttribute
    private Integer integerAsModelAttribute(@PathVariable("entityId") Integer id) {
        return id;
}


И получить ошибку при обращении к контроллеру. У врапперов генерик типов нет дефолтного конструктора без параметров и нет сеттера. Обходится это с помощью использования Optional. У него есть и дефолтный конструктор и сеттер принимающий обычные инты.

Превращаем Integer в Optional:


    @ModelAttribute
    private Integer integerAsModelAttribute(@PathVariable("entityId") Optional id) {
        return id.orElse(null);
    }


И соответственно сам метод контроллера и объявление валидатора:


    @InitBinder({"entityId"})
    protected void initCommissionIdBinder(WebDataBinder binder) {
        binder.setValidator(validateEntityIdValidator);
        binder.setBindEmptyMultipartFiles(false);
    }

    @RequestMapping(value = {"/entityName/{entityId}/get"}, method = RequestMethod.GET)
    @ResponseBody
    public Entity get(@Validated @ModelAttribute(value = "entityId") Integer entityId) {
       //Можно смело работать с ID. Оно уже валидировано.
    }


Класс валидатора абсолютно обычный:


public class ValidateIntegerValidator implements Validator {
    @Override
    public boolean supports(Class aClass) {
        return Integer.class.equals(aClass);
    }

    @Override
    public void validate(Object o, Errors errors) {

        if(o instanceof Integer) {
            Integer integer = (Integer) o;

            if(!dao.checkId(integer)) {
                errors.reject("-1", "ERROR");
            }
        } else {
            errors.reject("-2","WRONG TYPE");
        }

    }
}


Полный рабочий пример можно взять тут.

© Habrahabr.ru