Korzystając z okazji, że jestem sobie na urlopie ojcowskim postanowiłem dokładnie przyjrzeć się JSR-303 Bean Validation. Generalnie zdecydowana większość artykułów na blogach poświęcona temu tematowi ogranicza się do przedstawienia „podstawowych podstaw podstaw”. Oczywiście i ja od tego zacznę 🙂

Adnotacja i walidator

JSR 303 opiera się o przetwarzania adnotacji. Informacje o zasadach walidacji danej klasy znajdują się w jej definicji, ale zawsze można je nadpisać definiując je w pliku XML. Sama walidacja odbywa się w osobnej klasie – walidatorze.

Adnotacja

Z adnotacjami w Javie jest jeden drobny problem. Nie mogą dziedziczyć. To powoduje, że adnotacja walidatora nie są sprawdzane przez kompilator pod kątem poprawności. Zatem definiując adnotację należy uważać 🙂

Najprostsza adnotacja wygląda mniej więcej tak:

Listing 1. Prosta adnotacja

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Documented
@Target(value = { FIELD, METHOD, TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = BasicValidator.class)
public @interface Basic {

	public String message() default "Invalid Bean";

	public Class<?>[] groups() default {};

	public Class<? extends Payload>[] payload() default {};

}

Po kolei. Beans Validation w wersji 1.0 nie umożliwia walidacji parametrów metody przed jej wywołaniem. Dotyczy to także parametrów konstruktora. Zatem ograniczyłem użycie adnotacji do pól, typów (można adnotować całą klasę), metod (o tym za chwilę) i adnotacji (o tym później). Adnotacja musi być dostępna w trakcie działania programu stąd @Retention(RUNTIME). I najważniejsze – adnotacja @Constraint, która powoduje, że oznaczoną nią adnotacja jest traktowana jako działająca w ramach API Bean Validation.
Tu właśnie leży pies pogrzebany. O ile jeszcze kompilator jest wstanie sprawdzić czy w adniotacji @Constraint podaliśmy choć jedna klasę walidatora to już nie będzie wstanie sprawdzć czy nasza adnotacja została oznaczona jako element walidatora. Należy zatem pamiętać, że chcąc stworzyć własną adnotację dla walidatora musimy oznaczyć ją za pomocą @Constraint. Kompilatora tego za nas nie zrobi.
Jak już jesteśmy przy @Constraint to posiada ona tylko jedno pole validatedBy, które jest tablicą klas walidatorów obsługujących daną adnotację. Generalnie na tej liście musi być co najmniej jedna klasa. O walidatorze będziemy mówić za chwilę, ale już uprzedzając powiem, że jest on generyczny dzięki temu na liście mogą znajdować się walidatory obsługujące daną adnotację w zależności od typu sprawdzanego pola. Przy czym jeżeli na liście znajdą się dwa walidatory obsługujące ten sam typ to zostanie wyrzucony wyjątek UnexpectedTypeException
Co w bebechach? Zaczniemy od message, czyli wiadomości, że oznaczona wartość jest nieprawidłowa. Może tu być prosta wiadomość tak jak na listingu 1. może być też wiadomość w formacie {klucz}, gdzie klucz oznacza klucz w pliku ValidationMessages.properties. Komunikat zostanie pobrany przez klasę wyspecjalizowaną w obsłudze komunikatów, która dodatkowo obsługuje internacjonalizację.
groups wskazuje grupy, do których należy dana adnotacja. Grupy pozwalają na grupowanie (ojej…) części walidatorów tak by przyspieszyć działanie całego api. Wartością domyślna MUSI być pusta tablica. Domyślnie też wszystkie adnotacje należą do grupy Default. O grupach kiedy indziej.
Ostatnim elementem jest tablica payload. Domyślnie musi być ona pusta. Payload jest interfejsem znacznikowym, takim samym jak Serializable, pozwalającym na wprowadzenie do danego miejsca użycia adnotacji pewnych dodatkowych informacji w sposób bezpieczny. Nie ma już np. potrzeby parsowania stringów.

Tyle o podstawowej adnotacji walidatora. Czas na sam walidator.

Walidator

Skoro mamy już zaadnotowane pole/klasę/metodę to czas na drugi element układanki. Klasa walidująca jest to zwykła klasa, która musi implementować interfejs ConstraintValidator<A extends Annotation, T>. Dla naszej adnotacji będzie ona wyglądać w następujący sposób:

Listing 2. Walidator dla adnotacji

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class BasicValidator implements ConstraintValidator<Basic, SimpleBean> {

	@Override
	public void initialize(Basic constraintAnnotation) {

	}

	@Override
	public boolean isValid(SimpleBean value, ConstraintValidatorContext context) {
		return !value.getFirstName().isEmpty()
				&& !value.getLastName().isEmpty();
	}

}

Mamy tu kilka rzeczy. Po pierwsze generyczny interfejs. Pierwszy parametr to obsługiwana adnotacja. Drugi to typ, który będzie sprawdzany. Tu problemem jest sztuczne ograniczenie dotyczące typów. Na początek co wolno. Otóż specyfikacja dopuszcza:

  • Wszystkie typy nie generyczne.
  • Typy prymitywne należy przedstawić jako typu opakowujące (int -> Integer itd.)
  • Jeżeli typ jest generyczny można przedstawić go jako niegeneryczny albo jak ogólny (znak ?).

Nie wolno jednak używać:

  • Skonkretyzowanych typów generycznych
  • Typów generycznych z ograniczonym uogólnieniem

Co ważne dla kompilatora te ograniczenia nie istnieją zatem może się okazać, że zapis:

Listing 3. Dozwolone wersje

public class MyValidator implements ConstraintValidator<Basic, Object> {
//..
}
//
public class MyValidator implements ConstraintValidator<Basic, Collection> {
//..
}
//
public class MyValidator implements ConstraintValidator<Basic, Collection<?>> {
//..
}

jest poprawny, ale już

Listing 4. Nie dozwolone wersje

public class MyValidator implements ConstraintValidator<Basic, Collection<Object>> {
//..
}
//
public class MyValidator implements ConstraintValidator<Basic, Collection<? extends MyBean>> {
//..
}

przechodzi kompilację i testy (sic!), ale w praktyce wysypuje się z UnexpectedTypeException. Notka w specyfikacji wspomina co prawda, że to ograniczenie jest sztuczne i może zostać zmienione w kolejnych wersjach (wersja 1.1 jest w drodze), ale…

Drugą rzeczą jest metoda initialize. Najważniejsza związana z nią informacja jest taka, że będzie ona wywołana tylko raz dla danej instancji walidatora (instancji interfejsu Validator). Za jej pomocą można pobrać informacje z adnotacji i skonfigurować walidator (tym ramem nasz). Należy jednak pamiętać iż pobranie informacji nastąpi tylko jeden raz i jeżeli stosujemy różne wersje parameterów w adnotacji to należy za każdym razem pobierać nowy obiekt walidatora (Validator) by ten każdorazowo konfigurował walidator (nasz). Już sie zgubiliście? Ta? to dobrze 😀

Ostatnim elementem jest metoda isValid. Musi być ona bezpieczna wądkowo. Przyjmuje dwa parametry. Pierwszy to obiekt do sprawdzenia. Drugi to kontekst walidatora. O kontekście będzie przy okazji komunikatów. Metoda zwraca boolean, przy czym dobrą praktyką jest zwracanie true jeżeli przekazana wartość jest null (tą wartość obsługujemy, jak by co, za pomocą adnotacji @NotNull i kompozycji adnotacji). Metoda nie może też modyfikować przekazanej wartości (chyba, że lubicie długie wieczory z debuggerem).

Ja – klient walidatora

Ostatnią dziś poruszoną sprawą będzie to jak dobrać się do walidatora. Nie jest to trudne. I na całe szczęście 🙂

Listing 5. Pobranie instancji walidatora i walidacja

import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;

public class App {

	public static void main(String[] args) {
		Validator validator = Validation.buildDefaultValidatorFactory()
				.getValidator();

		SimpleBean bean = new SimpleBean();
		bean.setFirstName("First");
		bean.setLastName("Last");

		Set<constraintviolation>> validationResults = validator
				.validate(bean);
		System.out.println(validationResults);
	}

}</constraintviolation>

Co my tu mamy 🙂 Validation jest to jedyna, z pominięciem wyjątków, publiczna klasa w API JSR 303. Stanowi ona punkt wejścia do API. Metoda buildDefaultValidatorFactory pobiera pierwszą napotkaną implementację ValidationFactory, która poprzez metodę getValidator udostępnia nam walidator. Ta ostatnia metoda powinna gwarantować zwrócenie nowej instancji za każdym razem kiedy jest wywołana oraz brak cacheowania utworzonych instancji. Jest to bardzo ważne z punktu widzenia inicjacji poszczególnych walidatorów – kwestia wywołania metody initialize.

Podsumowanie

Mam nadzieję, że udało mi się przybliżyć „podstawy podstaw” JSR 303. W kolejnych wpisach omówię tworzenie adnotacji złożonych, sposób obsługi komunikatów oraz grup i do czego nam Payload.