Валидация generic параметров в Spring контроллерах
Все мы часто пишем простые методы в контроллерах работающие через числовые идентификаторы.
@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");
}
}
}
Полный рабочий пример можно взять тут.