Validation API: собственный валидатор на примере @NotNullObjectId

Автор PremaServices - Среда, 19 Июнь 2013. В рубриках: Validation API, Hibernate

Проверка данных в Spring + Hibernate

Проверка пользовательских данных в Spring является одним из ключевых моментов в разработке приложений, и просто необходима, если мы хотим создать стабильный и дружественный пользователю проект. Обычно она производится на двух этапах: как на стороне клиента при помощи скриптов, например, JQuery, так и на стороне сервера.

Для этой цели в этой статье мы используем Hibernate Validation API, который расширяет стандартные возможности Java Validation API, и в свою очередь расширим его возможности, создав свой собственный класс валидации.

Определим проблему и поставим задачу. У меня есть web-приложение по учету студентов в одной из заочных школ астрологии. При регистрации нового студента менеджеру необходимо указать в каком городе (и, соответственно, стране) он проживает. Естественно, по мере необходимости менеджер добавляет в таблицы стран и городов новые значения, расширяя географию учащихся.

Вот на этом простейшем примере стран и городов мы и рассмотрим решение нашей задачи.


Где проблема?

Итак, у нас есть две таблицы (страны и города) и соответствующие им объекты Hibernate. Getters, setters, а также import-директивы я опускаю, чтобы выделить самый важный код.

package ru.premaservices.astroved.student.pojo;
...
@Entity
@Table(name="COUNTRIES") @Unique(name = "country")
public class Country {
	
	@Id @GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name="ID") 
	private Integer id;
	
	@NotBlank
	@Column(name="COUNTRY", length=45, nullable = false, unique = true)
	private String country;
	
	@OneToMany(mappedBy = "country", fetch = FetchType.LAZY)
	@Sort(type = SortType.NATURAL)
	@Cascade(value = CascadeType.SAVE_UPDATE)
	private SortedSet<City> cities = new TreeSet<City>();
    ...
}

Итак, объект Country имеет идентификатор и наименование, а также список городов, студенты из которых проживают в этой стране.

package ru.premaservices.astroved.student.pojo;
...
@Entity
@Table(name="CITIES") @Unique(name = "city")
public class City implements Comparable<City> {
	@Id @GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name="ID")
	private Integer id;
	
	@NotBlank 
	@Column(name="CITY", length=45, nullable = false, unique = true)
	private String city;
	
	@ManyToOne(fetch=FetchType.EAGER)
	@JoinColumn(name="COUNTRY", nullable = false)
	private Country country;
	...
	
	@Override
	public int compareTo(City that) {
		if (that == null) {
	       return -1;
	    }
		return this.getCity().compareTo(that.getCity());
	}
	
}

Сущность City включает идентификатор, наименование города и страну принадлежности. И как мы видим, в обоих случаях для проверки наименований города и страны на непустое значение использована стандартная аннотация @NotBlank.

Идентификатор новой записи в нашем случае СУБД назначит автоматически. Однако, нам необходимо также проверить, указал ли пользователь при вводе города страну, ведь это обязательный параметр. И если мы укажем вот так:

	@NotNull
	@ManyToOne(fetch=FetchType.EAGER)
	@JoinColumn(name="COUNTRY", nullable = false)
	private Country country;

то есть попытаемся проверять объект Country на существование, нас постигнет разочарование. На web-странице пользователю предлагается выбрать страну из предложенного списка, т.е. через select, где вполне может присутствовать строка вида -- не выбрано --, значение которой, передаваемое на сервер, будет пустым. И в этом случае Spring сформирует объект Country, но с пустыми полями равными null. При попытке сохранить новый город в базе данных возникнет исключение. Чтобы не доводить дело то таких крайностей, мы и создадим дополнительную проверку @NotNullObjectId, которая обязует приложение проверять идентификатор вложенного объекта на существование и значение большее 0.


Шаг первый. Создание аннотации

В нашей аннотации мы зададим дополнительный метод value(). Он будет возвращать нам имя переменной класса, которую необходимо проверить на null. Не будем зацикливаться на том, что это именно id, ведь идентификатор может быть назван как угодно, uid, например.

package ru.premaservices.astroved.student.validation;
...
@Documented
@Constraint(validatedBy = {NotNullObjectIdValidator.class})
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNullObjectId {
	String message() default "{ru.premaservices.astroved.student.validation.NotNullObjectId.message}";
	Class<?>[] groups() default {};
	Class<? extends Payload>[] payload() default {};
	String value();
}

Из кода также видно, что сообщение об ошибке по умолчанию содержит не текст, а наименование свойства, по которому API определит текст в специальном файле properties. Но об этом позже. А теперь давайте рассмотрим сам класс валидации, на который ссылается @Constraint.


Шаг второй. Класс валидации

Для извлечения значения переменной класса используем Reflection API. Код класса достаточно прозрачен.

package ru.premaservices.astroved.student.validation;
...
public class NotNullObjectIdValidator implements ConstraintValidator<NotNullObjectId, Object> {
	private String value;
	
	@Override
	public void initialize(NotNullObjectId annotation) {
		value = annotation.value();	
	}
	@Override
	public boolean isValid(Object object, ConstraintValidatorContext context) {
		
		try {
			if (object == null) return false;
		
			Class<?> c = object.getClass();  
			
			String name = "get" + String.valueOf(value.charAt(0)).toUpperCase() + value.substring(1);
			
			Method method = c.getDeclaredMethod(name);
			Object id = method.invoke(object);
			
			if (id == null) return false;
			
			Type type = method.getReturnType();
			
			if (type == String.class) {
				if (!CommonUtil.isNotBlank((String)id)) return false;
			}
			else if (type == Integer.class || type == int.class) {
				if ((Integer)id < 1) return false;
				
			}
			else if (type == Long.class || type == long.class) {
				if ((Long)id < 1) return false;
			}
			
		}
		catch (Exception e) {
			e.printStackTrace();
			return true;
		}
			
		return true;
		
	}
}

Что делает метод isValid? Получает значение объекта, указанного в value() аннотации. И далее, в зависимости от типа этого значения проверяет его. Если это объект, он проверяется на null. Если это строка, то на null и length=0, а если целое число, то на равенство нулю. Единственное, может вызвать вопросы статическая функция CommonUtil.isNotBlank. Это custom-функция проекта, она просто проверяет строку на ненулевое и непустое значение.


Шаг третий. Определение сообщения об ошибке

Теперь почти последнее. В файл ValidationMessages.properties мы должны прописать сообщение об ошибке с идентификатором ru.premaservices.astroved.student.validation.NotNullObjectId.message который указан в аннотации. Тогда при проверке (в случае ее неудачного завершения) это сообщение будет отправлено пользователю.

ru.premaservices.astroved.student.validation.NotNullObjectId.message=Поле обязательно для заполнения

О том, где искать файл ValidationMessages.properties смотрите здесь: Validation API: русификация сообщений об ошибках.


Шаг последний. Включаем проверку

Теперь осталось просто добавить нашу аннотацию в класс City. Вот так:

package ru.premaservices.astroved.student.pojo;
...
@Entity
@Table(name="CITIES") @Unique(name = "city")
public class City implements Comparable<City> {
	@Id @GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name="ID")
	private Integer id;
	
	@NotBlank 
	@Column(name="CITY", length=45, nullable = false, unique = true)
	private String city;
	
	@NotNullObjectId(value = "id")
	@ManyToOne(fetch=FetchType.EAGER)
	@JoinColumn(name="COUNTRY", nullable = false)
	private Country country;
	...
	
	@Override
	public int compareTo(City that) {
		if (that == null) {
	       return -1;
	    }
		return this.getCity().compareTo(that.getCity());
	}
	
}

Сама проверка данных происходит в контроллере Spring на этапе вызова метода, инициированного запросом от клиента. В нашем случае это выглядит так:

package ru.premaservices.astroved.student.controller;
...
@Controller
public class DictionaryController {
	public static final String CITY_FORM_REQUEST = "/city/update";
	
	...
	@RequestMapping(value = CITY_FORM_REQUEST, method = RequestMethod.POST)
	public ModelAndView updateCity (@ModelAttribute("city") @Valid City city, BindingResult result) { //AJAX
		
		ModelAndView model = null;
		
		if (result.hasErrors()) {
			model = new ModelAndView(CITY_FORM_JSP);
			model.addAllObjects(result.getModel());
			...
		}
		else {
			...
		}
		
		return model;
	}
	
}

Аннотация @Valid инициирует проверку полученного объекта City на корректность согласно указанным нами ограничениям. Результат проверки помещается в объект BindingResult. И в случае неудачи мы отсылаем обратно пользователю форму и все введенные им данные вместе с сообщением об ошибке.

  • 0.0/5 оценка (0 голосов)
  • Тэги: Validation API, аннотация, проверка данных

Об авторе

Комментарии (0)

Оставить комментарий

Вы комментируете как Гость.