piątek, 3 lutego 2012

Hibernate Validator (JSR 303) + mechanizm refleksji = walidacja absolutna

Przygarnięcie przez JavaEE projektu Hibernate Validator pod numerem JSR 303 było wg. mnie kolejnym dobrym krokiem w standaryzacji dobry rozwiązań z projektów opensourcowych.

Jednak po jakimś czasie używania (jakże przyjemnego) standardowych walidatorów, doszedłem do wniosku, że to za mało. Napisanie kilku własnych, które swoją drogą również tworzy się bardzo prosto, tylko na chwilę zaspokoiło moje potrzeby. Dopiero połączenie JSR 303 i mechanizmu refleksji w javie, daje maksymalne możliwości wykorzystania tego standardu.

Załóżmy, że mamy klasę która posiada dwie daty: od i do.
public class Entity {

    private Date from;
    private Date to;
}

Chcielibyśmy sprawdzić czy podane daty są po kolei, tj 'from' <= 'to'. Możemy napisać własny walidator dla klasy Entity, ale w projekcie takich klas z datami możemy mieć mnóstwo i dla każdej z nich należałoby stworzyć oddzielny walidator. Dzięki refleksją w javie możemy zrobić jeden uniwersalny, który jako parametry przyjmowałby 2 wartości, nazwa pola 'from', nazwa pola 'to'.
@Target({ TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = CheckDatesValidator.class)
public @interface CheckDates {

    String message() default "{pl.costam.CheckDates}";

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

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

    String dateFrom();

    String dateTo();

}

I sam walidator:
public class CheckDatesValidator implements ConstraintValidator<CheckDates, Object> {

    private String dateFromFieldName;
    private String dateToFieldName;
    private String message;

    @Override
    public void initialize(CheckDates checkDates) {
        
        dateFromFieldName = checkDates.dateFrom();
        dateToFieldName = checkDates.dateTo();
        message = checkDates.message();
    }

    @Override
    public boolean isValid(Object object, ConstraintValidatorContext context) {

        boolean result = validateDates(object);

        if (!result) {
            
            //jeśli walidacja nie powiodła sie to tworzymy własny constraint - wskazujemy dla którego pola wystąpił błąd
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(message)
                    .addNode(dateToFieldName)
                    .addConstraintViolation();
        }

        return result;

    }

    private boolean validateDates(Object object) {

        try {
            
            Date dateFrom = field(dateFromFieldName).ofType(Date.class).in(object).get();
            Date dateTo = field(dateToFieldName).ofType(Date.class).in(object).get();

            // sprawdzenie dat
            return checkDatesInterval(dateFrom, dateTo);
        }
        catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}

Poprzez refleksję można odwołać sie do pola używając właściwie "czystej" javy, ale wygląda to średnio w kodzie i lepiej użyć jakiegoś gotowego rozwiązania. Ja osobiście polecam biliotekę: FEST

Zastowanie adnotacji jest następujące:
@CheckDates(dateFrom="from", dateTo="to")
public class Entity {

    private Date from;
    private Date to;
}

5 komentarzy:

  1. albo mozesz enkapsulowac from i to do osobnego obiektu DateRange i go osobno zwalidowac walidatorem dla tej klasy.

    Choc pewnie znajda sie tez inne przyklady z ktorymi tak prosto nie pojdzie.

    Dla jasnosci dodam rowniez ze z 303 nie korzystalem jeszcze.

    Pozdrawiam
    Michal

    OdpowiedzUsuń
  2. Racja, o ile coś projektujemy od zera:) Czasami refaktor jest nieopłacalny.

    OdpowiedzUsuń
  3. Enkapsulacja do osobnego obiektu jest IMHO preferowanym rozwiązaniem. Bez tego tworzysz obiekt który jest w stanie poprawnym, niepoprawnym albo diabli wiedząc jakim (albo jak kot Schrödingera poprawny i niepoprawny jednocześnie). Na ogół wychodzi się lepiej nie pozwalając na wprowadzenie obiektu w stan niepoprawny.

    A najlepiej zamiast badziewnych dat z java.util użyć Joda-Time a tam mamy dostępny out-of-the-box typ Interval i problem z głowy. Wsadzanie JSR 303 i tony refleksji do modelu domeny to dodawanie niepotrzebnej złożoności.

    Z drugiej strony - z powodzeniem używałem JSR-303 do prostych walidacji DTOsów przylatujących przylatujących z GUI.

    OdpowiedzUsuń
  4. Refleksją pakujesz się na bardzo nieprzyjemną minę zwaną wydajnością. Przy niewielkiej "gęstości" wywołania walidatora wykorzystującego mechanizmy refleksji jest to niezauważalne, ale przy dużej ilości tego typu wywołań to boli. Tym bardziej, że problem pojawia się zazwyczaj w produkcji, bo w testach zazwyczaj nie sprawdzamy wydajności.

    Rozwiązań jest kilka. Po pierwsze wspomniany przez Marcina Joda Time. Po drugie daty można opakować w ich własną klasę zawierającą tylko dwa pola. Po trzecie można napisać specjalizowane walidatory, które będą przyjmowały konkretne typy zamiast ogólnego Object.

    Generalnie JSR303 jest bardzo przyjemny, ale niestety przy bardziej skomplikowanych zadaniach wysiada.

    OdpowiedzUsuń
  5. Zgadzam się, że najlepszym rozwiązaniem jest lepsza enkapsulacja. Post był pisany raczej jako ciekawostka, niż jakiś wzorzec. Ostatnio po prostu kombinuję do czego by tu jeszcze wykorzystać refleksje.

    OdpowiedzUsuń