Java – programowanie refleksyjne i właściwości
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 AnnotatedElement i Member:
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ów i getteró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 – getGeterName i getSetterName – pobierające właściwe nazwy gettera i settera:
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.



