Czerwiec
12
2009

Java – programowanie refleksyjne i właściwości

Słowa kluczowe: | Kategorie: Java
No Gravatar

Pisałem jakiś czas temu na temat przewagi właściwości nad polami. Muszę jednak przyznać, że operowanie właściwościami sprawia sporo problemów podczas programowania refleksyjnego, które jest szczególnie przydatne podczas pracy z adnotacjami (z mechanizmu refleksji korzystałem przy zapisie do pliku XML).

Ponieważ język Java nie przewiduje obecnie właściwości w pakiecie java.lang.reflect, postanowiłem utworzyć własną klasę Property, która operowałaby na właściwościach. Wzorem klasy java.lang.reflect.Field będzie ona implementować interfejsy AnnotatedElementMember:

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.ArrayList;

public class Property
implements AnnotatedElement, Member {
}

Zacznę od kilku pomocniczych stałych – przyrostków dla setterówgetterów:

private static final String getterPrefix = "get";
private static final String setterPrefix = "set";
private static final String booleanGetterPrefix = "is";

oraz komunikatów dla wyjątków:

private static final String wrongGetterName =
    "'%s' is not a well formed property getter name.";
private static final String noSuchProperty =
    "Property '%s' not found for the '%s' class.";
private static final String readOnlyProperty =
    "Property '%s' is read only.";

Dla każdej właściwości będzie trzeba zapamiętywać jej nazwę oraz metody do ustawiania i–pobierania wartości:

private String name;
private Method getter;
private Method setter;

Implementację zacznę od interfejsu AnnotatedElement. Podobnie jak w bibliotece JPA czy Hibernate, założę, że adnotacje do właściwości przypisywane są tylko do getterów (istnieją właściwości tylko do odczytu – nie posiadające settera), zatem właśnie do tej metody oddeleguję wszystkie wywołania dotyczące adnotacji:

public <T extends Annotation> T getAnnotation(
        Class<T> annotationClass) {
    return getter.getAnnotation(annotationClass);
}

public Annotation[] getAnnotations() {
    return getter.getAnnotations();
}

public Annotation[] getDeclaredAnnotations() {
    return getter.getDeclaredAnnotations();
}

public boolean isAnnotationPresent(
        Class<? extends Annotation> annotationClass) {
    return getter.isAnnotationPresent(annotationClass);
}

Analogicznie postąpię w przypadku większości metod interfejsu Member – jedynie getName zostawię na później:

public Class<?> getDeclaringClass() {
    return getter.getDeclaringClass();
}

public int getModifiers() {
    return getter.getModifiers();
}

public boolean isSynthetic() {
    return getter.isSynthetic();
}

Zdefiniuję dwie proste metody pomocnicze:

isReadOnly
określa, czy właściwość jest tylko do odczytu (nie ma settera),
isBoolean
określa, czy właściwość jest typu logicznego (nazwa gettera zaczyna się od is – zamiast od get):
public boolean isReadOnly() {
    return (setter == null);
}

protected boolean isBoolean() {
    return (getType() == boolean.class) ||
        (getType() == Boolean.class);
}

Teraz mogę zaimplementować metodę getName (pobierające nazwę właściwości zaczynającą się od małej litery) oraz dwie dodatkowe metody – getGeterNamegetSetterName – pobierające właściwe nazwy getterasettera:

public String getName() {
    return name.substring(0, 1).toLowerCase().concat(
            name.substring(1));
}

public String getGetterName() {
    if (isBoolean()) {
        return booleanGetterPrefix.concat(name);
    } else {
        return getterPrefix.concat(name);
    }
}

public String getSetterName() {
    return setterPrefix.concat(name);
}

Pora teraz na metody pobierające i ustawiające wartość właściwości:

public Object get(Object object)
        throws IllegalAccessException,
        InvocationTargetException {
    return getter.invoke(object);
}

public void set(Object object, Object value)
        throws IllegalAccessException,
        InvocationTargetException {
    // Check if the property is read only:
    if (isReadOnly()) {
        throw new IllegalAccessException(
                String.format(readOnlyProperty,
                        getName()));
    }
    // Set the value:
    setter.invoke(object, value);
}

Należy zauważyć, że w przypadku właściwości tylko do odczytu, setter zgłosi wyjątek.

Przyda się jeszcze metoda getType pobierająca typ właściwości:

public Class getType() {
    return getter.getReturnType();
}

Teraz pozostały już tylko metody wspomagające inicjowanie właściwości. Na początek ustalenie nazwy właściwości na podstawie nazwy gettera – metoda ta sprawdza także poprawność nazwy gettera:

protected void initName() throws NoSuchPropertyException {
    // Get the getter name:
    name = getter.getName();
    // Remove the name prefix:
    if (name.startsWith(getterPrefix)) {
        name = name.substring(getterPrefix.length());
    } else if (name.startsWith(booleanGetterPrefix)) {
        name = name.substring(booleanGetterPrefix.length());
    }
    // Check if the getter name is well formed:
    if (!getGetterName().equals(getter.getName())) {
        throw new NoSuchPropertyException(
                String.format(wrongGetterName,
                        getter.getName()));
    }
}

Następnie ustawienie settera na podstawie nazwy właściwości:

protected void initSetter() {
    try {
        setter = getter.getDeclaringClass().getMethod(
                setterPrefix.concat(name), getType());
    } catch (NoSuchMethodException e) {
        setter = null;
    }
}

Teraz można już zdefiniować konstruktory. Pierwszy tworzy właściwość na postawie konkretnej metody gettera:

public Property(Method getter) throws NoSuchPropertyException {
    this.getter = getter;
    initName();
    initSetter();
}

Drugi konstruktor wynajduje w podanej klasie właściwość o podanej nazwie:

public Property(Class<?> containingClass, String name)
throws NoSuchPropertyException {
    // Generate the name starting with capital letter:
    this.name = name.substring(0, 1).toUpperCase().
        concat(name.substring(1));
    // Generate possible getter names:
    String getterName = getterPrefix.concat(this.name);
    String booleanGetterName =
        booleanGetterPrefix.concat(this.name);
    // Search for the getter:
    try {
        getter = containingClass.getMethod(getterName);
        if (isBoolean()) {
            throw new NoSuchMethodException();
        }
    } catch (NoSuchMethodException e) {
        try {
            getter = containingClass.getMethod(
                    booleanGetterName);
            if (!isBoolean()) {
                throw new NoSuchMethodException();
            }
        } catch (NoSuchMethodException eBoolean) {
            getter = null;
        }
    }
    // If the getter is not found:
    if (getter == null) {
        throw new NoSuchPropertyException(
                String.format(noSuchProperty,
                        name,
                        containingClass.getName()));
    }
    // Initialize the property setter:
    initSetter();
}

Na sam koniec dodam jeszcze statyczną metodę, pobierającą tablicę wszystkich właściwości danej klasy:

public static Property[] getProperties(
        Class<?> containingClass) {
    // Create a list:
    ArrayList<Property> properties =
        new ArrayList<Property>();
    // Get the methods:
    Method[] methods = containingClass.getMethods();
    // Copy getters to the list:
    for (Method method : methods) {
        try {
            Property property = new Property(method);
            properties.add(property);
        } catch (Exception e) {
        }
    }
    // Convert to an array and return:
    Property[] result = new Property[properties.size()];
    result = properties.toArray(result);
    return result;
}

Zdefiniowana przez mnie klasa nie jest może tak elegancko połączona z java.lang.Class jak Field czy Method, ale zapewnia ich podstawową funkcjonalność. Nie podałem jeszcze definicji wyjątku NoSuchPropertyException, ale chyba wystarczy jak napiszę, że jest to klasa dziedzicząca po java.lang.Exception.

W niedalekiej przyszłości spróbuję pokazać jakieś użyteczne zastowanie dla napisanego dzisiaj kodu.

Napisz Komentarz

*